diff --git a/progi/alembic/versions/0005_drop_input_output_spec.py b/progi/alembic/versions/0005_drop_input_output_spec.py new file mode 100644 index 0000000..fad3fb5 --- /dev/null +++ b/progi/alembic/versions/0005_drop_input_output_spec.py @@ -0,0 +1,28 @@ +"""drop input_spec and output_spec from steps + +Revision ID: 0005 +Revises: 4643697c3984 +Create Date: 2026-06-27 +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "0005" +down_revision: Union[str, None] = "4643697c3984" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + with op.batch_alter_table("steps") as batch_op: + batch_op.drop_column("input_spec") + batch_op.drop_column("output_spec") + + +def downgrade() -> None: + with op.batch_alter_table("steps") as batch_op: + batch_op.add_column(sa.Column("input_spec", sa.JSON(), nullable=True)) + batch_op.add_column(sa.Column("output_spec", sa.JSON(), nullable=True)) diff --git a/progi/db.py b/progi/db.py index 58fcfdf..e3552a0 100644 --- a/progi/db.py +++ b/progi/db.py @@ -272,7 +272,7 @@ def save_workflow( "name": str, "description": str, "steps": [ - {"order": int, "name": str, "input_spec": {...}, "output_spec": {...}} + {"order": int, "name": str} ], "edges": [ # optional; auto-generated if absent {"from": str, "to": str, "condition": {...} | null, "priority": int} @@ -301,8 +301,6 @@ def save_workflow( workflow_id=wf_id, order=step["order"], name=step["name"], - input_spec=step["input_spec"], - output_spec=step["output_spec"], requires_approval=step.get("requires_approval", False), ) ).inserted_primary_key[0] @@ -377,8 +375,6 @@ def list_workflows(cfg: Config) -> list[dict[str, Any]]: "id": s["id"], "order": s["order"], "name": s["name"], - "input_spec": s["input_spec"], - "output_spec": s["output_spec"], } ) @@ -448,8 +444,6 @@ def add_step_to_workflow( workflow_id: int, name: str, order: int, - input_spec: dict[str, Any], - output_spec: dict[str, Any], playbook: str | None = None, requires_approval: bool = False, *, @@ -489,8 +483,6 @@ def add_step_to_workflow( workflow_id=workflow_id, order=order, name=name, - input_spec=input_spec, - output_spec=output_spec, requires_approval=requires_approval, ) ).inserted_primary_key[0] @@ -652,8 +644,6 @@ def update_step( step_id: int, *, name: str | None = None, - input_spec: dict[str, Any] | None = None, - output_spec: dict[str, Any] | None = None, order: int | None = None, requires_approval: bool | None = None, ) -> dict[str, Any]: @@ -661,10 +651,6 @@ def update_step( values: dict[str, Any] = {} if name is not None: values["name"] = name - if input_spec is not None: - values["input_spec"] = input_spec - if output_spec is not None: - values["output_spec"] = output_spec if order is not None: values["order"] = order if requires_approval is not None: @@ -735,7 +721,6 @@ def create_task(cfg: Config, name: str, workflow_id: int, description: str = "") result["first_step"] = { "name": first["name"], "order": first["order"], - "input_spec": first["input_spec"], } return result @@ -798,11 +783,7 @@ def start_task(cfg: Config, task_id: int) -> dict[str, Any]: ) first_step = _start_step(conn, task["workflow_id"]) - input_spec = first_step["input_spec"] - input_data = { - "description": input_spec.get("description", ""), - "source": "static", - } + input_data = {"value": task["description"] or ""} _activate_step(conn, task_id, task["workflow_id"], first_step, input_data) @@ -849,8 +830,8 @@ def start_or_continue_task(cfg: Config, task_id: int) -> dict[str, Any]: - todo → starts the task (todo → in_progress), returns full step context. - in_progress → returns full step context so the agent can resume. - Full context: task info, current step name + position, input_data, output_spec, - playbook content, and progress_notes (if any). + Full context: task info, current step name, input_data, playbook content, + and progress_notes (if any). """ engine = get_engine(cfg) with engine.connect() as conn: @@ -899,7 +880,6 @@ def start_or_continue_task(cfg: Config, task_id: int) -> dict[str, Any]: "current_step": { "name": current_step["name"], "input_data": si["input_data"] if si else None, - "output_spec": current_step["output_spec"], "playbook": pb[0] if pb else None, "requires_approval": bool(current_step["requires_approval"]), }, @@ -992,41 +972,10 @@ def submit_output( ) return {"status": "done"} - # Resolve input_data for next step - next_input_spec = next_step["input_spec"] - if next_input_spec.get("source") == "previous_step_output": - from_step_name = next_input_spec.get("from_step") - if from_step_name and from_step_name != current_step["name"]: - # Pull output from a specific earlier step by name - source_step_row = conn.execute( - sa.select(steps.c.id).where( - steps.c.workflow_id == task["workflow_id"], - steps.c.name == from_step_name, - ) - ).first() - if source_step_row is None: - raise ValueError(f"from_step '{from_step_name}' not found in workflow.") - source_si = conn.execute( - sa.select(step_instances.c.output).where( - step_instances.c.task_id == task_id, - step_instances.c.step_id == source_step_row[0], - step_instances.c.status == "complete", - ).order_by(step_instances.c.id.desc()) - ).first() - source_output = source_si[0] if source_si else {} - else: - # Default: use the step we just completed - source_output = output - - next_input_data = { - "value": source_output if isinstance(source_output, str) else source_output.get("value", source_output), - "from_step": from_step_name or current_step["name"], - } - else: - next_input_data = { - "description": next_input_spec.get("description", ""), - "source": "static", - } + # Pass the current step's output as the next step's input + next_input_data = { + "value": output if isinstance(output, str) else output.get("value", output), + } # Activate next step (create instance + update task pointer) conn.execute( @@ -1114,8 +1063,6 @@ def get_workflow_with_playbooks(cfg: Config, workflow_id: int) -> dict[str, Any] "id": s["id"], "order": s["order"], "name": s["name"], - "input_spec": s["input_spec"], - "output_spec": s["output_spec"], "playbook": playbook_by_step.get(s["id"]), } for s in step_rows @@ -1143,8 +1090,6 @@ def export_workflow(cfg: Config, workflow_id: int) -> dict[str, Any]: { "order": s["order"], "name": s["name"], - "input_spec": s["input_spec"], - "output_spec": s["output_spec"], } for s in wf["steps"] ], @@ -1244,8 +1189,6 @@ def get_step_detail(cfg: Config, workflow_id: int, step_id: int) -> dict[str, An "id": step["id"], "order": step["order"], "name": step["name"], - "input_spec": step["input_spec"], - "output_spec": step["output_spec"], "playbook": pb, "library_entry_id": step["library_entry_id"], }, diff --git a/progi/mcp_server.py b/progi/mcp_server.py index 52d471f..04912da 100644 --- a/progi/mcp_server.py +++ b/progi/mcp_server.py @@ -84,13 +84,9 @@ def start_or_continue_task(task_id: int) -> dict: - todo → starts the task (todo → in_progress) and returns step context. - in_progress → returns step context so the agent can resume. - Context includes task info, the current step name + position, input_data, - output_spec (the expected format/type of the deliverable), the playbook - markdown, and progress_notes (if any). - - Before calling finish_step, verify that your output satisfies output_spec - (correct type, meets constraints, includes any fields referenced by branching - conditions). + Context includes task info, the current step name, input_data, the playbook + markdown, and progress_notes (if any). Follow the playbook's Output section + for what to produce and how to report it back. Always show the user the monitoring_url from the response. """ @@ -199,7 +195,7 @@ def get_process_skeleton_prompt() -> str: The returned prompt covers both passes: - Pass 1: work with the user to produce and approve a skeleton JSON - (steps with input/output specs). + (workflow name, description, steps, and edges). - Pass 2: once the user approves, generate all step playbooks silently (no further tool calls needed) and call save_workflow with everything. """ @@ -241,7 +237,7 @@ def list_workflows() -> dict: @mcp.tool(title="Get Workflow") def get_workflow(workflow_id: int) -> dict: - """Return a workflow's full detail: steps (with order, specs, playbook) and edges. + """Return a workflow's full detail: steps (with order, playbook) and edges. Use this as a read step before calling add_step, edit_step, or delete_step so you have the current step IDs, order values, and playbook content. @@ -256,8 +252,6 @@ def add_step( workflow_id: int, name: str, order: int, - input_spec: dict, - output_spec: dict, playbook: str = "", requires_approval: bool = False, reorder: bool = True, @@ -285,8 +279,6 @@ def add_step( workflow_id=workflow_id, name=name, order=order, - input_spec=input_spec, - output_spec=output_spec, playbook=playbook or None, requires_approval=requires_approval, reorder=reorder, @@ -297,8 +289,6 @@ def add_step( def edit_step( step_id: int, name: str | None = None, - input_spec: dict | None = None, - output_spec: dict | None = None, playbook: str | None = None, requires_approval: bool | None = None, ) -> dict: @@ -321,8 +311,6 @@ def edit_step( _cfg, step_id, name=name, - input_spec=input_spec, - output_spec=output_spec, requires_approval=requires_approval, ) updated_playbook = None diff --git a/progi/models.py b/progi/models.py index 3f020bf..c6738c8 100644 --- a/progi/models.py +++ b/progi/models.py @@ -3,7 +3,7 @@ This module is the single source of truth for the schema. It is imported by ``db.py`` (queries/mutations) and by Alembic (autogenerate migrations). -``input_spec`` / ``output_spec`` / ``input_data`` / ``output`` are JSON columns: +``input_data`` / ``output`` are JSON columns on step_instances: SQLAlchemy serialises dicts to TEXT on write and parses them back on read, so the Python side always deals in plain dicts (no manual json.dumps/loads). """ @@ -36,8 +36,6 @@ # "order" is a SQL keyword; SQLAlchemy quotes it automatically. sa.Column("order", sa.Integer, nullable=False), sa.Column("name", sa.String(255), nullable=False), - sa.Column("input_spec", sa.JSON, nullable=False), - sa.Column("output_spec", sa.JSON, nullable=False), # When True, the agent must present the step output to the user and get # explicit approval before calling finish_step. sa.Column("requires_approval", sa.Boolean, nullable=False, server_default="0"), diff --git a/progi/prompts/workflow_skeleton.md b/progi/prompts/workflow_skeleton.md index b3e7080..f7d1e70 100644 --- a/progi/prompts/workflow_skeleton.md +++ b/progi/prompts/workflow_skeleton.md @@ -6,8 +6,8 @@ by **edges** that define how execution flows between them — including conditio branches. Each step will later get a playbook (that happens in Pass 2 — not now). Your job in this pass is to turn the user's plain-language description of a -workflow into a **structured process skeleton**: the list of steps with their -input and output specifications, plus the edges that connect them. +workflow into a **structured process skeleton**: the list of steps plus the edges +that connect them. ## How to work @@ -15,10 +15,12 @@ input and output specifications, plus the edges that connect them. 2. If the workflow is ambiguous (unclear steps, unclear deliverables, unclear branching logic), ask the user clarifying questions before producing the skeleton. Confirm the step list and branching logic with the user before - finalizing — playbooks are written against these specs, so the structure must - be right first. -3. When a step has conditional outgoing edges, consider adding helper field(s) in the `output_spec` for the condition check(s). -3. Produce the skeleton as a single JSON object (see schema below). + finalizing — playbooks are written against this structure, so it must be right + first. +3. When a step has conditional outgoing edges, the step's playbook will need to + produce an output dict containing the field(s) used by those conditions. Keep + this in mind when naming steps and edges. +4. Produce the skeleton as a single JSON object (see schema below). ## Output schema @@ -29,23 +31,13 @@ Return exactly one JSON object of this shape: "name": "Blog Post", "description": "Workflow for researching, writing, editing, and publishing a blog post.", "steps": [ - { - "order": 1, - "name": "Research", - "input_spec": { - "description": "What this step needs to begin.", - "source": "static", - "from_step": null - }, - "output_spec": { - "type": "file", - "description": "The deliverable that proves the step is done.", - "constraints": "e.g. must be a markdown file" - } - } + {"order": 1, "name": "Research"}, + {"order": 2, "name": "Outline"}, + {"order": 3, "name": "Draft"} ], "edges": [ - {"from": "Research", "to": "Outline", "condition": null, "priority": 0} + {"from": "Research", "to": "Outline", "condition": null, "priority": 0}, + {"from": "Outline", "to": "Draft", "condition": null, "priority": 0} ] } ``` @@ -57,9 +49,9 @@ For a branching workflow, the edges express the routing logic: "name": "Content Review", "description": "Write and review before publishing, with an optional fast-track.", "steps": [ - {"order": 1, "name": "Draft", "input_spec": {"description": "Topic.", "source": "static", "from_step": null}, "output_spec": {"type": "file", "description": "Draft", "constraints": "include review_needed boolean"}}, - {"order": 2, "name": "Edit", "input_spec": {"description": "Draft.", "source": "previous_step_output", "from_step": "Draft"}, "output_spec": {"type": "file", "description": "Edited doc", "constraints": "markdown"}}, - {"order": 3, "name": "Publish", "input_spec": {"description": "Doc to publish.", "source": "previous_step_output", "from_step": "Edit"}, "output_spec": {"type": "url", "description": "Published URL", "constraints": "valid URL"}} + {"order": 1, "name": "Draft"}, + {"order": 2, "name": "Edit"}, + {"order": 3, "name": "Publish"} ], "edges": [ {"from": "Draft", "to": "Edit", "condition": {"field": "review_needed", "operator": "eq", "value": true}, "priority": 0}, @@ -73,15 +65,12 @@ For a branching workflow, the edges express the routing logic: - `order` — integer used only for display/layout ordering; it does **not** determine execution order (edges do). Use sequential 1-based integers. -- `input_spec.source` — either `"static"` (the step starts from information given - at task creation) or `"previous_step_output"` (it consumes a prior step's - output). When `previous_step_output`, set `from_step` to the **name** of the - source step (e.g. `"Draft"`); otherwise `from_step` is `null`. -- `output_spec.type` — one of `"file"`, `"url"`, or `"text"`. -- The **entry step** (no incoming edges) should almost always have - `input_spec.source = "static"`. - Keep steps coarse enough to be meaningful deliverables (3–6 steps is typical), not micro-tasks. +- **Data flow**: the first step automatically receives `input_data.value` = the + task's description/topic (whatever the user provided when creating the task). + Every subsequent step automatically receives `input_data.value` = the previous + step's output value. The playbook for each step describes how to use it. - `edges` — list of transitions. Each edge has: - `from` — name of the source step - `to` — name of the destination step @@ -99,10 +88,7 @@ For a branching workflow, the edges express the routing logic: linear edges from the `order` values. - **Loops** are expressed as back-edges: an edge from a later step back to an earlier step. Use a conditional edge (priority 0) for the exit path and a - second edge (priority 1, condition or `null`) for the loop-back. The looping - step should use `source: "previous_step_output"` to carry state forward across - iterations — the runtime always uses the most recent completed output for the - named `from_step`. + second edge (priority 1, condition or `null`) for the loop-back. ## After the user approves the skeleton @@ -123,33 +109,29 @@ designed. A playbook is one self-contained markdown document that the AI **agent** (the assistant inside the user's harness — Claude Code, Cursor, etc.) will follow to perform that step at runtime. -For each step in the skeleton, write a playbook against its `input_spec`, -`output_spec`, and `requires_approval` flag. Collect all playbooks into the +For each step in the skeleton, write a playbook. Collect all playbooks into the `playbooks_by_step` map (step name → markdown string) and pass them to `save_workflow`. -## What a good playbook contains - -Write a markdown document that includes: - -1. **A heading** naming the step and its role in the larger workflow. -2. **Input** — what the step starts from. If `input_spec.source` is - `previous_step_output`, the prior step's output is available; say how to find - or use it. If `static`, describe what to ask the user for. -3. **Working instructions** — the concrete actions the agent takes to produce - the deliverable. -4. **Human-involvement points** — there is no separate "human step": every step - is run by the agent, and the playbook decides when to pull the human in. Be - explicit, e.g. "ask the user to confirm tone before drafting". The - `requires_approval` flag on the step controls whether the agent must present - the final output to the user and receive explicit sign-off before calling - `finish_step`. If it is true, the playbook should describe what to show the - user and how to handle requested changes before submitting. -5. **Output** — exactly what deliverable satisfies `output_spec` (a file path, a - URL, or text) and how the agent reports it back. The agent submits this via - `finish_step`, which advances the task to the next step. +## Playbook structure + +Every playbook must contain exactly these four `##` sections, in this order. No `#` (h1) heading. Subsections (`###`, `####`, etc.) are allowed within each section as needed. + +### `## Input` +Describe what the step starts from. +- For the **first step**: `input_data.value` contains the task description/topic the user provided at task creation. Say what to do with it (e.g. ask the user for clarification if needed). +- For **all subsequent steps**: `input_data.value` contains the previous step's output (typically a file path or URL). Say where to find it and how to use it. + +### `## Instructions` +The concrete actions the agent takes to produce the deliverable. Be specific and actionable. + +### `## Human involvement` +Explicitly state every point where a human must act or approve something, and what they need to do. If no human involvement is needed, state that clearly (e.g. "None — proceed autonomously."). + +### `## Output` +Exactly what deliverable the step produces, what format, and how the agent reports it back via `finish_step`. For steps with conditional outgoing edges, explicitly list the output dict fields that edge conditions reference (e.g. "`review_needed`: true/false"). ## Style -- Address the agent in the second person ("You are working on…"). +- Address the agent in the second person. - Be specific and actionable; no fluff. - Keep each playbook to a single markdown document — no separate files. diff --git a/progi/seed.py b/progi/seed.py index d96287e..0db0501 100644 --- a/progi/seed.py +++ b/progi/seed.py @@ -14,208 +14,113 @@ "name": "Blog Post", "description": "Workflow for researching, writing, editing, and publishing a blog post.", "process": [ - { - "order": 1, - "name": "Research", - "input_spec": { - "description": "Topic name and any initial notes or reference links the user supplies.", - "source": "static", - "from_step": None, - }, - "output_spec": { - "type": "file", - "description": "Research notes covering key points, credible sources, and angles for the post.", - "constraints": "must be a markdown file", - }, - }, - { - "order": 2, - "name": "Outline", - "input_spec": { - "description": "Research notes from the Research step.", - "source": "previous_step_output", - "from_step": "Research", - }, - "output_spec": { - "type": "file", - "description": "Structured outline with headings and key bullet points per section.", - "constraints": "must be a markdown file", - }, - }, - { - "order": 3, - "name": "Draft", - "input_spec": { - "description": "Approved outline from the Outline step.", - "source": "previous_step_output", - "from_step": "Outline", - }, - "output_spec": { - "type": "file", - "description": "Full first draft of the blog post.", - "constraints": "must be a markdown file", - }, - }, - { - "order": 4, - "name": "Edit", - "input_spec": { - "description": "First draft from the Draft step.", - "source": "previous_step_output", - "from_step": "Draft", - }, - "output_spec": { - "type": "file", - "description": "Edited, publish-ready version of the post.", - "constraints": "must be a markdown file", - }, - }, - { - "order": 5, - "name": "Publish", - "input_spec": { - "description": "Final edited post from the Edit step.", - "source": "previous_step_output", - "from_step": "Edit", - }, - "output_spec": { - "type": "url", - "description": "Link to the live published post.", - "constraints": "must be a valid URL", - }, - }, + {"order": 1, "name": "Research"}, + {"order": 2, "name": "Outline"}, + {"order": 3, "name": "Draft"}, + {"order": 4, "name": "Edit"}, + {"order": 5, "name": "Publish"}, ], } PLAYBOOKS: dict[str, str] = { - "Research": """# Step: Research - -You are working on the **Research** step of a Blog Post task. -Your goal is to gather enough information on the topic to write a well-informed post. + "Research": """## Input -## Input +`input_data.value` — the topic and any initial notes the user provided when creating the task. -The topic name (and any initial notes) were provided when the task was created. -Ask the user for the topic and any seed links or constraints before proceeding if they are not already clear. - -## Working instructions +## Instructions 1. Ask the user: "What is the exact topic and target audience? Do you have any reference links or key points you want included?" 2. Wait for the user's reply. 3. Gather information from the provided links plus your own knowledge. Identify 5-8 key points or angles most relevant to the target audience. 4. Assess source credibility; note any conflicting information. +5. Save research notes to `research.md`. The file must list the main talking points with short explanations, cite sources with URLs where applicable, and note any open questions or gaps. -## Output - -Save your research notes to `research.md` in the working directory. The file must: -- List the main talking points with short explanations. -- Cite sources with URLs where applicable. -- Note any open questions or gaps. +## Human involvement -Report back that `research.md` is ready once saved.""", - "Outline": """# Step: Outline +None required beyond the initial clarification in step 1. -You are working on the **Outline** step of a Blog Post task. -Your goal is to turn the research notes into a clear, logical structure for the post. - -## Input +## Output -The research notes from the previous step are available as `research.md` in the working directory. +`research.md` in the working directory. Report back that it is ready once saved.""", + "Outline": """## Input -## Before you start +`input_data.value` — the file path to the research notes from the previous step (typically `research.md`). -Ask the user: -- What is the desired post length (short ~500 w, medium ~1000 w, long ~2000 w)? -- Any sections that must or must not be included? +## Instructions -Wait for the user's reply before proceeding. +1. Ask the user: "What is the desired post length (short ~500 w, medium ~1000 w, long ~2000 w)? Any sections that must or must not be included?" Wait for their reply. +2. Read `research.md` fully. +3. Group related points into 3-6 sections. +4. Order sections for logical flow (context → problem → solution → conclusion, or similar). +5. Write a one-sentence summary of each section. +6. Save the outline to `outline.md` and present it to the user. -## Working instructions +## Human involvement -1. Read `research.md` fully. -2. Group related points into 3-6 sections. -3. Order sections for logical flow (context → problem → solution → conclusion, or similar). -4. Write a one-sentence summary of each section. +The user must approve the outline structure before the step is complete. Ask: "Does this structure look right, or would you like to move/remove/add any sections?" Iterate until the user approves. ## Output -Save the outline to `outline.md`. Present it to the user and ask: -"Does this structure look right, or would you like to move/remove/add any sections?" -Iterate until the user approves, then report that `outline.md` is ready.""", - "Draft": """# Step: Draft - -You are working on the **Draft** step of a Blog Post task. -Your goal is to turn the approved outline into a full first draft. +`outline.md` — the approved outline. Report that it is ready once the user approves.""", + "Draft": """## Input -## Input +`input_data.value` — the file path to the approved outline from the previous step (typically `outline.md`). -The approved outline is available as `outline.md` in the working directory. +## Instructions -## Before you start +1. Ask the user in one message: "What tone should the post have (casual, technical, formal)? Any specific phrasing or terminology to use or avoid?" Wait for their reply. +2. Follow the outline's section structure exactly. +3. Write full prose for each section — vary sentence length, avoid filler phrases. +4. Include a working title and a short intro paragraph. +5. Aim for the length implied by the Outline step's approved scope. +6. Write the full draft autonomously with no mid-draft check-ins. +7. Save the draft to `draft.md`. -Ask the user in one message: -- What tone should the post have (casual, technical, formal)? -- Any specific phrasing or terminology to use or avoid? +## Human involvement -Wait for their reply before writing. - -## Working instructions - -1. Follow the outline's section structure exactly. -2. Write full prose for each section—vary sentence length, avoid filler phrases. -3. Include a working title and a short intro paragraph. -4. Aim for the length implied by the Outline step's approved scope. -5. No mid-draft check-ins—write the full draft autonomously based on the instructions above. +One upfront question (tone/terminology) before writing. No approval needed at this stage — review happens in the Edit step. ## Output -Save the draft to `draft.md`. Report that it is ready; no further approval is needed at this stage (that happens in Edit).""", - "Edit": """# Step: Edit - -You are working on the **Edit** step of a Blog Post task. -Your goal is to turn the first draft into a publish-ready post. +`draft.md` — the first draft. Report that it is ready.""", + "Edit": """## Input -## Input +`input_data.value` — the file path to the first draft from the previous step (typically `draft.md`). -The first draft is available as `draft.md` in the working directory. - -## Working instructions +## Instructions 1. Read the draft end-to-end before making any changes. 2. Fix grammar, punctuation, and spelling errors. 3. Improve sentence flow: split run-ons, vary structure, remove redundancy. 4. Verify the opening paragraph hooks the reader and the conclusion is clear. 5. Ensure headings are consistent and match the outline. -6. No mid-edit check-ins needed—edit autonomously. +6. Edit autonomously with no mid-edit check-ins. +7. Present the revised post to the user. -## Human review +## Human involvement -After editing, present the revised post to the user and ask: -"Here is the edited draft. Does it read well, or are there any sections you would like adjusted?" -Iterate until the user approves. +The user must approve the edited post before the step is complete. Ask: "Here is the edited draft. Does it read well, or are there any sections you would like adjusted?" Iterate until the user approves. ## Output -Save the final approved version to `edited_post.md` and report it is ready.""", - "Publish": """# Step: Publish - -You are working on the **Publish** step of a Blog Post task. -Your goal is to get the edited post live and capture its URL. +`edited_post.md` — the final approved post. Report that it is ready once the user approves.""", + "Publish": """## Input -## Input +`input_data.value` — the file path to the final edited post from the previous step (typically `edited_post.md`). -The final edited post is available as `edited_post.md` in the working directory. - -## Working instructions +## Instructions 1. Ask the user: "Where should this post be published (CMS name / URL, or shall I walk you through it)?" -2. Follow the user's publishing workflow—copy/paste content, set metadata (title, tags, publish date) as directed. +2. Follow the user's publishing workflow — copy/paste content, set metadata (title, tags, publish date) as directed. 3. Confirm with the user once the post is live. +## Human involvement + +The user must provide the publishing destination and confirm the post is live. Ask them to confirm the public URL once published. + ## Output -Once the post is live, ask the user to confirm the public URL. -Report the URL back—this is the step's deliverable.""", +The public URL of the published post. Report it back — this is the step's deliverable.""", } @@ -226,62 +131,10 @@ "or deep-edit before publishing." ), "process": [ - { - "order": 1, - "name": "Draft", - "input_spec": { - "description": "Topic and any initial notes.", - "source": "static", - "from_step": None, - }, - "output_spec": { - "type": "file", - "description": "Draft document plus a boolean review_needed field.", - "constraints": "markdown file; output dict must include review_needed (bool)", - }, - }, - { - "order": 2, - "name": "Quick Publish", - "input_spec": { - "description": "Draft from the Draft step.", - "source": "previous_step_output", - "from_step": "Draft", - }, - "output_spec": { - "type": "url", - "description": "Published URL (no review required).", - "constraints": "valid URL", - }, - }, - { - "order": 3, - "name": "Deep Edit", - "input_spec": { - "description": "Draft from the Draft step.", - "source": "previous_step_output", - "from_step": "Draft", - }, - "output_spec": { - "type": "file", - "description": "Edited, review-ready document.", - "constraints": "markdown file", - }, - }, - { - "order": 4, - "name": "Publish", - "input_spec": { - "description": "Edited document from the Deep Edit step.", - "source": "previous_step_output", - "from_step": "Deep Edit", - }, - "output_spec": { - "type": "url", - "description": "Published URL.", - "constraints": "valid URL", - }, - }, + {"order": 1, "name": "Draft"}, + {"order": 2, "name": "Quick Publish"}, + {"order": 3, "name": "Deep Edit"}, + {"order": 4, "name": "Publish"}, ], # Draft branches: review_needed=False → Quick Publish (terminal), # review_needed=True → Deep Edit → Publish (terminal) @@ -309,10 +162,19 @@ } CONTENT_REVIEW_PLAYBOOKS: dict[str, str] = { - "Draft": """# Step: Draft + "Draft": """## Input -Write a draft on the given topic. At the end, decide whether the content -needs editorial review before publishing. +`input_data.value` — the topic and any initial notes the user provided when creating the task. + +## Instructions + +1. Write a draft on the given topic. +2. At the end, decide whether the content needs editorial review before publishing. +3. Save the draft to `draft.md`. + +## Human involvement + +None required unless the topic is ambiguous — ask for clarification upfront if needed. ## Output @@ -320,37 +182,52 @@ - `value`: path to the draft file (e.g. `draft.md`) - `review_needed`: `true` if editorial review is required, `false` to fast-track """, - "Quick Publish": """# Step: Quick Publish + "Quick Publish": """## Input + +`input_data.value` — the draft file path. + +## Instructions -Publish the draft directly (no review required). +Publish the draft directly without editorial review. -## Input +## Human involvement -The draft file path is in `input_data.value`. +Ask the user for the publishing destination if not already known. Confirm the post is live before finishing. ## Output Submit `{"value": ""}`. """, - "Deep Edit": """# Step: Deep Edit + "Deep Edit": """## Input -Perform a thorough editorial review and polish of the draft. +`input_data.value` — the draft file path. -## Input +## Instructions -The draft file path is in `input_data.value`. +1. Read the draft end-to-end. +2. Fix grammar, punctuation, and spelling errors. +3. Improve sentence flow and remove redundancy. +4. Save the polished version to `edited.md`. + +## Human involvement + +None required — edit autonomously. ## Output -Submit `{"value": ""}`. +Submit `{"value": "edited.md"}`. """, - "Publish": """# Step: Publish + "Publish": """## Input + +`input_data.value` — the edited file path. + +## Instructions -Publish the edited document. +Publish the edited document to the target destination. -## Input +## Human involvement -The edited file path is in `input_data.value`. +Ask the user for the publishing destination if not already known. Confirm the post is live before finishing. ## Output diff --git a/progi/web/routers/workflows.py b/progi/web/routers/workflows.py index d7fd591..3d42442 100644 --- a/progi/web/routers/workflows.py +++ b/progi/web/routers/workflows.py @@ -134,7 +134,7 @@ def update_step( if "playbook" in payload: db.update_playbook(cfg, step_id, payload.pop("playbook")) step_fields = { - k: v for k, v in payload.items() if k in ("name", "input_spec", "output_spec") + k: v for k, v in payload.items() if k in ("name",) } if step_fields: db.update_step(cfg, step_id, **step_fields) diff --git a/progi/web/static/app.js b/progi/web/static/app.js index 2441a8d..103d4b4 100644 --- a/progi/web/static/app.js +++ b/progi/web/static/app.js @@ -394,15 +394,13 @@ function stepDetail() { workflowId: null, stepId: null, stepName: '', - inputSpec: null, - outputSpec: null, playbook: '', prevSteps: [], nextSteps: [], libraryEntryId: null, - editing: { input_spec: false, output_spec: false, playbook: false }, - drafts: { input_spec: '', output_spec: '', playbook: '' }, - errors: { input_spec: '', output_spec: '' }, + editing: { playbook: false }, + drafts: { playbook: '' }, + errors: {}, init() { const data = JSON.parse(document.getElementById('step-detail-data').textContent); @@ -421,17 +419,7 @@ function stepDetail() { }, async saveEdit(field) { - const body = {}; - if (field === 'playbook') { - body.playbook = this.drafts.playbook; - } else { - try { - body[field] = JSON.parse(this.drafts[field]); - } catch { - this.errors[field] = 'Invalid JSON'; - return; - } - } + const body = { [field]: this.drafts[field] }; const resp = await fetch(`/workflows/${this.workflowId}/steps/${this.stepId}`, { method: 'PATCH', @@ -440,12 +428,8 @@ function stepDetail() { }); if (resp.ok) { - // Persist new value locally - if (field === 'input_spec') this.inputSpec = body.input_spec; - if (field === 'output_spec') this.outputSpec = body.output_spec; - if (field === 'playbook') this.playbook = body.playbook; + if (field === 'playbook') this.playbook = body.playbook; this.editing[field] = false; - this.errors[field] = ''; } }, diff --git a/progi/web/templates/partials/step_detail.html b/progi/web/templates/partials/step_detail.html index 4ef32b6..7805c07 100644 --- a/progi/web/templates/partials/step_detail.html +++ b/progi/web/templates/partials/step_detail.html @@ -1,4 +1,4 @@ - +
- {# Input spec #} -
-

Input spec

- {# Display mode #} -
-

-        
-      
- {# Edit mode #} -
- -

-
- - -
-
-
- - {# Output spec #} -
-

Output spec

- {# Display mode #} -
-

-        
-      
- {# Edit mode #} -
- -

-
- - -
-
-
- {# Previous steps #}

Previous steps

diff --git a/tests/test_db.py b/tests/test_db.py index 85fe8b0..fb0564d 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -44,7 +44,7 @@ def test_full_work_loop(monkeypatch): ctx = db.start_or_continue_task(cfg, task_id) assert ctx["task"]["status"] == "in_progress" assert ctx["current_step"]["name"] == "Research" - assert ctx["current_step"]["playbook"].startswith("# Step: Research") + assert ctx["current_step"]["playbook"].startswith("## Input") # Advance through all 5 steps. step_names = ["Research", "Outline", "Draft", "Edit", "Publish"] @@ -105,22 +105,8 @@ def test_authoring_roundtrip(monkeypatch): "name": "Tiny", "description": "two-step workflow", "process": [ - { - "order": 1, - "name": "A", - "input_spec": {"description": "start", "source": "static"}, - "output_spec": {"type": "text", "description": "out", "constraints": ""}, - }, - { - "order": 2, - "name": "B", - "input_spec": { - "description": "from A", - "source": "previous_step_output", - "from_step": "A", - }, - "output_spec": {"type": "text", "description": "out", "constraints": ""}, - }, + {"order": 1, "name": "A"}, + {"order": 2, "name": "B"}, ], } wf = db.save_workflow(cfg, skeleton, {"A": "playbook A", "B": "playbook B"}) @@ -152,9 +138,9 @@ def test_save_workflow_without_edges_creates_linear(monkeypatch): "name": "Linear", "description": "three steps, no edges key", "process": [ - {"order": 1, "name": "A", "input_spec": {"source": "static", "description": "x"}, "output_spec": {"type": "text", "description": "x", "constraints": ""}}, - {"order": 2, "name": "B", "input_spec": {"source": "static", "description": "x"}, "output_spec": {"type": "text", "description": "x", "constraints": ""}}, - {"order": 3, "name": "C", "input_spec": {"source": "static", "description": "x"}, "output_spec": {"type": "text", "description": "x", "constraints": ""}}, + {"order": 1, "name": "A"}, + {"order": 2, "name": "B"}, + {"order": 3, "name": "C"}, ], } wf = db.save_workflow(cfg, skeleton, {}) @@ -237,8 +223,8 @@ def test_no_matching_condition_raises(monkeypatch): "name": "Strict", "description": "conditional edge, no fallback", "process": [ - {"order": 1, "name": "A", "input_spec": {"source": "static", "description": "x"}, "output_spec": {"type": "text", "description": "x", "constraints": ""}}, - {"order": 2, "name": "B", "input_spec": {"source": "static", "description": "x"}, "output_spec": {"type": "text", "description": "x", "constraints": ""}}, + {"order": 1, "name": "A"}, + {"order": 2, "name": "B"}, ], "edges": [ {"from": "A", "to": "B", "condition": {"field": "go", "operator": "eq", "value": True}, "priority": 0}, @@ -263,7 +249,7 @@ def test_delete_workflow(monkeypatch): "name": "Disposable", "description": "will be deleted", "process": [ - {"order": 1, "name": "Only", "input_spec": {"source": "static", "description": "x"}, "output_spec": {"type": "text", "description": "x", "constraints": ""}}, + {"order": 1, "name": "Only"}, ], } wf = db.save_workflow(cfg, skeleton, {}) @@ -320,22 +306,14 @@ def test_update_step(monkeypatch): "name": "Edit me", "description": "one step", "process": [ - {"order": 1, "name": "Alpha", "input_spec": {"source": "static", "description": "x"}, "output_spec": {"type": "text", "description": "x", "constraints": ""}}, + {"order": 1, "name": "Alpha"}, ], } wf = db.save_workflow(cfg, skeleton, {}) step_id = wf["steps"][0]["id"] - updated = db.update_step( - cfg, - step_id, - name="Alpha Renamed", - input_spec={"source": "static", "description": "updated"}, - ) + updated = db.update_step(cfg, step_id, name="Alpha Renamed") assert updated["name"] == "Alpha Renamed" - assert updated["input_spec"]["description"] == "updated" - # output_spec unchanged - assert updated["output_spec"]["type"] == "text" def test_board_tasks(monkeypatch): @@ -357,7 +335,7 @@ def _make_one_step_workflow(db, cfg, name="Lib Test", playbook_content="# My Pla "name": name, "description": "for library tests", "process": [ - {"order": 1, "name": "Only", "input_spec": {"source": "static", "description": "x"}, "output_spec": {"type": "text", "description": "x", "constraints": ""}}, + {"order": 1, "name": "Only"}, ], } wf = db.save_workflow(cfg, skeleton, {"Only": playbook_content})