Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions webapp/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,7 @@ def _jira_unauthorized() -> HTMLResponse:


def _jira_summary(s) -> str:
"""A concise Epic summary derived from the generated brief."""
"""A concise, outcome-based Epic summary."""
return flow.jira_epic_summary(s)


Expand Down Expand Up @@ -1080,8 +1080,8 @@ async def jira_export(request: Request, sid: str = Form(...),
cloud_id = entry.meta.get("cloud_id", "")
site_url = entry.meta.get("site_url", "")
md = flow.assemble_final(s)
jira_md = flow.assemble_jira_export(s)
adf = markdown_adf.to_adf(jira_md)
jira_projection = flow.assemble_jira_export(s)
adf = markdown_adf.to_adf(jira_projection)
summary = _jira_summary(s)
budget = config.JIRA_EXPORT_TIMEOUT_MS / 1000

Expand Down
265 changes: 191 additions & 74 deletions webapp/app/wizard/flow.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion webapp/app/wizard/prompts/system.md.j2
Original file line number Diff line number Diff line change
@@ -1 +1 @@
You are a senior product manager helping turn a rough software idea into a precise, structured specification. You write tight, concrete, testable specs — never marketing fluff. You follow output format instructions exactly: no preamble, no closing remarks, no markdown code fences around the whole answer. Do not introduce numeric thresholds, vendors, protocols, compliance claims, or deployment choices as facts unless they came from the idea, repo context, or clarifications; otherwise mark them "[ASSUMPTION]". When input text contains the two-character sequence "\n", treat it as a line break.
You are a senior product manager helping turn a rough software idea into a precise, structured specification. You write tight, concrete, testable specs — never marketing fluff. You follow output format instructions exactly: no preamble, no closing remarks, no markdown code fences around the whole answer. Do not introduce numeric thresholds, vendors, protocols, compliance claims, deployment choices, or resource targets as facts unless they come from the idea, repository context, or clarifications; otherwise mark them "[ASSUMPTION]". When input text contains the two-character sequence "\n", treat it as a line break.
63 changes: 33 additions & 30 deletions webapp/app/wizard/sections.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,37 @@ sections:
- id: goals
title: Goals & Background
instruction: >
Start with exactly one line: "Epic Summary: <short outcome-based title>".
The title must name the concrete project outcome, not "Incipit",
"mega-prompt", or other generic wording. Then write 3-6 Markdown bullet
points capturing the desired outcomes, followed by one short paragraph of
background context explaining why this is being built. Capability language
only — no implementation details.
Write a short "Goals & Background" section. Start with exactly two
parseable lines: "Epic Summary: <short outcome-based Jira Epic title>"
and "Outcome: <one-sentence user/business outcome>". The Epic Summary must
be specific to this work and must not be generic text such as "Incipit
mega-prompt" or "Implementation Brief". Then add a "Planning Summary:"
Markdown bullet list with 3-5 bullets capturing desired outcomes, followed
by one short background paragraph explaining why this is being built.
Capability language only — no implementation details.

- id: functional_requirements
title: Functional Requirements
instruction: >
Write the functional requirements as a Markdown bullet list labeled FR1,
FR2, ... Each requirement must use SHALL language and be a single
story-sized, testable statement of WHAT the system does (e.g. "- FR3: The
system SHALL let users export the result as a markdown file"). Each FR
should map cleanly to one Jira Story: avoid tiny UI-only steps and avoid
broad epics disguised as requirements. Cover the core happy path first,
then secondary behaviors. 5-12 requirements depending on stakes. Do not
include performance/quality attributes here.
Write the functional requirements as a Markdown list with stable IDs FR1,
FR2, ... Each item must use SHALL and be a single story-sized, testable
statement of WHAT the system does (e.g. "- FR3: The system SHALL let users
export the result as a markdown file"). Child Jira stories are derived
from these FRs, so do not bundle unrelated behavior into one FR. Cover the
core happy path first, then secondary behaviors. 5-12 requirements
depending on stakes. Do not include performance/quality attributes here.

- id: non_functional_requirements
title: Non-Functional Requirements
instruction: >
Write the non-functional requirements as a Markdown bullet list labeled
NFR1, NFR2, ... using SHALL language. Cover only the attributes that matter
for these stakes: performance, security/auth, reliability, resource
Write the non-functional requirements as a Markdown list with stable IDs
NFR1, NFR2, ... Each item must use SHALL and cover only attributes that
matter for these stakes: performance, security/auth, reliability, resource
constraints, compatibility. Keep it to 3-7 items. Each must be concrete
and checkable, not aspirational fluff. Do not invent numeric thresholds,
vendors, protocols, compliance claims, or deployment choices as facts
unless they came from the idea, repo context, or clarifications; otherwise
mark them "[ASSUMPTION]".
protocols, vendors, compliance claims, deployment choices, or resource
targets unless they came from the idea, repository context, or
clarifications; otherwise mark the item "[ASSUMPTION]".

- id: tech_constraints
title: Tech Constraints & Stack
Expand All @@ -42,20 +43,22 @@ sections:
the technology choices and constraints a coding agent must respect —
language/runtime, frameworks, storage, deployment target, existing systems
to integrate with, and anything explicitly ruled out. Derive choices from
the user's answers; where the user expressed no preference, choose the
simplest mainstream option and mark it "[ASSUMPTION]". Do not introduce
vendors, protocols, compliance claims, or deployment choices as facts unless
they came from the idea, repo context, or clarifications.
the user's answers, repository context, or prior sections. Do not present
vendors, protocols, compliance requirements, deployment targets, or
infrastructure choices as facts unless they came from those sources; when
a choice is necessary but not known, choose the simplest mainstream option
and mark it "[ASSUMPTION]".

- id: acceptance_criteria
title: Acceptance Criteria
instruction: >
Write the acceptance criteria as a Markdown bullet list labeled AC1, AC2,
... Each criterion must cite the FR ids it validates in brackets, for
example "- AC1: [FR2, FR5] Given ...". Every FR must be covered by at
least one acceptance criterion. Each criterion is a concrete, observable
check in Given/When/Then form or "Done when ..." form. Include at least one
criterion for error/edge behavior. 5-10 criteria.
Write the acceptance criteria as a Markdown list with stable IDs AC1, AC2,
... Each criterion is a concrete, observable check in Given/When/Then form
or "Done when ..." form. Each AC must cite covered FR IDs in square
brackets immediately after the label, e.g. "- AC1: [FR2, FR5] Given ...".
Every FR must have at least one AC covering it, and each AC must reference
only FR IDs that exist in the Functional Requirements section. Include at
least one criterion for error/edge behavior. 5-12 criteria.

- id: out_of_scope
title: Out of Scope
Expand Down
3 changes: 3 additions & 0 deletions webapp/tests/test_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ def test_download_jira_md_returns_jira_export():
assert "## Epic" in body
assert "## Child Stories" in body
assert "### Story FR1" in body
assert "Title: Users can create todos." in body
assert "Covers: [FR1]" in body
assert "- **Story summary:** Users can create todos." in body
assert "Users can create todos." in body
assert "AC-FR1: Given a todo" in body
assert "# Implementation Brief" not in body

Expand Down
218 changes: 199 additions & 19 deletions webapp/tests/test_flow_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,50 +642,69 @@ def test_assemble_jira_export_groups_functional_requirements_into_stories():
title="Functional Requirements",
instruction="",
content=(
"FR1: The system SHALL let partners submit onboarding applications.\n"
"FR-2: The system SHALL let staff approve or reject applications.\n"
"- FR1: The system SHALL let partners submit onboarding applications.\n"
"- FR-2: The system SHALL let staff approve or reject applications.\n"
),
),
Section(
id="acceptance",
title="Acceptance Criteria",
instruction="",
content=(
"AC1: [FR1] Given a partner application, when it is submitted, "
"- AC1: [FR1] Given a partner application, when it is submitted, "
"then staff can review it.\n"
"AC2: [FR-2] Given a pending application, when staff approves it, "
"- AC2: [FR-2] Given a pending application, when staff approves it, "
"then the partner is activated.\n"
"AC-EDGE: The export preserves criteria that do not map to a "
"- AC3: The export preserves criteria that do not map to a "
"functional requirement.\n"
),
),
Section(
id="non_functional_requirements",
title="Non-Functional Requirements",
instruction="",
content="- NFR1: The system SHALL mark invented latency targets as [ASSUMPTION].",
),
],
)

