Skip to content

Latest commit

 

History

History
675 lines (501 loc) · 27.5 KB

File metadata and controls

675 lines (501 loc) · 27.5 KB

CoQuill — v2 Specification

Version: 0.2 Date: 2026-02-16 Platform: Claude Cowork Extends: coquill_mvp_spec.md (v0.1)


1. Overview

v2 adds four capabilities to CoQuill:

  1. Conditional logic — templates can use {% if %} / {% else %} / {% endif %} to include or exclude sections based on variable values. The interview skips questions that aren't relevant.
  2. Loops — templates can use {% for item in items %} / {% endfor %} for repeating sections. The interview collects lists with an "add another?" flow.
  3. Skill separation — the monolithic SKILL.md is split into three focused skills: Orchestrator, Analyzer, and Renderer.
  4. Developer configuration — an optional config.yaml file per template provides question overrides, grouping hints, validation rules, defaults, and choice types.

1.1 v2 Scope Constraints

These limits keep v2 tractable. They can be relaxed in v3.

  • Single-level nesting only — no {% for %} inside {% if %}, or {% if %} inside {% for %}. Each block is top-level relative to the template body.
  • Two condition forms only:
    • Truthiness: {% if variable_name %} — variable is truthy (boolean true, non-empty)
    • Equality: {% if variable_name == 'value' %} — variable equals a specific string
  • {% else %} supported, {% elif %} deferred{% else %} covers the common case. Multi-way branching via {% elif %} is deferred to v3.
  • No computed fields or expressions — variables are always collected from the user, never calculated.

2. Architecture

2.1 Skill Separation

v2 splits the single SKILL.md into three skills:

Skill Location Purpose Triggered by
Orchestrator .claude/skills/coquill/SKILL.md Entry point. Discovery, interview, confirmation, post-render. User ("prepare an NDA")
Analyzer .claude/skills/coquill-analyzer/SKILL.md Template parsing, variable extraction, manifest generation. Orchestrator
Renderer .claude/skills/coquill-renderer/SKILL.md Docx/HTML rendering, output validation. Orchestrator

The Orchestrator is the only user-facing skill. It invokes the Analyzer and Renderer via prompt-level instructions (e.g., "Run the coquill-analyzer skill on this template directory").

2.2 Skill Interface Contracts

Analyzer

  • Input: Template directory path (e.g., templates/meeting_notes/)
  • Output: manifest.yaml written to the template directory
  • Behaviour: Checks cache validity first. If config.yaml is present, merges overrides into the manifest.

Renderer

  • Input: Template path, format (docx or html), variable dictionary, output directory path
  • Output: Rendered document(s) in a job folder under output/
  • Behaviour: Renders, validates output for unfilled placeholders, reports results.

Orchestrator

  • Input: User's natural language request
  • Output: Completed document(s)
  • Behaviour: Discovery → Analyze → Interview Plan → Interview Loop → Confirmation → Render → Post-Render

3. Manifest v2 Schema

The manifest is the single source of truth the Orchestrator reads. The Analyzer produces it. The Orchestrator never reads config.yaml directly — config overrides are merged into the manifest by the Analyzer.

3.1 Full Schema

# ── Metadata ──────────────────────────────────────────────
schema_version: 2
template: "meeting_notes.md"
template_path: "templates/meeting_notes/meeting_notes.md"
format: md                      # "docx", "html", or "md"
analyzed_at: "2026-02-16T10:30:00Z"
variable_count: 14              # total unique variables (all scopes)

# ── From config.yaml (optional, passthrough) ──────────────
meta:                           # present only if config.yaml provides it
  display_name: "Meeting Notes"
  description: "Structured meeting notes with action items, optional sections, and meeting-type-specific content"

# ── Unconditional variables ───────────────────────────────
# Always collected during interview.
variables:
  - name: meeting_title
    label: "Meeting Title"
    type: text
    # Optional fields (from config.yaml merge):
    # question: "What is the title of this meeting?"
    # required: true

  - name: meeting_type
    label: "Meeting Type"
    type: choice               # new type in v2
    choices:                   # required when type is "choice"
      - standup
      - workshop
      - review
    # question: "What type of meeting was this?"
    # default: "standup"

  - name: meeting_date
    label: "Meeting Date"
    type: date

