Skip to content

Commit 61a820c

Browse files
djm81cursoragent
andcommitted
fix(backlog): address CodeQL/Codex PR 181 findings
- Replace empty except with debug_log_operation in _load_standup_config and _load_backlog_config (correct signature: operation, target, status, error) - Add dim console message in sprint end date parse except block - Gate summarize prompt description/comments on --comments; add include_comments to _build_summarize_prompt_content and call site - Add test for metadata-only summarize when include_comments=False; update existing test to pass include_comments=True Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent bb7214f commit 61a820c

File tree

2 files changed

+63
-30
lines changed

2 files changed

+63
-30
lines changed

src/specfact_cli/commands/backlog_commands.py

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ def _load_standup_config() -> dict[str, Any]:
168168
with open(path, encoding="utf-8") as f:
169169
data = yaml.safe_load(f) or {}
170170
config = dict(data.get("standup", data))
171-
except Exception:
172-
pass
171+
except Exception as exc:
172+
debug_log_operation("config_load", str(path), "error", error=repr(exc))
173173
break
174174
if os.environ.get("SPECFACT_STANDUP_STATE"):
175175
config["default_state"] = os.environ["SPECFACT_STANDUP_STATE"]
@@ -203,8 +203,8 @@ def _load_backlog_config() -> dict[str, Any]:
203203
config = dict(nested) if isinstance(nested, dict) else {}
204204
else:
205205
config = dict(data) if isinstance(data, dict) else {}
206-
except Exception:
207-
pass
206+
except Exception as exc:
207+
debug_log_operation("config_load", str(path), "error", error=repr(exc))
208208
break
209209
return config
210210