out = flow.assemble_jira_export(s)

assert out.startswith("# Jira Export")
assert "## Epic" in out
assert "Jira-specific Epic projection" in out
assert "Summary: Launch partner onboarding" in out
assert "**Summary:** Launch partner onboarding" in out
assert "**Outcome:** Launch partner onboarding" in out
assert "Description: Launch partner onboarding" in out
assert "Outcome: Launch partner onboarding" in out
assert "**MVP Scope:**" in out
assert "Requirements:" in out
assert "Done when:" in out
assert "Boundaries:" in out
assert "References:" in out
assert "Notes:" in out
assert "**Project type:** existing" in out
assert "- existing repository — https://github.com/acme/portal" in out
assert "**Existing repository:** https://github.com/acme/portal" in out
assert "## Child Stories" in out
assert "### Story FR1" in out
assert "Title: The system SHALL let partners submit onboarding applications." in out
assert "- **Story summary:** The system SHALL let partners submit onboarding applications." in out
assert "Covers: [FR1]" in out
assert "Verify: Acceptance criteria pass for the covered requirement." in out
assert "AC1: [FR1] Given a partner application" in out
assert "### Story FR-2" in out
assert "The system SHALL let staff approve or reject applications." in out
assert "### Story FR2" in out
assert "Title: The system SHALL let staff approve or reject applications." in out
assert "Covers: [FR2]" in out
assert "AC2: [FR-2] Given a pending application" in out
assert "### Unmatched Acceptance Criteria" in out
assert "AC-EDGE: The export preserves criteria" in out
assert "## Unmatched Acceptance Criteria" in out
assert "AC3: The export preserves criteria" in out