# ── Conditional blocks ────────────────────────────────────
# Variables gated by {% if %} / {% else %} blocks.
# Asked only if the condition evaluates to true/false.
conditionals:
  - condition: "meeting_type == 'workshop'"  # equality test
    gate_type: equality
    gate_variable: meeting_type              # which variable to check
    gate_value: "workshop"                   # what value triggers the if-branch
    if_variables:
      - name: workshop_materials
        label: "Workshop Materials"
        type: text
    else_variables: []

  - condition: "include_next_meeting"        # truthiness test
    gate_type: boolean                       # "boolean" or "equality"
    if_variables:
      - name: next_meeting_date
        label: "Next Meeting Date"
        type: date
      - name: next_meeting_topic
        label: "Next Meeting Topic"
        type: text
    else_variables: []                       # populated if {% else %} block has variables

# ── Loop blocks ───────────────────────────────────────────
# Variables inside {% for %} blocks. Collected as lists.
loops:
  - loop_var: "item"                         # iteration variable name
    collection: "action_items"               # collection variable name
    label: "Action Item"                     # human-readable label for the loop
    min_items: 1                             # minimum items required (default: 1)
    variables:                               # sub-variables per item
      - name: description
        label: "Description"
        type: text
      - name: assignee
        label: "Assignee"
        type: text
      - name: due_date
        label: "Due Date"
        type: date

# ── Dependencies (pre-computed) ───────────────────────────
# Gate variable → list of dependent variable names.
# Used by the Orchestrator to quickly determine which
# questions to skip without re-parsing conditions.
dependencies:
  meeting_type:
    - workshop_materials
    - items_reviewed
  include_next_meeting:
    - next_meeting_date
    - next_meeting_topic

# ── Interview groups (optional) ───────────────────────────
# If present (from config.yaml), the Orchestrator uses these
# groups instead of auto-grouping. If absent, the Orchestrator
# groups variables by logical affinity as in v1.
groups:
  - name: "Meeting Details"
    variables: [meeting_title, meeting_date, meeting_type, facilitator_name, meeting_location]

  - name: "Attendees & Agenda"
    variables: [attendees, agenda]

  - name: "Workshop Materials"
    condition: "meeting_type == 'workshop'"   # conditional group
    variables: [workshop_materials]

  - name: "Action Items"
    loop: action_items                        # loop group
    variables: [description, assignee, due_date]

  - name: "Next Meeting"
    condition: include_next_meeting           # conditional group
    variables: [next_meeting_date, next_meeting_topic]

# ── Validation rules (optional, from config.yaml) ────────
validation:
  - rule: "next_meeting_date > meeting_date"
    message: "The next meeting date must be after this meeting's date"

3.2 Schema Notes

schema_version — Always 2 for v2 manifests. The Analyzer regenerates the manifest if it encounters a v1 manifest (which has no schema_version field).

Backward compatibility — Templates with no conditionals or loops produce a v2 manifest with empty conditionals: [] and loops: []. The Orchestrator treats this identically to v1 behaviour.

Variable fields reference:

Field Source Required Description
name Analyzer Yes Variable name as it appears in the template
label Analyzer (auto) or config Yes Human-readable label
type Analyzer (inferred) or config Yes text, date, number, email, phone, boolean, choice
question config only No Custom interview question
description config only No Context for Claude when asking the question
default config only No Default value (special: "today" for dates)
required config only No Whether the variable must have a value (default: true)
format_hint config only No Format guidance shown to user (e.g., "DD/MM/YYYY")
choices config only When type=choice List of valid values
validation config only No Validation rules (min, max for numbers)

New types in v2:

Type Description Interview behaviour
boolean True/false value Yes/no question
choice One of a fixed set of values Present options, validate against choices list

