@1mancompany/onemancompany 0.7.32 → 0.7.33
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.
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -37,6 +37,12 @@ from onemancompany.core.models import (
|
|
|
37
37
|
)
|
|
38
38
|
from onemancompany.core.store import _read_yaml, _write_yaml, mark_dirty
|
|
39
39
|
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Configurable constants
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
HISTORY_MAX_ENTRIES: int = 100
|
|
45
|
+
|
|
40
46
|
# ---------------------------------------------------------------------------
|
|
41
47
|
# Per-slug threading locks (same pattern as project_archive.py)
|
|
42
48
|
# ---------------------------------------------------------------------------
|
|
@@ -269,8 +275,8 @@ def _append_history(data: dict, field: str, old_value, new_value, changed_by: st
|
|
|
269
275
|
"new_value": str(new_value) if new_value is not None else None,
|
|
270
276
|
"changed_by": changed_by,
|
|
271
277
|
})
|
|
272
|
-
if len(data["history"]) >
|
|
273
|
-
data["history"] = data["history"][-
|
|
278
|
+
if len(data["history"]) > HISTORY_MAX_ENTRIES:
|
|
279
|
+
data["history"] = data["history"][-HISTORY_MAX_ENTRIES:]
|
|
274
280
|
|
|
275
281
|
|
|
276
282
|
def create_issue(
|
|
@@ -355,8 +361,12 @@ def list_issues(
|
|
|
355
361
|
priority: IssuePriority | None = None,
|
|
356
362
|
labels: list[str] | None = None,
|
|
357
363
|
sprint: str | None = None,
|
|
364
|
+
assignee_id: str | None = None,
|
|
358
365
|
) -> list[dict]:
|
|
359
|
-
"""List issues for a product, optionally filtered.
|
|
366
|
+
"""List issues for a product, optionally filtered.
|
|
367
|
+
|
|
368
|
+
assignee_id: filter by assignee. Empty string "" means unassigned.
|
|
369
|
+
"""
|
|
360
370
|
issues_path = _issues_dir(slug)
|
|
361
371
|
if not issues_path.exists():
|
|
362
372
|
return []
|
|
@@ -378,6 +388,14 @@ def list_issues(
|
|
|
378
388
|
continue
|
|
379
389
|
if sprint is not None and data.get("sprint") != sprint:
|
|
380
390
|
continue
|
|
391
|
+
if assignee_id is not None:
|
|
392
|
+
issue_assignee = data.get("assignee_id") or ""
|
|
393
|
+
if assignee_id == "":
|
|
394
|
+
# Filter for unassigned
|
|
395
|
+
if issue_assignee:
|
|
396
|
+
continue
|
|
397
|
+
elif issue_assignee != assignee_id:
|
|
398
|
+
continue
|
|
381
399
|
results.append(data)
|
|
382
400
|
return results
|
|
383
401
|
|
|
@@ -23,6 +23,16 @@ from onemancompany.core.system_cron import system_cron
|
|
|
23
23
|
# Priorities that auto-trigger project creation
|
|
24
24
|
_AUTO_PROJECT_PRIORITIES = {IssuePriority.P0.value, IssuePriority.P1.value}
|
|
25
25
|
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Configurable thresholds (B4 audit: extracted from inline magic numbers)
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
KR_LAGGING_THRESHOLD: int = 50 # KR progress % below which it's "lagging"
|
|
31
|
+
MAX_ACTIVE_PROJECTS: int = 3 # Max concurrent active projects per product
|
|
32
|
+
BACKLOG_GROOMING_THRESHOLD: int = 5 # P2/P3 unscheduled issues before grooming nudge
|
|
33
|
+
STALE_REVIEW_HOURS: int = 24 # Hours before an open review is considered stale
|
|
34
|
+
BLOCKED_DAYS_THRESHOLD: int = 7 # Days before a blocked issue is flagged
|
|
35
|
+
UNHANDLED_BACKLOG_THRESHOLD: int = 2 # Unhandled backlog issues before alert
|
|
26
36
|
|
|
27
37
|
# ---------------------------------------------------------------------------
|
|
28
38
|
# Trigger handlers
|
|
@@ -332,7 +342,7 @@ async def check_kr_progress(product_slug: str) -> list[dict]:
|
|
|
332
342
|
if target <= 0:
|
|
333
343
|
continue
|
|
334
344
|
progress_pct = current / target * 100
|
|
335
|
-
if progress_pct >=
|
|
345
|
+
if progress_pct >= KR_LAGGING_THRESHOLD:
|
|
336
346
|
continue
|
|
337
347
|
|
|
338
348
|
# Check if an open issue already exists for this KR
|
|
@@ -415,7 +425,7 @@ async def run_product_check(product_slug: str) -> dict:
|
|
|
415
425
|
|
|
416
426
|
# High priority + no active project → create project
|
|
417
427
|
if priority in _AUTO_PROJECT_PRIORITIES and not linked:
|
|
418
|
-
if len(active_for_product) >=
|
|
428
|
+
if len(active_for_product) >= MAX_ACTIVE_PROJECTS:
|
|
419
429
|
logger.debug("[PRODUCT_CHECK] Skipping project for issue {} — 3+ active projects", issue["id"])
|
|
420
430
|
continue
|
|
421
431
|
project_id = await _create_project_for_issue(product_slug, issue)
|
|
@@ -430,7 +440,7 @@ async def run_product_check(product_slug: str) -> dict:
|
|
|
430
440
|
|
|
431
441
|
# Has assignee but no project → create project
|
|
432
442
|
elif issue.get("assignee_id") and not linked:
|
|
433
|
-
if len(active_for_product) >=
|
|
443
|
+
if len(active_for_product) >= MAX_ACTIVE_PROJECTS:
|
|
434
444
|
continue
|
|
435
445
|
project_id = await _create_project_for_issue(product_slug, issue)
|
|
436
446
|
if project_id:
|
|
@@ -484,34 +494,31 @@ async def run_product_check(product_slug: str) -> dict:
|
|
|
484
494
|
logger.debug("[PRODUCT_CHECK] Invalid end_date '{}' on sprint {}", end_date_str, active_sprint.get("id"))
|
|
485
495
|
|
|
486
496
|
# --- Step 4: Backlog grooming reminder ---
|
|
487
|
-
_BACKLOG_GROOMING_THRESHOLD = 5
|
|
488
497
|
unscheduled_low = [
|
|
489
498
|
i for i in all_issues
|
|
490
499
|
if i.get("priority") in (IssuePriority.P2.value, IssuePriority.P3.value)
|
|
491
500
|
and not i.get("sprint")
|
|
492
501
|
and i.get("status") not in (IssueStatus.DONE.value, IssueStatus.RELEASED.value)
|
|
493
502
|
]
|
|
494
|
-
if len(unscheduled_low) >=
|
|
503
|
+
if len(unscheduled_low) >= BACKLOG_GROOMING_THRESHOLD:
|
|
495
504
|
actions_taken.append(f"{len(unscheduled_low)} P2/P3 issues unscheduled — backlog grooming needed")
|
|
496
505
|
|
|
497
|
-
# --- Step 5: Stale review check
|
|
506
|
+
# --- Step 5: Stale review check ---
|
|
498
507
|
from datetime import datetime as _datetime, timedelta as _timedelta
|
|
499
508
|
|
|
500
509
|
open_reviews = prod.list_reviews(product_slug, status="open")
|
|
501
|
-
_STALE_REVIEW_HOURS = 24
|
|
502
510
|
stale_reviews = []
|
|
503
511
|
for rev in open_reviews:
|
|
504
512
|
try:
|
|
505
513
|
created = _datetime.fromisoformat(rev.get("created_at", ""))
|
|
506
|
-
if _datetime.now() - created > _timedelta(hours=
|
|
514
|
+
if _datetime.now() - created > _timedelta(hours=STALE_REVIEW_HOURS):
|
|
507
515
|
stale_reviews.append(rev)
|
|
508
516
|
except (ValueError, TypeError):
|
|
509
517
|
logger.debug("[PRODUCT_CHECK] Invalid created_at on review {}", rev.get("id"))
|
|
510
518
|
if stale_reviews:
|
|
511
|
-
actions_taken.append(f"{len(stale_reviews)} stale review(s) open > {
|
|
519
|
+
actions_taken.append(f"{len(stale_reviews)} stale review(s) open > {STALE_REVIEW_HOURS}h")
|
|
512
520
|
|
|
513
|
-
# --- Step 6: Blocked issue check
|
|
514
|
-
_BLOCKED_DAYS_THRESHOLD = 7
|
|
521
|
+
# --- Step 6: Blocked issue check ---
|
|
515
522
|
for issue in all_issues:
|
|
516
523
|
if issue.get("status") in (IssueStatus.DONE.value, IssueStatus.RELEASED.value):
|
|
517
524
|
continue
|
|
@@ -532,9 +539,9 @@ async def run_product_check(product_slug: str) -> dict:
|
|
|
532
539
|
oldest_blocked_at = link_created
|
|
533
540
|
except (ValueError, TypeError):
|
|
534
541
|
logger.debug("[PRODUCT_CHECK] Invalid created_at on link in issue {}", issue.get("id"))
|
|
535
|
-
if oldest_blocked_at and _datetime.now() - oldest_blocked_at > _timedelta(days=
|
|
542
|
+
if oldest_blocked_at and _datetime.now() - oldest_blocked_at > _timedelta(days=BLOCKED_DAYS_THRESHOLD):
|
|
536
543
|
actions_taken.append(
|
|
537
|
-
f"Issue '{issue['title']}' blocked for >{
|
|
544
|
+
f"Issue '{issue['title']}' blocked for >{BLOCKED_DAYS_THRESHOLD} days"
|
|
538
545
|
)
|
|
539
546
|
|
|
540
547
|
# --- Step 7: Check if owner review is needed ---
|
|
@@ -546,7 +553,7 @@ async def run_product_check(product_slug: str) -> dict:
|
|
|
546
553
|
i for i in all_issues
|
|
547
554
|
if i.get("status") == IssueStatus.BACKLOG.value and not i.get("linked_task_ids")
|
|
548
555
|
]
|
|
549
|
-
if len(unhandled_backlog) >
|
|
556
|
+
if len(unhandled_backlog) > UNHANDLED_BACKLOG_THRESHOLD:
|
|
550
557
|
needs_review = True
|
|
551
558
|
review_reasons.append(f"{len(unhandled_backlog)} unhandled backlog issues")
|
|
552
559
|
|
|
@@ -570,7 +577,7 @@ async def run_product_check(product_slug: str) -> dict:
|
|
|
570
577
|
logger.debug("[PRODUCT_CHECK] Invalid end_date on sprint {} for review check", active_sprint.get("id"))
|
|
571
578
|
|
|
572
579
|
# Backlog grooming threshold → needs owner review
|
|
573
|
-
if len(unscheduled_low) >=
|
|
580
|
+
if len(unscheduled_low) >= BACKLOG_GROOMING_THRESHOLD:
|
|
574
581
|
needs_review = True
|
|
575
582
|
review_reasons.append(f"{len(unscheduled_low)} P2/P3 issues need sprint assignment")
|
|
576
583
|
|