@@ -489,19 +489,25 @@ def _build_summarize_prompt_content(
489489
filter_context: dict[str, Any],
490490
include_value_score: bool = False,
491491
comments_by_item_id: dict[str, list[str]] | None = None,
492+
include_comments: bool = False,
492493
) -> str:
493494
"""
494495
Build prompt content for standup summary: instruction + filter context + per-item data.
495496
496-
Includes body (description) and annotations (comments) per item so an LLM can produce
497-
a meaningful summary. For use with slash command (e.g. specfact.daily) or copy-paste to Copilot.
497+
When include_comments is True, includes body (description) and annotations (comments) per item
498+
so an LLM can produce a meaningful summary. When False, only metadata (id, title, status,
499+
assignees, last updated) is included to avoid leaking sensitive or large context.
500+
For use with slash command (e.g. specfact.daily) or copy-paste to Copilot.
498501
"""
499502
lines: list[str] = []
500503
lines.append("--- BEGIN STANDUP PROMPT ---")
501504
lines.append("Generate a concise daily standup summary from the following data.")
502-
lines.append(
503-
"Include: current focus, blockers, and pending items. Use each item's description and comments for context. Keep it short and actionable."
504-
)
505+
if include_comments:
506+
lines.append(
507+
"Include: current focus, blockers, and pending items. Use each item's description and comments for context. Keep it short and actionable."
508+
)
509+
else:
510+
lines.append("Include: current focus and pending items from the metadata below. Keep it short and actionable.")
505511
lines.append("")
506512
lines.append("## Filter context")
507513
lines.append(f"- Adapter: {filter_context.get('adapter', '—')}")
@@ -510,7 +516,8 @@ def _build_summarize_prompt_content(
510516
lines.append(f"- Assignee: {filter_context.get('assignee', '—')}")
511517
lines.append(f"- Limit: {filter_context.get('limit', '—')}")
512518
lines.append("")
513-
lines.append("## Standup data (with description and comments)")
519+
data_header = "Standup data (with description and comments)" if include_comments else "Standup data (metadata only)"
520+
lines.append(f"## {data_header}")
514521
lines.append("")
515522
comments_map = comments_by_item_id or {}
516523
for item in items:
@@ -523,24 +530,25 @@ def _build_summarize_prompt_content(
523530
item.updated_at.strftime("%Y-%m-%d %H:%M") if hasattr(item.updated_at, "strftime") else str(item.updated_at)
524531
)
525532
lines.append(f"- **Last updated:** {updated}")
526-
body = (item.body_markdown or "").strip()
527-
if body:
528-
snippet = body[:_SUMMARIZE_BODY_TRUNCATE]
529-
if len(body) > _SUMMARIZE_BODY_TRUNCATE:
530-
snippet += "\n..."
531-
lines.append("- **Description:**")
532-
lines.append(snippet)
533-
lines.append("")
534-
yesterday, today, blockers = _parse_standup_from_body(item.body_markdown or "")
535-
if yesterday or today:
536-
lines.append(f"- **Progress:** Yesterday: {yesterday or '—'}; Today: {today or '—'}")
537-
if blockers:
538-
lines.append(f"- **Blockers:** {blockers}")
539-
item_comments = comments_map.get(item.id, [])
540-
if item_comments:
541-
lines.append("- **Comments (annotations):**")
542-
for c in item_comments:
543-
lines.append(f" - {c}")
533+
if include_comments:
534+
body = (item.body_markdown or "").strip()
535+
if body:
536+
snippet = body[:_SUMMARIZE_BODY_TRUNCATE]
537+
if len(body) > _SUMMARIZE_BODY_TRUNCATE:
538+
snippet += "\n..."
539+
lines.append("- **Description:**")
540+
lines.append(snippet)
541+
lines.append("")
542+
yesterday, today, blockers = _parse_standup_from_body(item.body_markdown or "")
543+
if yesterday or today:
544+
lines.append(f"- **Progress:** Yesterday: {yesterday or '—'}; Today: {today or '—'}")
545+
if blockers:
546+
lines.append(f"- **Blockers:** {blockers}")
547+
item_comments = comments_map.get(item.id, [])
548+
if item_comments:
549+
lines.append("- **Comments (annotations):**")
550+
for c in item_comments:
551+
lines.append(f" - {c}")
544552
if item.story_points is not None:
545553
lines.append(f"- **Story points:** {item.story_points}")
546554
if item.priority is not None:
@@ -1224,6 +1232,7 @@ def daily(
12241232
filter_context=filter_ctx,
12251233
include_value_score=include_score,
12261234
comments_by_item_id=comments_by_item_id or None,
1235+
include_comments=include_comments,
12271236
)
12281237
if summarize_to:
12291238
Path(summarize_to).write_text(content, encoding="utf-8")
@@ -1309,7 +1318,7 @@ def daily(
13091318
end_date = dt.strptime(str(sprint_end)[:10], "%Y-%m-%d").date()
13101319
console.print(f"[dim]{_format_sprint_end_header(end_date)}[/dim]")
13111320
except (ValueError, TypeError):
1312-
pass
1321+
console.print("[dim]Sprint end date could not be parsed; header skipped.[/dim]")
13131322

13141323
def _add_standup_rows_to_table(tbl: Table, row_list: list[dict[str, Any]], include_pri: bool) -> None:
13151324
for r in row_list:

tests/unit/commands/test_backlog_daily.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ def test_summarize_prompt_contains_per_item_data(self) -> None:
502502
assert "## " in content
503503

504504
def test_summarize_prompt_includes_body_and_comments_when_provided(self) -> None:
505-
"""Summarize prompt includes description (body) and comments so LLM can create meaningful summary."""
505+
"""Summarize prompt includes description (body) and comments when include_comments=True."""
506506
items = [
507507
_item(
508508
"1",
@@ -517,11 +517,35 @@ def test_summarize_prompt_includes_body_and_comments_when_provided(self) -> None
517517
filter_context={"adapter": "github", "state": "open", "sprint": "—", "assignee": "—", "limit": 20},
518518
include_value_score=False,
519519
comments_by_item_id=comments_by_id,
520+
include_comments=True,
520521
)
521522
assert "Description" in content and "issue description" in content
522523
assert "Comments" in content or "annotations" in content
523524
assert "In progress" in content and "Blocked on API" in content
524525

526+
def test_summarize_prompt_metadata_only_when_include_comments_false(self) -> None:
527+
"""Summarize prompt omits description and comments when include_comments=False (gated on --comments)."""
528+
items = [
529+
_item(
530+
"1",
531+
"Story one",
532+
state="open",
533+
body_markdown="This is the issue description and context.",
534+
),
535+
]
536+
comments_by_id = {"1": ["Comment from Alice: In progress."]}
537+
content = _build_summarize_prompt_content(
538+
items,
539+
filter_context={"adapter": "github", "state": "open", "sprint": "—", "assignee": "—", "limit": 20},
540+
include_value_score=False,
541+
comments_by_item_id=comments_by_id,
542+
include_comments=False,
543+
)
544+
assert "metadata only" in content
545+
assert "issue description" not in content
546+
assert "In progress" not in content
547+
assert "Status:" in content and "Story one" in content
548+
525549
def test_summarize_prompt_has_start_end_markers(self) -> None:
526550
"""Summarize prompt is wrapped in BEGIN/END markers for extraction or emphasis."""
527551
items = [_item("1", "Story", state="open")]

0 commit comments

Comments
 (0)