4. Developer Configuration (config.yaml)

4.1 Purpose

An optional YAML file placed alongside the template by the template developer. It overrides the Analyzer's auto-inferred values and controls the interview flow. If absent, the Analyzer and Orchestrator use their default behaviours.

4.2 Full Format

# ── Template metadata ─────────────────────────────────────
meta:
  display_name: "Meeting Notes"              # shown to user during discovery
  description: "Structured meeting notes with action items, optional sections, and meeting-type-specific content"
  category: "productivity"                   # reserved for v3 categorisation

# ── Variable overrides ────────────────────────────────────
# Keys are variable names from the template.
# Any field here overrides the Analyzer's auto-inferred values.
variables:
  meeting_title:
    label: "Meeting Title"
    question: "What is the title of this meeting?"
    required: true

  meeting_date:
    question: "When was the meeting held?"
    default: "today"                         # resolves to current date at interview time
    format_hint: "DD/MM/YYYY"

  meeting_type:
    type: choice
    choices:
      - standup
      - workshop
      - review
    question: "What type of meeting was this?"
    default: "standup"

  decisions_made:
    type: boolean
    question: "Were any decisions made during this meeting?"
    default: false

  include_next_meeting:
    type: boolean
    question: "Is a follow-up meeting scheduled?"
    default: false

# ── Interview groups ──────────────────────────────────────
# Overrides the Orchestrator's auto-grouping.
# Each group defines which variables are asked together.
groups:
  - name: "Meeting Details"
    variables: [meeting_title, meeting_date, meeting_type, facilitator_name, meeting_location]

  - name: "Attendees & Agenda"
    variables: [attendees, agenda]

  - name: "Workshop Materials"
    condition: "meeting_type == 'workshop'"    # only asked if meeting type is workshop
    variables: [workshop_materials]

  - name: "Action Items"
    loop: action_items                         # collected as a list
    variables: [description, assignee, due_date]

  - name: "Next Meeting"
    condition: include_next_meeting            # only asked if gate is true
    variables: [next_meeting_date, next_meeting_topic]

# ── Cross-field validation ────────────────────────────────
# Evaluated during the Confirmation phase after all values are collected.
# Rules are simple comparisons the Orchestrator can evaluate.
validation:
  - rule: "next_meeting_date > meeting_date"
    message: "The next meeting date must be after this meeting's date"

4.3 Merge Rules

When config.yaml is present, the Analyzer merges it into the manifest as follows:

  1. meta — copied to manifest as-is
  2. variables — for each variable name in config, override matching fields in the manifest. Config values take precedence. Fields not in config retain their auto-inferred values.
  3. groups — copied to manifest as the groups section. If absent, the manifest has no groups section and the Orchestrator auto-groups.
  4. validation — copied to manifest as-is

The Analyzer does not create variables that appear in config but not in the template. Config can only override variables the template actually uses.


5. Analyzer v2

5.1 Extended Extraction

The v1 Analyzer only extracts {{ variable_name }} patterns. The v2 Analyzer additionally detects:

Pattern Regex Purpose
If open \{%[-\s]*if\s+(.+?)\s*[-]?%\} Start of conditional block
Else \{%[-\s]*else\s*[-]?%\} Else branch
Endif \{%[-\s]*endif\s*[-]?%\} End of conditional block
For open \{%[-\s]*for\s+(\w+)\s+in\s+(\w+)\s*[-]?%\} Start of loop block
Endfor \{%[-\s]*endfor\s*[-]?%\} End of loop block
Variable \{\{\s*([\w.]+)\s*\}\} Variable reference (now supports dotted names for loop sub-vars)

5.2 Two-Pass Analysis

Pass 1 — Block structure:

Scan the full text content and identify all block boundaries. Build a list of scopes:

TEMPLATE_TEXT
├── [top-level] ... {{ meeting_title }} ... {{ meeting_date }} ...
├── [if meeting_type == 'workshop']
│   └── [if-branch] ... {{ workshop_materials }} ...
├── [if meeting_type == 'review']
│   └── [if-branch] ... {{ items_reviewed }} ...
├── [for item in action_items]
│   └── [loop-body] ... {{ item.description }} ... {{ item.assignee }} ... {{ item.due_date }} ...
└── [if include_next_meeting]
    ├── [if-branch] ... {{ next_meeting_date }} ... {{ next_meeting_topic }} ...
    └── [else-branch] ... (static text, no variables) ...

Pass 2 — Variable classification:

For each {{ variable }} found, classify it based on its scope:

Scope Classification Manifest location
Top-level Unconditional variables
Inside {% if %} if-branch Conditional (if) conditionals[].if_variables
Inside {% if %} else-branch Conditional (else) conditionals[].else_variables
Inside {% for %} body Loop-scoped loops[].variables

5.3 Boolean Type Inference

If a variable name appears only as a condition in {% if variable_name %} (truthiness test) and never inside {{ variable_name }}, infer it as type: boolean. The Orchestrator will ask a yes/no question for it.

If a variable appears both as a condition and as a {{ }} reference, keep its inferred type from the {{ }} context (it's used as both a gate and a display value).

5.4 Loop Sub-Variable Extraction

For {% for item in action_items %} containing {{ item.description }}:

  1. action_items is the collection variable (type: list, not directly asked)
  2. item is the loop variable (iteration alias)
  3. item.description → sub-variable description (strip the loop variable prefix)
  4. Apply standard type inference to the sub-variable name: descriptiontext, assigneetext, due_datedate

5.5 Condition Parsing

The Analyzer extracts condition metadata for each {% if %} block:

Condition form Example gate_type gate_variable gate_value
Truthiness {% if include_ip %} boolean include_ip
Equality {% if method == 'bank' %} equality method "bank"

For equality conditions, the Analyzer extracts the variable name and comparison value using regex: (\w+)\s*==\s*['\"](.+?)['\"]

5.6 Docx-Specific Handling

In .docx files, Word may split Jinja2 tags across multiple XML runs (e.g., {% in one run and if x %} in another). The Analyzer must handle this:

  1. Load the template via docxtpl.DocxTemplate(path)
  2. Access the preprocessed XML source where docxtpl has merged split runs
  3. Apply regex extraction on the merged text

If accessing docxtpl internals proves fragile, fall back to: extract all text by concatenating paragraph runs, table cell text, header/footer text, then apply regex. This loses positional information but is robust.

For HTML templates, read the raw file text directly — no preprocessing needed.

5.7 Dependency Graph Generation

After extracting all conditionals, the Analyzer builds the dependencies map:

dependencies = {}
for cond in conditionals:
    gate_var = cond["gate_variable"]  # or condition name for boolean
    dep_vars = [v["name"] for v in cond["if_variables"] + cond["else_variables"]]
    dependencies.setdefault(gate_var, []).extend(dep_vars)

5.8 Edge Cases

  • Malformed blocks (e.g., {% if %} without {% endif %}) — warn the user, skip the block, extract variables as unconditional
  • Nested blocks — warn the user that nesting is not supported in v2; treat inner block as part of the outer block's scope
  • Variable in multiple scopes — if the same variable name appears both at top-level and inside a conditional, classify it as unconditional (it's always needed)
  • Empty conditional/loop body — valid; record the block with no variables
  • {% else %} without variables — record else_variables: []

6. Orchestrator v2

6.1 Interview Plan Changes

The v1 interview plan groups all variables by logical affinity. The v2 plan must additionally account for:

  1. Gate variables — boolean and choice variables that control conditionals must be asked before their dependent groups
  2. Conditional groups — groups that are only asked if a gate condition is met
  3. Loop groups — groups that collect lists, planned after unconditional/conditional groups

Planning algorithm:

  1. Read the manifest's variables, conditionals, loops, dependencies, and groups
  2. If groups is present (from config), use it as the interview structure
  3. If groups is absent, auto-group: a. Separate unconditional variables into logical groups (same as v1) b. Ensure gate variables are placed in groups that come before their dependent conditional groups c. Add one group per conditional block d. Add one group per loop block, placed last
  4. Order: unconditional groups → conditional groups (interleaved after their gates) → loop groups

6.2 Conditional Interview Flow

During the interview loop:

  1. Ask the group containing the gate variable (e.g., "Does this engagement involve IP assignment?")
  2. Record the answer
  3. Evaluate the condition:
    • Boolean gate: true if user answered yes/affirmative, false otherwise
    • Equality gate: true if the collected value matches gate_value
  4. If condition is true: ask the if_variables group
  5. If condition is false and else_variables is non-empty: ask the else_variables group
  6. If condition is false and else_variables is empty: skip, move to next group

6.3 Loop Collection Flow

For each loop group:

  1. Introduce: "Now let's add the project milestones. I'll ask for each one individually."
  2. First item: Ask for all sub-variables as a group: "What's the description, due date, and amount for the first milestone?"
  3. Confirm item: "Got it — Milestone 1: 'Design phase', due 15 March 2026, $5,000."
  4. Continue: "Would you like to add another milestone?"
  5. Repeat steps 2-4 until the user says no
  6. Minimum check: At least min_items (default 1) must be collected
  7. Summary: "I have 3 milestones recorded: [numbered list]"

The collected data is a list of dictionaries:

{
    "milestones": [
        {"description": "Design phase", "date": "2026-03-15", "amount": "5000"},
        {"description": "Development", "date": "2026-04-30", "amount": "10000"},
    ]
}

6.4 Confirmation Phase Changes

The v2 confirmation summary:

  1. Shows unconditional values grouped as before
  2. Shows conditional sections with their gate status:
    • If included: display the collected values
    • If skipped: "IP Assignment: Skipped (not applicable)"
  3. Shows loop items as a numbered list
  4. Allows the user to:
    • Edit any individual value
    • Change a gate answer (which reveals or hides dependent sections; if revealing, asks the newly-required questions)
    • Add, edit, or remove loop items

6.5 Validation

If the manifest includes validation rules (from config), the Orchestrator evaluates them during confirmation:

  1. After the user confirms all values, check each rule
  2. If a rule fails, report the error message and ask the user to correct the relevant values
  3. Re-validate after corrections
  4. Do not proceed to rendering until all rules pass

7. Renderer v2

7.1 Changes from v1

The renderer requires minimal changes because docxtpl and jinja2 natively support {% if %} and {% for %}:

  1. Data structure — the context dictionary now contains both flat values and list values (for loops). No code change needed — both docxtpl.render(context) and jinja2.Template.render(context) accept nested structures.

  2. Boolean coercion — ensure boolean variables are passed as Python True/False, not strings "yes"/"no". The renderer should coerce type: boolean variables before rendering.

  3. Validation scan update — the post-render validator should check for unrendered {% %} tags in addition to {{ }} placeholders. Add regex: \{%.*?%\}. An unrendered control tag means a conditional or loop was not processed correctly.


8. Directory Structure (v2)

coquill/
├── CLAUDE.md
├── README.md
├── LICENSE
├── .gitignore
├── .claude/
│   └── skills/
│       ├── coquill/
│       │   └── SKILL.md              # Orchestrator (entry point)
│       ├── coquill-analyzer/
│       │   └── SKILL.md              # Analyzer
│       └── coquill-renderer/
│           └── SKILL.md              # Renderer
├── docs/
│   ├── coquill_mvp_spec.md           # MVP specification
│   └── coquill_v2_spec.md            # This file
├── templates/
│   ├── _examples/
│   │   ├── Bonterms_Mutual_NDA/
│   │   │   ├── Bonterms-Mutual-NDA.docx
│   │   │   └── Readme.md
│   │   ├── invoice/
│   │   │   └── invoice.html
│   │   └── meeting_notes/            # v2 example (md, conditionals + loops)
│   │       ├── meeting_notes.md
│   │       └── config.yaml
│   └── <user_template>/
│       ├── <name>.docx or <name>.html
│       ├── manifest.yaml             # auto-generated
│       └── config.yaml               # optional, developer-authored
└── output/
    └── <job_name>/

9. Example Session (v2 — Conditionals + Loops)

User: I need to prepare meeting notes

Claude: I'll use the Meeting Notes template. I've found 14 fields to
        complete across several sections. Some sections are optional depending
        on your answers.

        Let's start with the meeting details. What is the title of the
        meeting, and when was it held?

User: Q1 Sprint Retrospective, 28 February 2026

Claude: What type of meeting was this?
        Options: standup, workshop, or review

User: Workshop

Claude: Who facilitated the meeting, and where was it held?

User: Sarah Chen, Conference Room B

Claude: Now the attendees and agenda. Who attended the meeting?

User: Sarah Chen, James Tan, Priya Sharma, David Kim

Claude: What was the agenda?

User: Review sprint outcomes, workshop on improving CI pipeline, assign follow-up tasks

Claude: Since this was a workshop, what materials were used or distributed?

User: CI pipeline architecture diagrams, benchmark results spreadsheet

Claude: Summarize the key discussion points.

User: Reviewed sprint velocity trends. Discussed bottlenecks in the CI pipeline. Workshopped solutions including parallel test execution and caching strategies.

Claude: Now let's add the action items. I'll ask for each one.
        What's the description, assignee, and due date for the first item?

User: Implement parallel test execution, James Tan, 7 March 2026

Claude: Got it — Action Item 1: Implement parallel test execution,
        assigned to James Tan, due 7 March 2026.
        Would you like to add another action item?

User: Yes. Set up build caching, Priya Sharma, 14 March 2026

Claude: Action Item 2: Set up build caching, assigned to Priya Sharma,
        due 14 March 2026. Another?

User: No, that's all

Claude: Were any decisions made during this meeting?

User: Yes

Claude: What decisions were made?

User: Agreed to adopt parallel test execution as the default CI strategy starting next sprint.

Claude: Is a follow-up meeting scheduled?

User: Yes, 7 March 2026, CI pipeline progress review

Claude: Who recorded these notes?

User: David Kim

Claude: Here's a summary of all the details:

        Meeting Details:
        - Title: Q1 Sprint Retrospective
        - Date: 28/02/2026
        - Type: Workshop
        - Facilitator: Sarah Chen
        - Location: Conference Room B

        Attendees: Sarah Chen, James Tan, Priya Sharma, David Kim
        Agenda: Review sprint outcomes, workshop on improving CI pipeline,
                assign follow-up tasks

        Workshop Materials: CI pipeline architecture diagrams, benchmark
                           results spreadsheet

        Action Items:
        1. Implement parallel test execution — James Tan — 7 March 2026
        2. Set up build caching — Priya Sharma — 14 March 2026

        Decisions: Agreed to adopt parallel test execution as the default
                   CI strategy starting next sprint.

        Next Meeting: 7 March 2026 — CI pipeline progress review

        Recorder: David Kim

        Shall I produce the document?

User: Yes

Claude: Done! Your meeting notes have been saved as:
        output/meeting_notes_q1_sprint_retrospective_2026-02-28/
        meeting_notes_q1_sprint_retrospective_2026-02-28.md

10. Technical Decisions (v2)

Decision Choice Rationale
Nesting depth Single-level only Covers majority of real templates; nested flows too complex for prompt-based skills
Condition forms Truthiness + equality Evaluable from collected answers without expression parsing
Config → Manifest Analyzer merges config into manifest Orchestrator reads one file; config changes only affect Analyzer
{% elif %} Deferred to v3 Multi-way branching complicates manifest and interview; rare in practice
Skill invocation Prompt-level references No programmatic API in Cowork; consistent with how MVP works
Loop minimum At least 1 item (configurable via min_items) A {% for %} block with 0 items renders empty
Manifest format YAML (unchanged from MVP implementation) Human-readable, extensible, consistent
schema_version Integer, starting at 2 Forward compatibility; Analyzer regenerates stale manifests