child_stories = out.split("## Child Stories", 1)[1]
assert child_stories.index("### Story FR1") < child_stories.index("AC1: [FR1]")
assert child_stories.index("AC1: [FR1]") < child_stories.index("### Story FR-2")
assert child_stories.index("### Story FR-2") < child_stories.index("AC2: [FR-2]")
assert child_stories.index("AC2: [FR-2]") < child_stories.index("### Unmatched Acceptance Criteria")
stories = out.split("## Child Stories", 1)[1]
assert stories.index("### Story FR1") < stories.index("AC1:")
assert stories.index("AC1:") < stories.index("### Story FR2")
assert stories.index("### Story FR2") < stories.index("AC2:")
assert stories.index("AC2:") < stories.index("## Unmatched Acceptance Criteria")


def test_assemble_jira_export_parses_bold_markdown_requirement_and_criteria_labels():
Expand Down Expand Up @@ -718,11 +737,12 @@ def test_assemble_jira_export_parses_bold_markdown_requirement_and_criteria_labe

out = flow.assemble_jira_export(s)

assert "## Child Stories" in out
assert "### Story FR1" in out
assert "Users can configure billing alert thresholds." in out
assert "Title: Users can configure billing alert thresholds." in out
assert "AC-FR1: Given a threshold" in out
assert "### Story FR-2" in out
assert "Users can preview who will receive an alert." in out
assert "### Story FR2" in out
assert "Title: Users can preview who will receive an alert." in out
assert "AC-FR-2: Given recipients" in out
assert "**FR1:**" not in out
assert "**AC-FR1:**" not in out
Expand Down Expand Up @@ -756,12 +776,172 @@ def test_assemble_jira_export_uses_single_fallback_story_without_requirements():
assert "## Epic" in out
assert "## Child Stories" in out
assert "### Story 1" in out
assert "Title: Improve account audit logging" in out
assert "Improve account audit logging" in out
assert "Add durable audit events for account mutations." in out
assert "AC1: Audit events include actor, target account, and timestamp." in out
assert "No FR-labeled requirements found" in out
assert "### Story FR" not in out
assert "### Unmatched Acceptance Criteria" not in out
assert "## Unmatched Acceptance Criteria" not in out


def test_jira_epic_summary_prefers_parseable_non_generic_title():
s = _session(
idea="Incipit mega-prompt",
project_type="new",
stakes="serious",
form_factor="web app",
sections=[
Section(
id="goals",
title="Goals & Background",
instruction="",
content=(
"Epic Summary: Export implementation briefs as Jira-ready epics\n"
"Outcome: Teams can create scoped Jira work from a brief."
),
)
],
)

assert flow.jira_epic_summary(s) == "Export implementation briefs as Jira-ready epics"


def test_jira_epic_summary_parses_bolded_label():
s = _session(
idea="Fallback",
project_type="new",
stakes="serious",
form_factor="web app",
sections=[
Section(
id="goals",
title="Goals & Background",
instruction="",
content="**Epic Summary:** Create readable Jira epics from Incipit briefs",
)
],
)

assert flow.jira_epic_summary(s) == "Create readable Jira epics from Incipit briefs"


