devops-bot-sdk 1.4.46__tar.gz → 1.4.55__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/client.py +1 -1
  6. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/ipc/handlers.py +196 -25
  7. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/run_auto.py +25 -10
  8. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/README.md +0 -0
  9. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  10. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  11. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  12. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/devops_bot_sdk.egg-info/requires.txt +0 -0
  13. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  14. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/collectors/__init__.py +0 -0
  15. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/collectors/files.py +0 -0
  16. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/collectors/process.py +0 -0
  17. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/collectors/screenshot.py +0 -0
  18. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/config.py +0 -0
  19. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/crucial.py +0 -0
  20. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/exceptions.py +0 -0
  21. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/git_ops.py +0 -0
  22. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/graphify.py +0 -0
  23. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/hooks/__init__.py +0 -0
  24. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/hooks/crucial_guard.py +0 -0
  25. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/ipc/__init__.py +0 -0
  26. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/ipc/electron_bridge.py +0 -0
  27. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/local_exec.py +0 -0
  28. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/models/__init__.py +0 -0
  29. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/models/envelope.py +0 -0
  30. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/models/requests.py +0 -0
  31. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/models/responses.py +0 -0
  32. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/models/snapshots.py +0 -0
  33. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/py.typed +0 -0
  34. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.46 → devops_bot_sdk-1.4.55}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.46
3
+ Version: 1.4.55
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.46
3
+ Version: 1.4.55
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devops-bot-sdk"
7
- version = "1.4.46"
7
+ version = "1.4.55"
8
8
  description = "DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.46
3
+ Version: 1.4.55
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -30,7 +30,7 @@ Rules:
30
30
  - All data egress through submit_webhook only