def test_epic_outcome_fallback_skips_epic_summary_label():
s = _session(
idea="Build a demo app",
project_type="new",
stakes="serious",
form_factor="web app",
sections=[
Section(
id="goals",
title="Goals & Background",
instruction="",
content=(
"Epic Summary: Build a demo app\n"
"- Bullet one: the demo runs locally.\n"
"- Bullet two: the demo exports its results.\n"
),
)
],
)

assert flow._epic_outcome(s) == "Bullet one: the demo runs locally."

out = flow.assemble_jira_export(s)
assert "Description: Bullet one: the demo runs locally." in out
assert "Outcome: Bullet one: the demo runs locally." in out
assert "Description: Epic Summary:" not in out
assert "Outcome: Epic Summary:" not in out


def test_epic_outcome_fallback_returns_idea_when_goals_is_all_labels():
s = _session(
idea="Build a demo app",
project_type="new",
stakes="serious",
form_factor="web app",
sections=[
Section(
id="goals",
title="Goals & Background",
instruction="",
content="Epic Summary: Build a demo app\nOutcome:\n",
)
],
)

assert flow._epic_outcome(s) == "Build a demo app"


def test_assemble_jira_export_story_labels_do_not_collapse_in_adf():
s = _session(
idea="Launch partner onboarding",
project_type="new",
stakes="serious",
form_factor="web app",
sections=[
Section(
id="functional_requirements",
title="Functional Requirements",
instruction="",
content=(
"- FR1: The system SHALL let partners submit onboarding applications.\n"
"- FR2: The system SHALL let staff approve applications.\n"
),
),
Section(
id="acceptance_criteria",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generic-prefix stripper in _clean_jira_title silently mangles real project names that start with the word "Brief": because the connector group in _JIRA_GENERIC_PREFIX_RE is optional and can match empty, _JIRA_GENERIC_PREFIX_RE.sub strips a leading "Brief" from any title, so a project literally called "Brief case manager" becomes "Case manager", and "Brief case manager for accounting" becomes "Case manager for accounting". This output is the live Jira Epic summary on export, not just a cosmetic detail — the epic loses its name. (The brief for payments → Payments case in test_jira_title_cleaning_preserves_brief_word_prefixes shows the intent, but the word boundary alone cannot distinguish "brief for X" from "Brief case X".)

Suggested change: require the connector to actually be present, e.g. change the tail to a non-optional (?:\s+(?:for|about)|\s*:\s*|\s+[-<]\s+) (drop the ?), or add a lookahead (?=\s*(?:for|about|:|-|<)\b) so a bare brief followed by ordinary words is never consumed; then extend this file's cleaning fixture matrix with "Brief case manager", "Briefing service", "brief for payments", and "Incipit brief" so the distinction stays pinned.

🔁 Powered by Looper · runner=reviewer · agent=pi · An autonomous AI dev team for your GitHub repos.

title="Acceptance Criteria",
instruction="",
content="- AC1: [FR1] Given an application, then staff can review it.\n",
),
],
)

out = flow.assemble_jira_export(s)
story = out.split("### Story FR1", 1)[1].split("### Story FR2", 1)[0]
assert "Title: The system SHALL let partners submit onboarding applications." in story
assert "Covers: [FR1]" in story
assert "Description:\nThe system SHALL let partners submit onboarding applications.\n\nAcceptance Criteria:" in story

from app import markdown_adf
adf = markdown_adf.to_adf(out)
paragraphs = [n for n in adf["content"] if n["type"] == "paragraph"]
texts = ["".join(n.get("text", "") for n in p["content"]) for p in paragraphs]
assert not any("Title: " in t and ("Covers:" in t or "Verify:" in t)
for t in texts)
fr_text = "The system SHALL let partners submit onboarding applications."
assert "Description: " + fr_text in texts
assert fr_text not in texts


def test_jira_export_validation_notes_cover_fr_ac_reference_problems():
s = _session(
idea="Improve billing alerts",
project_type="new",
stakes="internal",
form_factor="API/service",
sections=[
Section(
id="functional_requirements",
title="Functional Requirements",
instruction="",
content="- FR1: The system SHALL send billing alerts.\n- FR2: The system SHALL preview recipients.",
),
Section(
id="acceptance_criteria",
title="Acceptance Criteria",
instruction="",
content="- AC1: [FR1, FR9] Given a bill, when due, then an alert is sent.",
),
],
)

notes = flow.jira_export_validation_notes(s)

assert "FR2 has no mapped acceptance criteria." in notes
assert any("references unknown FR9" in note for note in notes)


def test_jira_consistency_warnings_cover_missing_and_unknown_fr_refs():
Expand Down
Loading
Loading