31
31
  """
32
32
 
33
- __version__ = "1.4.46"
33
+ __version__ = "1.4.55"
34
34
  __author__ = "AgentOS"
35
35
 
36
36
  from sdk.client import BackendClient
@@ -58,7 +58,7 @@ def _notify_status(task_id: str, status: str) -> None:
58
58
  pass
59
59
 
60
60
 
61
- SDK_VERSION = "1.4.46"
61
+ SDK_VERSION = "1.4.55"
62
62
  _POLL_INTERVAL = 3.0
63
63
  _POLL_TIMEOUT = 600.0
64
64
  _ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
@@ -210,6 +210,17 @@ def _build_local_prompt(
210
210
  "before changing it.\n\n"
211
211
  + human_instructions.strip()
212
212
  )
213
+ # 1b. If a PRD is present (a dev Story was converted to this Task with its PRD
214
+ # folded into the description), it is the AUTHORITATIVE spec — build the
215
+ # whole feature to satisfy it end-to-end.
216
+ if user_input and _PRD_MARKER in user_input:
217
+ parts.append(
218
+ "This task includes a PRD (Product Requirements Document) in its description "
219
+ "below. Treat the PRD as the AUTHORITATIVE specification: implement the WHOLE "
220
+ "feature to satisfy its Functional Requirements, Non-Functional Requirements, "
221
+ "and Acceptance Criteria, following the PRD end-to-end. Do not narrow or skip "
222
+ "any part of the PRD's scope."
223
+ )
213
224
  # 2. Distilled plan (acceptance criteria + steps) — the compact, actionable
214
225
  # content the backend planner derived from the full description.
215
226
  sp = structured_plan or {}
@@ -231,6 +242,15 @@ def _build_local_prompt(
231
242
  "above are authoritative if anything conflicts) ===\n"
232
243
  + _clip_text(user_input.strip())
233
244
  )
245
+ # House standards — if this task produces a PRD document, hold it to the
246
+ # same standard format the bot uses everywhere else (a Task converted from a
247
+ # Story keeps its PRD directive in the instructions).
248
+ if _wants_prd(human_instructions) or _wants_prd(user_input):
249
+ parts.append(
250
+ "If you create a PRD / product-requirements document (e.g. a PRD.md), "
251
+ "write it in this STANDARD PRD FORMAT (keep the numbered headings):\n"
252
+ + _PRD_FORMAT
253
+ )
234
254
  if git_managed:
235
255
  # The target is a git repo the SDK manages — it branches before and
236
256
  # commits/pushes/opens the PR after, so the agent must not touch git here.
@@ -360,13 +380,78 @@ _MAX_EPIC_STORIES = int(os.getenv("AGENTOS_MAX_EPIC_STORIES", "") or 8)
360
380
  # Label recorded in the ticket's modelUsed on Epic completion.
361
381
  _EPIC_MODELS = [m.strip() for m in (os.getenv("AGENTOS_EPIC_MODELS", "") or "claude-code").split(",") if m.strip()]
362
382
 
383
+ # ── Standard output formats ─────────────────────────────────────────────────
384
+ # One house style for every generated artifact so Epics, Stories and PRDs read
385
+ # consistently across tickets. Referenced by the generation prompts below.
386
+ _EPIC_FORMAT = (
387
+ "## Epic\n<concise epic title>\n\n"
388
+ "## Overview\n<1-2 paragraph summary of the goal and context>\n\n"
389
+ "## Business Value / Goal\n<why this matters; the outcome it drives>\n\n"
390
+ "## Scope\n**In scope:** <what is included>\n**Out of scope:** <what is not>\n\n"
391
+ "## User Stories\n1. <story title> — <one-line summary>\n2. <story title> — <one-line summary>\n\n"
392
+ "## Epic-level Acceptance Criteria\n- <observable, high-level pass/fail criteria>\n\n"
393
+ "## Dependencies & Risks\n- <key dependencies, assumptions, risks>"
394
+ )
395
+ _USER_STORY_FORMAT = (
396
+ "## User Story\n"
397
+ "As a <role>, I want <capability>, so that <benefit>.\n\n"
398
+ "## Acceptance Criteria\n"
399
+ "- Given <context>, when <action>, then <expected outcome>.\n"
400
+ "- <list every testable criterion, Gherkin Given/When/Then style>\n\n"
401
+ "## Notes & Constraints\n"
402
+ "- <context, dependencies, constraints; for software work: affected files/modules & patterns to follow>\n\n"
403
+ "## Definition of Done\n"
404
+ "- Delivered against the acceptance criteria above\n"
405
+ "- Reviewed / validated (for software: tests added & passing)\n"
406
+ "- Documentation updated where relevant"
407
+ )
408
+ _PRD_FORMAT = (
409
+ "# PRD: <title>\n\n"
410
+ "## 1. Overview\n<what the feature is and why, in brief>\n\n"
411
+ "## 2. Goals & Non-Goals\n<what it must achieve; what is explicitly out of scope>\n\n"
412
+ "## 3. Current Behaviour & Context\n<the existing system/modules this touches — "
413
+ "grounded in the REAL codebase: files, components, current flow>\n\n"
414
+ "## 4. How It Works — Mechanism\n<the core mechanism end-to-end: the exact flow of "
415
+ "control and data, step by step, from trigger/input to output/persistence. Describe "
416
+ "how the feature actually functions, not just what it does>\n\n"
417
+ "## 5. Architecture & Components\n<components/modules/services involved and their "
418
+ "responsibilities; where new code lives; how they interact>\n\n"
419
+ "## 6. Data Model & Schema\n<entities, fields + types, relationships, indexes, and "
420
+ "any migrations / schema changes>\n\n"
421
+ "## 7. API / Interface Contracts\n<each endpoint or interface: HTTP method + path, "
422
+ "request shape, response shape, status codes, auth. Include example payloads>\n\n"
423
+ "## 8. Detailed Logic & Business Rules\n<algorithms, validation rules, state "
424
+ "transitions, sequence of operations, ordering/idempotency, concurrency>\n\n"
425
+ "## 9. Data Flow & Integrations\n<how data moves between components/services; "
426
+ "external systems, events, queues, third-party APIs>\n\n"
427
+ "## 10. Error Handling & Edge Cases\n<failure modes, validation errors, retries, "
428
+ "timeouts, boundary conditions, empty/duplicate/large inputs>\n\n"
429
+ "## 11. Security & Performance (Non-Functional)\n<authn/authz, input hardening, "
430
+ "secrets, rate limits, performance/scale targets, accessibility where relevant>\n\n"
431
+ "## 12. Testing Strategy\n<unit / integration / e2e coverage and the key test cases "
432
+ "that prove each requirement>\n\n"
433
+ "## 13. Acceptance Criteria\n<observable, testable pass/fail tied to the mechanism>\n\n"
434
+ "## 14. Dependencies, Risks & Rollout\n<dependencies, assumptions, risks, migration "
435
+ "and rollout/phasing>"
436
+ )
437
+
363
438
 
364
439
  def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
365
440
  human_instructions: str | None) -> str:
366
441
  """Read-only prompt that turns an Epic into a decomposition JSON."""
367
442
  parts: list[str] = [
368
- "You are decomposing a JIRA EPIC into implementable work. This is ANALYSIS "
369
- "ONLY — do NOT create, edit, or delete any file.",
443
+ "You are decomposing a JIRA EPIC into implementable user stories. This is "
444
+ "ANALYSIS ONLY — do NOT create, edit, or delete any file.",
445
+ "Faithfulness (read first — this prevents hallucinated stories):\n"
446
+ "- Derive stories ONLY from what the epic actually states or clearly implies. "
447
+ "Do NOT invent features, scope, integrations, tools, or requirements that are "
448
+ "not grounded in the epic (or, for software, in the REAL codebase you inspect).\n"
449
+ "- If something needed is missing or ambiguous, capture it as an open question "
450
+ "in that story's Notes — do NOT fabricate a solution.\n"
451
+ "- Prefer fewer, well-grounded stories over many speculative ones. Every story "
452
+ "must trace back to something in the epic.\n"
453
+ "- This epic may be for ANY domain (software, Finance, HR, operations, …). Match "
454
+ "the stories to the epic's actual domain; do not assume it is software.",
370
455
  f"=== EPIC {jira_key} ===\nSummary: {summary}",
371
456
  ]
372
457
  if description and description.strip():
@@ -378,18 +463,20 @@ def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
378
463
  + human_instructions.strip()
379
464
  )
380
465
  parts.append(
381
- "First understand the existing codebase in this directory (read CLAUDE.md, "
382
- "use graphify / Read / Glob / Grep) so the stories fit the REAL architecture. "
383
- "Then produce the decomposition.\n\n"
466
+ "Ground your work first: if this is a SOFTWARE project, inspect the existing "
467
+ "codebase in this directory (read CLAUDE.md, use graphify / Read / Glob / Grep) "
468
+ "so the stories fit the REAL architecture. For a NON-software epic (Finance, HR, "
469
+ "operations, …), ground the stories in the epic's stated details and any "
470
+ "reference material present — never in invented facts. Then produce the "
471
+ "decomposition.\n\n"
384
472
  "Output ONLY a single fenced ```json block and nothing after it, with shape:\n"
385
473
  "{\n"
386
- ' "epic_description": "A detailed description for the epic: a full user-story '
387
- "write-up covering goal, the user stories, acceptance criteria and scope.\",\n"
474
+ ' "epic_description": "The epic write-up in the EPIC FORMAT below (Markdown).",\n'
388
475
  ' "plan": "An ordered, numbered high-level implementation plan.",\n'
389
476
  ' "stories": [\n'
390
477
  ' {\n'
391
478
  ' "summary": "Short imperative story title (a JIRA task summary)",\n'
392
- ' "description": "Detailed description: what to build + acceptance criteria.",\n'
479
+ ' "description": "The story in the USER STORY FORMAT below (Markdown).",\n'
393
480
  ' "humanInstructions": "Operator instructions to implement THIS story. MUST '
394
481
  "repeat the same project location/path and constraints from the epic so the "
395
482
  'coding run works in the right place.",\n'
@@ -397,12 +484,16 @@ def _build_epic_prompt(jira_key: str, summary: str, description: str | None,
397
484
  ' "points": 5\n'
398
485
  " }\n"
399
486
  " ]\n"
400
- "}\n"
487
+ "}\n\n"
401
488
  "Create as many stories as the epic genuinely needs (do not pad or truncate "
402
489
  "artificially); each story must be independently implementable.\n"
403
490
  "assigneeEmail is OPTIONAL per story: set it ONLY when the epic explicitly "
404
491
  "assigns that specific work to a named person/email (e.g. 'assign the API to "
405
- "alice@x.com'); otherwise omit it."
492
+ "alice@x.com'); otherwise omit it.\n\n"
493
+ "=== EPIC FORMAT (epic_description MUST follow this exactly) ===\n"
494
+ + _EPIC_FORMAT
495
+ + "\n\n=== USER STORY FORMAT (every story.description MUST follow this exactly) ===\n"
496
+ + _USER_STORY_FORMAT
406
497
  )
407
498
  return "\n\n".join(parts)
408
499
 
@@ -582,12 +673,34 @@ def _wants_prd(text: str | None) -> bool:
582
673
  return bool(text and _PRD_RE.search(text))
583
674
 
584
675
 
676
+ # Signals that a ticket is DEVELOPMENT work (→ always gets a PRD). Deliberately
677
+ # code-specific so Finance/HR/ops tickets don't false-positive (they get a PRD
678
+ # only when explicitly requested).
679
+ _DEV_RE = re.compile(
680
+ r"github\.com/|gitlab\.com/|bitbucket\.org/|\b(?:repo(?:sitory)?|codebase|"
681
+ r"source\s*code|frontend|front-end|back-?end|pull\s+requests?|merge\s+requests?|"
682
+ r"\bPRs?\b|feature\s+branch|create\s+a?\s*branch|commit|deploy(?:ment)?|compile|"
683
+ r"build\s+pipeline|microservice|endpoint|api\s+contract|database\s+migration|"
684
+ r"unit\s+test)\b",
685
+ re.I,
686
+ )
687
+
688
+
689
+ def _is_dev_task(text: str | None) -> bool:
690
+ """True when the text clearly describes software-development work."""
691
+ return bool(text and _DEV_RE.search(text))
692
+
693
+
585
694
  def _build_prd_prompt(jira_key: str, summary: str, user_story: str | None,
586
695
  human_instructions: str | None) -> str:
587
- """Read-only prompt that produces a PRD (Markdown) for a user story."""
696
+ """Read-only prompt that produces a TECHNICAL PRD (Markdown) for a user story."""
588
697
  parts: list[str] = [
589
- "Write a PRD (Product Requirements Document) for the user story below. This is "
590
- "ANALYSIS ONLY do NOT create, edit, or delete any file.",
698
+ "Write a TECHNICAL PRD (Product Requirements Document) for the user story below. "
699
+ "It must fully specify the MECHANISM how the feature works end-to-end "
700
+ "(architecture, components, data model, API/interface contracts, control & data "
701
+ "flow, business logic, error handling) — in enough detail that a developer can "
702
+ "implement it directly from the PRD without guessing. This is ANALYSIS ONLY — do "
703
+ "NOT create, edit, or delete any file.",
591
704
  f"=== USER STORY {jira_key} ===\nSummary: {summary}",
592
705
  ]
593
706
  if user_story and user_story.strip():
@@ -595,11 +708,17 @@ def _build_prd_prompt(jira_key: str, summary: str, user_story: str | None,
595
708
  if human_instructions and human_instructions.strip():
596
709
  parts.append("Operator instructions:\n" + human_instructions.strip())
597
710
  parts.append(
598
- "Explore the codebase (CLAUDE.md, graphify, Read/Glob/Grep) to ground the PRD in "
599
- "the REAL system. Output ONLY the PRD as Markdown — no preamble, no code edits — "
600
- "with sections: Overview, Goals & Success Metrics, User Stories, Functional "
601
- "Requirements, Non-Functional Requirements, Acceptance Criteria, Out of Scope, "
602
- "Dependencies & Risks."
711
+ "Ground the PRD in reality — for software, FIRST understand the current codebase "
712
+ "STRUCTURE (folder layout, key modules, existing patterns, data models, APIs) via "
713
+ "CLAUDE.md, graphify and Read/Glob/Grep, then specify the mechanism against the "
714
+ "REAL files, modules, data models and APIs that exist; for a non-software task, "
715
+ "ground it in "
716
+ "the story's stated details and mark technical sections that don't apply as "
717
+ "'N/A'. Do NOT invent requirements, systems, endpoints, or facts not supported by "
718
+ "the story or the code you inspect. Output ONLY the PRD as Markdown — no preamble, "
719
+ "no code edits — following this PRD FORMAT exactly (keep the numbered section "
720
+ "headings):\n\n"
721
+ + _PRD_FORMAT
603
722
  )
604
723
  return "\n\n".join(parts)
605
724
 
@@ -621,14 +740,25 @@ async def _handle_story(
621
740
  summary = task.get("summary", "") or ""
622
741
  user_story = task.get("description") or ""
623
742
  human = task.get("humanInstructions")
624
- wants_prd = _wants_prd(human) or _wants_prd(user_story)
625
-
743
+ # STEP 1 — validate whether this is a CODEBASE (development) task: repo links /
744
+ # file paths in the instructions, or dev signals (PRs, branches, endpoints,
745
+ # deploy, …). A codebase task ALWAYS gets a technical PRD built from the current
746
+ # codebase; other domains (Finance, HR, …) get one only when explicitly asked.
747
+ has_repo = bool(_extract_repo_targets(human)) or bool(_extract_repo_targets(user_story))
748
+ is_codebase_task = has_repo or _is_dev_task(human) or _is_dev_task(user_story)
749
+ wants_prd = is_codebase_task or _wants_prd(human) or _wants_prd(user_story)
750
+
751
+ if is_codebase_task:
752
+ step = ("validated as a codebase task — building a technical PRD from the "
753
+ "current codebase, then converting to Task")
754
+ elif wants_prd:
755
+ step = "generating the requested PRD, then converting to Task"
756
+ else:
757
+ step = "converting to Task"
626
758
  await send(Envelope(
627
759
  type="step_update", thread_id=thread_id,
628
760
  data={"jira_task_id": tid, "task_id": tid,
629
- "current_step": f"Story {jira_key}: "
630
- + ("generating PRD, then " if wants_prd else "")
631
- + "converting to Task"},
761
+ "current_step": f"Story {jira_key}: {step}"},
632
762
  ).model_dump())
633
763
 
634
764
  # Idempotent: if a PRD is already in the description, don't regenerate it or
@@ -1039,6 +1169,40 @@ async def _code_one_repo(
1039
1169
  except Exception: # noqa: BLE001
1040
1170
  branch_info = None
1041
1171
 
1172
+ # 3a. Technical PRD for a CODEBASE task that doesn't have one yet. A Task created
1173
+ # directly (not via a Story) may carry only a user-story/feature description —
1174
+ # for codebase work we first turn the recon understanding into a technical PRD
1175
+ # and build to it, exactly like a Story-derived Task. Resumes the recon session
1176
+ # (already understands the codebase) so it's cheap; persists the PRD to the
1177
+ # ticket and to pending (shared → other repos reuse it and skip regeneration).
1178
+ # Skipped when a PRD is already present, on a resume, or for non-git targets.
1179
+ if (is_git and recon_session and not resume_session_id
1180
+ and _PRD_MARKER not in (pending.get("user_input") or "")):
1181
+ await send(Envelope(
1182
+ type="step_update", thread_id=thread_id,
1183
+ data={"status": "running",
1184
+ "current_step": f"[{name}] Codebase task — writing a technical PRD from the codebase"},
1185
+ ).model_dump())
1186
+ prd_res = await local_exec.run_claude_local(
1187
+ "Using your exploration above, write a TECHNICAL PRD for this task that fully "
1188
+ "specifies the mechanism (how it works end-to-end) against the REAL code you "
1189
+ "found. Output ONLY the PRD as Markdown, following this format exactly:\n\n"
1190
+ + _PRD_FORMAT,
1191
+ repo_path, allowed_tools=_RECON_ALLOWED_TOOLS, # read-only
1192
+ github_token=gh_token, resume_session_id=recon_session,
1193
+ )
1194
+ recon_session = prd_res.get("session_id") or recon_session
1195
+ prd = (prd_res.get("result") or "").strip() if prd_res.get("ok") else ""
1196
+ if prd:
1197
+ enriched = (
1198
+ f"{pending.get('user_input') or ''}\n\n---\n\n{_PRD_MARKER}\n\n{prd}"
1199
+ )[:20000]
1200
+ pending["user_input"] = enriched # shared across repos → reused, not regenerated
1201
+ try:
1202
+ await client.update_task_ml(task_id, {"description": enriched}, model_used=_EPIC_MODELS)
1203
+ except Exception: # noqa: BLE001 — persisting the PRD is best-effort
1204
+ pass
1205
+
1042
1206
  await send(Envelope(
1043
1207
  type="step_update", thread_id=thread_id,
1044
1208
  data={"status": "running", "current_step": f"Executing locally in {name} (claude_code)"},
@@ -1047,7 +1211,8 @@ async def _code_one_repo(
1047
1211
  # Build the prompt with framing that matches the target: a git repo we manage
1048
1212
  # (branch + PR) vs a plain local folder (BA/analyst deliverables, no git/PR).
1049
1213
  # When recon ran, the implementation resumes that session so its grounded plan
1050
- # stays in context; the prompt tells the agent to implement THAT plan.
1214
+ # (and the technical PRD, if generated) stays in context; if a PRD is present the
1215
+ # prompt makes it the authoritative spec.
1051
1216
  prompt = _build_local_prompt(
1052
1217
  pending["user_input"], pending.get("structured_plan"),
1053
1218
  pending.get("human_instructions"),
@@ -2500,10 +2665,16 @@ async def handle_orchestrate_auto_local_run(
2500
2665
  # raised earliest is started first. Unknown/invalid timestamps sort last.
2501
2666
  tasks = sorted(tasks, key=_task_created_key)
2502
2667
 
2668
+ # `stories_only` (agentos-auto story) → convert Stories to Tasks ONLY: no
2669
+ # epic decomposition, no coding. Narrow the queue to Story-type tickets.
2670
+ stories_only = bool(payload.get("stories_only"))
2671
+ if stories_only:
2672
+ tasks = [t for t in tasks if str(t.get("type") or "").strip().lower() == "story"]
2673
+
2503
2674
  await send(Envelope(
2504
2675
  type="pipeline_started", thread_id=thread_id,
2505
2676
  data={"mode": "orchestrate-auto-local", "task_count": len(tasks),
2506
- "only_keys": only_keys or None},
2677
+ "only_keys": only_keys or None, "stories_only": stories_only or None},
2507
2678
  ).model_dump())
2508
2679
 
2509
2680
  if not tasks:
@@ -11,6 +11,8 @@ Commands:
11
11
  agentos-auto resume alias for run — re-fetches remaining To Do tasks
12
12
  agentos-auto ticket K [K...] run ONLY the named Jira ticket(s), e.g. BBW-81 —
13
13
  any status, in the order given (alias: tickets)
14
+ agentos-auto story [K ...] convert Story(ies) → Task(s) only (with PRD), NO
15
+ coding; no keys = all To Do Stories (alias: stories)
14
16
  agentos-auto stop ask the running loop to pause after the current task
15
17
  agentos-auto status show whether the background loop is running / stop pending
16
18
  agentos-auto update upgrade the SDK to the latest version
@@ -298,7 +300,8 @@ async def _send(env: dict) -> None:
298
300
  print(f" [{etype}] {json.dumps(data)[:200]}")
299
301
 
300
302
 
301
- def _run_loop(workspace_root: str | None, only_keys: list[str] | None = None) -> None:
303
+ def _run_loop(workspace_root: str | None, only_keys: list[str] | None = None,
304
+ stories_only: bool = False) -> None:
302
305
  from sdk import local_exec
303
306
  from sdk.client import BackendClient, register_status_observer
304
307
  from sdk.ipc import handlers
@@ -320,6 +323,9 @@ def _run_loop(workspace_root: str | None, only_keys: list[str] | None = None) ->
320
323
  if only_keys:
321
324
  # Targeted run: only these Jira tickets, in this order, regardless of status.
322
325
  payload["only_keys"] = only_keys
326
+ if stories_only:
327
+ # Story→Task-only run: convert Stories to Tasks (with PRD), no coding.
328
+ payload["stories_only"] = True
323
329
 
324
330
  _clear_stop() # a fresh run/resume clears any stale stop flag
325
331
  _clear_backlogged() # session-scoped: only track Backlog moves from THIS run
@@ -458,17 +464,22 @@ def main() -> None:
458
464
  print("stop pending: yes" if _stop_requested() else "stop pending: no")
459
465
  return
460
466
 
461
- if cmd in ("run", "resume", "ticket", "tickets"):
462
- # Targeted run: `ticket`/`tickets` runs ONLY the named Jira keys (any
463
- # status, in the order given); `run`/`resume` runs the whole To Do queue.
467
+ if cmd in ("run", "resume", "ticket", "tickets", "story", "stories"):
468
+ # Run modes:
469
+ # run/resume the whole To Do queue
470
+ # ticket K... → ONLY the named Jira keys (any status, in order)
471
+ # story [K...] → convert Stories → Tasks only (with PRD); NO coding.
472
+ # With keys: those Stories; without: every To Do Story.
464
473
  only_keys: list[str] | None = None
465
- if cmd in ("ticket", "tickets"):
474
+ stories_only = cmd in ("story", "stories")
475
+ if cmd in ("ticket", "tickets", "story", "stories"):
466
476
  only_keys = _parse_ticket_keys(argv)
467
- if not only_keys:
477
+ if cmd in ("ticket", "tickets") and not only_keys:
468
478
  print("usage: agentos-auto ticket <JIRA-KEY> [<JIRA-KEY> ...]")
469
479
  print(" e.g. agentos-auto ticket BBW-81")
470
480
  print(" agentos-auto ticket BBW-81 BBW-80")
471
481
  sys.exit(1)
482
+ # `story` keys are OPTIONAL — no keys means "all To Do Stories".
472
483
 
473
484
  # Self-update FIRST (before the already-running check) so the installed
474
485
  # package is refreshed even when a daemon is already running — otherwise a
@@ -494,14 +505,18 @@ def main() -> None:
494
505
  # attached (useful for debugging). On platforms without fork we stay attached.
495
506
  foreground = "--foreground" in argv or "-f" in argv
496
507
  if not foreground and hasattr(os, "fork"):
497
- target = f" — tickets: {', '.join(only_keys)}" if only_keys else ""
508
+ if stories_only:
509
+ target = (f" — stories→tasks: {', '.join(only_keys)}" if only_keys
510
+ else " — converting all To Do Stories → Tasks")
511
+ else:
512
+ target = f" — tickets: {', '.join(only_keys)}" if only_keys else ""
498
513
  print(f"▶ agentos-auto started in the background{target} — it keeps running "
499
514
  "after you close this terminal.")
500
515
  print(f" logs: {_LOG_FILE}")
501
516
  print(" status: agentos-auto status")
502
517
  print(" stop: agentos-auto stop")
503
518
  if not foreground and _daemonize():
504
- _run_loop(workspace_root, only_keys) # only the detached daemon child reaches here
519
+ _run_loop(workspace_root, only_keys, stories_only) # detached daemon child only
505
520
  return
506
521
  if not foreground:
507
522
  print("note: background mode unavailable on this platform — running in the "
@@ -510,10 +525,10 @@ def main() -> None:
510
525
  # Foreground run still records a PID so stop/status can find it.
511
526
  _AGENTOS_DIR.mkdir(parents=True, exist_ok=True)
512
527
  _PID_FILE.write_text(str(os.getpid()))
513
- _run_loop(workspace_root, only_keys)
528
+ _run_loop(workspace_root, only_keys, stories_only)
514
529
  return
515
530
 
516
- print(f"unknown command '{cmd}'. use: run | resume | ticket | stop | status | update")
531
+ print(f"unknown command '{cmd}'. use: run | resume | ticket | story | stop | status | update")
517
532
  sys.exit(1)
518
533
 
519
534