Skip to content

feat: pending instinct TTL pruning and /prune command#725

Open
chris-yyau wants to merge 6 commits intoaffaan-m:mainfrom
chris-yyau:feat/pending-instinct-prune
Open

feat: pending instinct TTL pruning and /prune command#725
chris-yyau wants to merge 6 commits intoaffaan-m:mainfrom
chris-yyau:feat/pending-instinct-prune

Conversation

@chris-yyau
Copy link
Contributor

@chris-yyau chris-yyau commented Mar 20, 2026

Summary

  • Add instinct-cli.py prune subcommand — auto-deletes pending instincts older than 30 days (configurable --max-age)
  • Enhance status command — shows pending instinct count, warns at 5+, highlights instincts expiring within 7 days
  • Add /prune slash command for user-facing manual pruning
  • Call prune from observer-loop.sh before each analysis cycle

Problem

Pending instincts generated by the observer accumulate indefinitely with no cleanup mechanism. There is no TTL, no cap, and no notification. Users must manually run /instinct-status and /promote — but without a reminder, pending instincts pile up forever.

Design Rationale

Auto-promote was considered and rejected — frequency of observation does not establish correctness. A pattern appearing in multiple projects proves it exists, not that it's good. TTL-based garbage collection is simpler and safer: if a pruned pattern is real, the observer will regenerate it.

Changes

File Change
scripts/instinct-cli.py Add prune subcommand, _collect_pending_dirs(), _parse_created_date(), _collect_pending_instincts() helpers, enhance cmd_status() with pending warnings
agents/observer-loop.sh Call prune --quiet before main analysis loop
commands/prune.md New /prune slash command

Test plan

  • python3 instinct-cli.py prune --dry-run — verify it finds pending instincts without deleting
  • python3 instinct-cli.py prune — verify deletion of instincts older than 30 days
  • python3 instinct-cli.py prune --max-age 0 — verify all pending instincts would be pruned
  • python3 instinct-cli.py status — verify pending count shown, expiry warnings displayed
  • Verify observer-loop.sh prune call doesn't block observer startup

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a /prune command to remove pending instincts older than 30 days (configurable via --max-age; preview with --dry-run) and automatic pruning at startup of the continuous-learning observer loop.
  • Improvements

    • Status now shows pending instinct counts and imminent-expiry warnings.
    • More robust import/export/promote flows, safer file handling, deterministic project detection, deduplication, and consistent UTF‑8/YAML handling.
  • Documentation

    • Command catalog updated to 60 and prune usage docs added.

Pending instincts generated by the observer accumulate indefinitely
with no cleanup mechanism. This adds lifecycle management:

- `instinct-cli.py prune` — delete pending instincts older than 30 days
  (configurable via --max-age). Supports --dry-run and --quiet flags.
- Enhanced `status` command — shows pending count, warns at 5+,
  highlights instincts expiring within 7 days.
- `observer-loop.sh` — runs prune before each analysis cycle.
- `/prune` slash command — user-facing command for manual pruning.

Design rationale: council consensus (4/4) rejected auto-promote in
favor of TTL-based garbage collection. Frequency of observation does
not establish correctness. Unreviewed pending instincts auto-delete
after 30 days; if the pattern is real, the observer will regenerate it.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Copilot AI review requested due to automatic review settings March 20, 2026 18:23
@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 20, 2026

Analyzing 5000 commits...

@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 20, 2026

Analysis Failed

Not Found - https://docs.github.com/rest/git/refs#get-a-reference

Troubleshooting
Cause Resolution
Large repository Analysis may timeout on repos with extensive history
API rate limits Wait 15 minutes before retrying
Network issues Queue timeout is 15 minutes; retry may succeed
Permissions Verify app has Contents: Read access

Retry: /ecc-tools analyze


Report Issue | ECC Tools

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added a new prune CLI command and docs; observer startup now invokes instinct-cli.py prune --quiet. instinct-cli.py gained pruning logic, pending-stats in status, YAML quoting/UTF-8 handling, import/export/promotion hardening, and advisory locking for registry updates.

Changes

Cohort / File(s) Summary
Documentation
commands/prune.md, AGENTS.md, README.md
New prune command docs and updated command count/command listings to include /prune (30d TTL).
Observer Integration
skills/continuous-learning-v2/agents/observer-loop.sh
Invoke instinct-cli.py prune --quiet at startup (uses ${CLV2_PYTHON_CMD:-python3}), append output to log, tolerate failures.
Core CLI Implementation
skills/continuous-learning-v2/scripts/instinct-cli.py
Added prune subcommand (scan global + per-project instincts/pending, parse frontmatter or mtime, delete >= --max-age, support --dry-run/--quiet); status now reports pending counts and expiry warnings; introduced _yaml_quote() and UTF-8 writes; hardened frontmatter parsing and default confidence; improved project root detection; advisory fcntl.flock for projects.json updates; refined import/export deduplication, scoping, stale-file cleanup; promotion supports --dry-run.

Sequence Diagram(s)

sequenceDiagram
  participant Observer as Observer Script
  participant CLI as instinct-cli.py
  participant FS as Filesystem
  participant Registry as projects.json (locked)
  Observer->>CLI: run "prune --quiet"
  CLI->>FS: enumerate pending dirs (global + per-project)
  CLI->>FS: read file frontmatter (created) or use mtime
  CLI->>CLI: compute age_days vs --max-age
  CLI->>FS: delete files (or list in dry-run)
  CLI->>Registry: acquire advisory lock (fcntl.flock) for registry updates
  Registry-->>CLI: lock released after update
  CLI-->>Observer: return summary/status
  Note over Observer,CLI: Observer proceeds to start analysis loop
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • affaan-m

Poem

🐰 I peek at pending notes where old ideas hide,

Thirty days of dust and I gently tidy the slide.
The watcher asks pruning before its daily run,
Locks snug the registry, cleanup quietly done.
A hop, a trim, new space for thoughts to glide.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: pending instinct TTL pruning and /prune command' directly and specifically describes the main changes: adding TTL-based pruning for pending instincts and the new /prune command.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 20, 2026

Greptile Summary

This PR introduces a TTL-based garbage-collection mechanism for pending instincts: a new prune subcommand in instinct-cli.py, a companion /prune slash command, automatic pruning at observer startup in observer-loop.sh, and enriched pending-instinct warnings in the status command. It also bundles several hardening improvements — file locking for the project registry, proper YAML quoting via _yaml_quote, scoped deduplication on import, consistent UTF-8 encoding, and more robust frontmatter parsing.

Key findings:

  • Silent data loss on import (instinct-cli.py lines 555–560): The new source-level deduplication dict uses inst.get('id') as the key. When an instinct has no id field, get returns None, so every ID-less instinct in the file maps to best_by_id[None]. Only the highest-confidence entry survives; all others are silently dropped. Additionally, the surviving entry is written as id: None (a bare YAML string) in the output file.

  • Unquoted project_name field (instinct-cli.py line 664): The PR carefully introduces _yaml_quote() for trigger and imported_from, but the project_name field written during project-scoped imports is still an unquoted bare scalar. A project name containing :, #, a leading *, or a newline produces structurally invalid YAML, silently corrupting the import file.

  • Several issues raised in earlier review rounds have been addressed in this revision: .yaml/.yml file extensions are now collected during pruning, the expiring_soon filter correctly excludes already-expired items, and successfully-deleted items are tracked separately from merely-expired ones.

Confidence Score: 3/5

  • Two P1 logic bugs in cmd_import (ID-less instinct loss and unquoted project_name) should be fixed before merge; the prune and status features themselves are safe.
  • The prune command, observer integration, and status enhancements are well-structured and the previous round's issues have largely been resolved. However, the new deduplication logic introduces a regression for any import file containing instincts without id fields, and the inconsistent application of _yaml_quote can produce malformed YAML for project-scoped imports. These two bugs are in the modified import path and could silently corrupt instinct data.
  • skills/continuous-learning-v2/scripts/instinct-cli.py — specifically the deduplication block (lines 555–560) and the project_name serialization (line 664)

Important Files Changed

Filename Overview
skills/continuous-learning-v2/scripts/instinct-cli.py Core file for this PR — adds prune command, TTL constants, pending instinct helpers, and numerous hardening improvements (file locking, encoding, YAML quoting). Two new logic issues: deduplication silently collapses all ID-less instincts via None key, and project_name is written unquoted while trigger/imported_from now use _yaml_quote.
skills/continuous-learning-v2/agents/observer-loop.sh Adds a one-time prune call at observer startup before the main loop, with stderr redirected to the log and a non-fatal fallback. The call is outside the while loop so it runs once per process lifetime, not every cycle.
commands/prune.md New /prune slash command that documents --max-age and --dry-run flags in Usage but does not forward user-supplied arguments to the underlying CLI call.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[observer-loop.sh starts] --> B["instinct-cli.py prune --quiet"]
    B --> C[_collect_pending_instincts]
    C --> D[Scan global pending dir]
    C --> E[Scan per-project pending dirs]
    D & E --> F{age_days >= max_age?}
    F -- Yes --> G[unlink file]
    F -- No --> H[Keep file]
    G --> I{OSError?}
    I -- No --> J[Add to pruned_items]
    I -- Yes --> K[Warn to stderr]
    J & H & K --> L[Print summary unless quiet]
    L --> M[return 0]

    N["slash command: /prune"] --> O["instinct-cli.py prune"]
    P["instinct-cli.py status"] --> Q[_collect_pending_instincts]
    Q --> R{count >= 5?}
    R -- Yes --> S[Show accumulation warning]
    Q --> T{age in expiry window?}
    T -- Yes --> U[Show days remaining]
Loading

Last reviewed commit: "fix: YAML single-quo..."

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds TTL-based cleanup for accumulated “pending instincts” and surfaces pending backlog visibility to users/operators, integrating the cleanup into the observer’s runtime loop and providing a user-facing /prune command.

Changes:

  • Add instinct-cli.py prune subcommand to delete pending instincts older than a configurable max age (default 30d).
  • Enhance status to display pending instinct counts and highlight items nearing expiry.
  • Invoke pruning automatically from observer-loop.sh, and document a new /prune slash command.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
skills/continuous-learning-v2/scripts/instinct-cli.py Adds prune/status pending features plus related helpers and some YAML safety/locking improvements.
skills/continuous-learning-v2/agents/observer-loop.sh Runs instinct-cli.py prune --quiet before the main observer loop.
commands/prune.md Documents the new /prune command and CLI invocation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1261 to +1267
# ─────────────────────────────────────────────
# Pending Instinct Helpers
# ─────────────────────────────────────────────

def _collect_pending_dirs() -> list[Path]:
"""Return all pending instinct directories (global + per-project)."""
dirs = []
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

There are duplicate definitions of _collect_pending_dirs() and _parse_created_date() earlier in the file (around lines 1141+). The later definitions here silently override the earlier ones, which is error-prone and makes it unclear which behavior is intended. Please remove the earlier duplicates and keep a single authoritative implementation (and a single section header) so future edits don’t accidentally change behavior by reordering code.

Copilot uses AI. Check for mistakes.
Comment on lines +1269 to +1275
if global_pending.exists():
dirs.append(global_pending)
if PROJECTS_DIR.exists():
for project_dir in sorted(PROJECTS_DIR.iterdir()):
if project_dir.is_dir():
pending = project_dir / "instincts" / "pending"
if pending.exists():
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

_collect_pending_dirs() currently uses Path.exists() for the pending paths. If a non-directory file exists at .../pending, it will be included and _collect_pending_instincts() will crash on pending_dir.iterdir(). Use is_dir() (or otherwise guard before iterating) for both the global and per-project pending paths.

Suggested change
if global_pending.exists():
dirs.append(global_pending)
if PROJECTS_DIR.exists():
for project_dir in sorted(PROJECTS_DIR.iterdir()):
if project_dir.is_dir():
pending = project_dir / "instincts" / "pending"
if pending.exists():
if global_pending.is_dir():
dirs.append(global_pending)
if PROJECTS_DIR.exists():
for project_dir in sorted(PROJECTS_DIR.iterdir()):
if project_dir.is_dir():
pending = project_dir / "instincts" / "pending"
if pending.is_dir():

Copilot uses AI. Check for mistakes.
Comment on lines +1361 to +1364
pending = _collect_pending_instincts()

expired = [p for p in pending if p["age_days"] > max_age]
remaining = [p for p in pending if p["age_days"] <= max_age]
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The PR description/test plan says --max-age 0 should prune all pending instincts, but the current predicate only prunes items where age_days > max_age. With max_age=0, items created today will have age_days == 0 and will not be pruned. Either update the docs/test plan, or change the predicate (and/or age calculation granularity) to match the intended semantics.

Copilot uses AI. Check for mistakes.
Comment on lines +1377 to +1394
pruned = 0
for item in expired:
try:
item["path"].unlink()
pruned += 1
except OSError as e:
if not quiet:
print(f"Warning: Failed to delete {item['path']}: {e}", file=sys.stderr)

if not quiet:
if pruned > 0:
print(f"\nPruned {pruned} pending instinct(s) older than {max_age} days.")
for item in expired:
print(f" - {item['name']} (age: {item['age_days']}d)")
else:
print(f"No pending instincts older than {max_age} days.")
print(f"\nSummary: {pruned} pruned, {len(remaining)} remaining")

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

When deletions fail, the output still prints every item in expired as if it was pruned (even though pruned may be less than len(expired)). Track successes/failures per file and only list the ones that were actually deleted (and consider including failures in the summary so the remaining count is accurate).

Suggested change
pruned = 0
for item in expired:
try:
item["path"].unlink()
pruned += 1
except OSError as e:
if not quiet:
print(f"Warning: Failed to delete {item['path']}: {e}", file=sys.stderr)
if not quiet:
if pruned > 0:
print(f"\nPruned {pruned} pending instinct(s) older than {max_age} days.")
for item in expired:
print(f" - {item['name']} (age: {item['age_days']}d)")
else:
print(f"No pending instincts older than {max_age} days.")
print(f"\nSummary: {pruned} pruned, {len(remaining)} remaining")
pruned_items = []
failed_items = []
for item in expired:
try:
item["path"].unlink()
pruned_items.append(item)
except OSError as e:
failed_items.append(item)
if not quiet:
print(f"Warning: Failed to delete {item['path']}: {e}", file=sys.stderr)
pruned = len(pruned_items)
if not quiet:
if pruned > 0:
print(f"\nPruned {pruned} pending instinct(s) older than {max_age} days.")
for item in pruned_items:
print(f" - {item['name']} (age: {item['age_days']}d)")
else:
print(f"No pending instincts older than {max_age} days.")
if failed_items:
print(f"\nFailed to prune {len(failed_items)} pending instinct(s):")
for item in failed_items:
print(f" - {item['name']} (age: {item['age_days']}d) — {item['path']}")
total_remaining = len(remaining) + len(failed_items)
print(f"\nSummary: {pruned} pruned, {total_remaining} remaining")

Copilot uses AI. Check for mistakes.
Comment on lines +578 to +580
# Count source-level duplicates for reporting
source_dupes = len(new_instincts) - len(deduped_instincts)

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

source_dupes is computed but never used. Either remove it or include it in the import summary output so users can see how many duplicates were collapsed within the source file.

Copilot uses AI. Check for mistakes.
Comment on lines +443 to +460
# Pending instinct stats
pending = _collect_pending_instincts()
if pending:
print(f"\n{'-'*60}")
print(f" Pending instincts: {len(pending)} awaiting review")

if len(pending) >= 5:
print(f"\n \u26a0 {len(pending)} pending instincts awaiting review."
f" Unreviewed instincts auto-delete after {PENDING_TTL_DAYS} days.")

# Show instincts expiring within PENDING_EXPIRY_WARNING_DAYS
expiry_threshold = PENDING_TTL_DAYS - PENDING_EXPIRY_WARNING_DAYS
expiring_soon = [p for p in pending if p["age_days"] >= expiry_threshold]
if expiring_soon:
print(f"\n Expiring within {PENDING_EXPIRY_WARNING_DAYS} days:")
for item in expiring_soon:
days_left = max(0, PENDING_TTL_DAYS - item["age_days"])
print(f" - {item['name']} ({days_left}d remaining)")
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

This PR adds new behavior in cmd_status() (pending counts/expiry warnings) and a new cmd_prune() command, but there are existing pytest tests for this module (scripts/test_parse_instinct.py) and none appear to cover pruning or pending-expiry output. Please add/extend tests to cover: created-date parsing, pending collection, prune dry-run vs delete, and status output when pending instincts exist/are expiring.

Copilot uses AI. Check for mistakes.

# Prune expired pending instincts before analysis
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
python3 "${SCRIPT_DIR}/../scripts/instinct-cli.py" prune --quiet 2>/dev/null || true
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

observer-loop.sh hardcodes python3 for pruning, but the launcher script already supports overriding the Python interpreter via CLV2_PYTHON_CMD. Consider using an environment-provided interpreter (e.g., ${CLV2_PYTHON_CMD:-python3}) so prune works in environments where only python is available or where a venv interpreter is required.

Suggested change
python3 "${SCRIPT_DIR}/../scripts/instinct-cli.py" prune --quiet 2>/dev/null || true
"${CLV2_PYTHON_CMD:-python3}" "${SCRIPT_DIR}/../scripts/instinct-cli.py" prune --quiet 2>/dev/null || true

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@skills/continuous-learning-v2/scripts/instinct-cli.py`:
- Line 661: The string appended to output_content uses an unnecessary f-string
prefix; locate the append statement that sets output_content (the line using
output_content += f"source: inherited\n") and remove the leading "f" so it
becomes a normal literal, ensuring no placeholder syntax is present and behavior
is unchanged.
- Around line 1377-1393: The reporting currently iterates over expired items
even when deletions fail, so change the deletion loop to record only
successfully deleted items (e.g., create a list deleted = [] inside the same
scope as pruned and append the item after item["path"].unlink() succeeds and
increment pruned there); keep the existing except branch unchanged. Then update
the summary/printing block to iterate over deleted (not expired) when listing
pruned items and compute pruned from len(deleted) (or continue using pruned but
base the list output on deleted) so failed deletions aren’t reported as pruned;
leave the remaining count logic using remaining unchanged.
- Around line 1141-1154: There are duplicate definitions of
_collect_pending_dirs and _parse_created_date; remove the earlier (first)
definitions so only the later implementations remain to avoid shadowing and
confusion. Locate the first occurrences of _collect_pending_dirs and
_parse_created_date (the ones before the later implementations) and delete those
blocks, keeping the later versions that use the intended behavior (e.g.,
exists()/sorted() semantics); after deletion run linters/tests and ensure all
call sites still resolve to the remaining functions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e56dc84-54c4-4a98-9168-a7b794cfb72f

📥 Commits

Reviewing files that changed from the base of the PR and between 7ccfda9 and e27fb14.

📒 Files selected for processing (3)
  • commands/prune.md
  • skills/continuous-learning-v2/agents/observer-loop.sh
  • skills/continuous-learning-v2/scripts/instinct-cli.py

…output

- Remove duplicate _collect_pending_dirs and _parse_created_date defs
- Use ALLOWED_INSTINCT_EXTENSIONS (.md/.yaml/.yml) instead of .md-only
- Track actually-deleted items separately from expired for accurate output
- Update README.md and AGENTS.md command counts: 59 → 60

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 20, 2026

Analyzing 5000 commits...

@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 20, 2026

Analysis Failed

Not Found - https://docs.github.com/rest/git/refs#get-a-reference

Troubleshooting
Cause Resolution
Large repository Analysis may timeout on repos with extensive history
API rate limits Wait 15 minutes before retrying
Network issues Queue timeout is 15 minutes; retry may succeed
Permissions Verify app has Contents: Read access

Retry: /ecc-tools analyze


Report Issue | ECC Tools

- Use is_dir() instead of exists() for pending path checks
- Change > to >= for --max-age boundary (--max-age 0 now prunes all)
- Use CLV2_PYTHON_CMD env var in observer-loop.sh prune call
- Remove unused source_dupes variable
- Remove extraneous f-string prefix on static string

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 20, 2026

Analyzing 5000 commits...

@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 20, 2026

Analysis Failed

Not Found - https://docs.github.com/rest/git/refs#get-a-reference

Troubleshooting
Cause Resolution
Large repository Analysis may timeout on repos with extensive history
API rate limits Wait 15 minutes before retrying
Network issues Queue timeout is 15 minutes; retry may succeed
Permissions Verify app has Contents: Read access

Retry: /ecc-tools analyze


Report Issue | ECC Tools

Comment on lines +15 to +23
```bash
python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" prune
```

Or if `CLAUDE_PLUGIN_ROOT` is not set (manual installation):

```bash
python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py prune
```
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 Slash command arguments are not forwarded to the CLI

The "Usage" section documents /prune --max-age 60 and /prune --dry-run, implying the user can pass flags to control pruning. However the "Implementation" section shows a hardcoded call with no mechanism to forward those flags:

python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" prune

When Claude executes a slash command, user-supplied arguments (everything after /prune) should be forwarded to the underlying CLI call. Without this, /prune --max-age 60 will silently run the default 30-day prune instead of a 60-day one — the user will have no indication their flag was ignored.

The implementation should include the argument passthrough, for example:

python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" prune $ARGUMENTS

(or use whatever variable/mechanism the command framework uses for user-supplied arguments)

Co-Authored-By: Claude <noreply@anthropic.com>
@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 20, 2026

Analyzing 5000 commits...

@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 20, 2026

Analysis Failed

Not Found - https://docs.github.com/rest/git/refs#get-a-reference

Troubleshooting
Cause Resolution
Large repository Analysis may timeout on repos with extensive history
API rate limits Wait 15 minutes before retrying
Network issues Queue timeout is 15 minutes; retry may succeed
Permissions Verify app has Contents: Read access

Retry: /ecc-tools analyze


Report Issue | ECC Tools

Comment on lines +1253 to +1265
for fmt in (
"%Y-%m-%dT%H:%M:%S%z",
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%d",
):
try:
dt = datetime.strptime(date_str, fmt)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
except ValueError:
continue
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 Microsecond timestamps not parsed — mtime fallback used for most Python-generated files

The four explicit strptime patterns do not include any variant with fractional seconds. Python's datetime.isoformat() (the most common way Python code writes timestamps) emits strings like 2025-01-15T10:30:00.123456+00:00 — none of the four formats matches this pattern, so every pending file whose created field was written by Python would silently fall through to the st_mtime fallback.

This means the age calculated for those files is based on the file's last-modification time rather than the intended creation date, which can be wrong after copies, syncs, or any tool that touches the file.

datetime.fromisoformat() (available since Python 3.7) handles all ISO 8601 variants including fractional seconds, +00:00, and the Z suffix (Python 3.11+), making the explicit format list unnecessary:

if in_frontmatter and ':' in line:
    key, value = line.split(':', 1)
    if key.strip() == 'created':
        date_str = value.strip().strip('"').strip("'")
        try:
            # fromisoformat handles microseconds, timezone offsets, and Z
            dt = datetime.fromisoformat(date_str)
            if dt.tzinfo is None:
                dt = dt.replace(tzinfo=timezone.utc)
            return dt
        except ValueError:
            pass  # fall through to mtime fallback

If Python 3.6 compatibility is required, add a fallback that strips fractional seconds before trying the explicit formats.

Comment on lines 113 to +120

def _yaml_quote(value: str) -> str:
"""Quote a string for safe YAML frontmatter serialization.

Uses double quotes and escapes embedded double-quote characters to
prevent malformed YAML when the value contains quotes.
"""
escaped = value.replace('\\', '\\\\').replace('"', '\\"')
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 _yaml_quote does not escape newlines or other control characters

YAML double-quoted strings support \n, \t, and other escape sequences for control characters, but _yaml_quote only escapes \ and ". If a trigger value contains a literal newline character (e.g., from a multi-line observation pattern), the emitted YAML would be invalid:

trigger: "some pattern
  with a newline"   # ← not a valid YAML double-quoted scalar

Any downstream parser reading such a file would fail or return garbage data. The fix is to escape control characters before quoting:

def _yaml_quote(value: str) -> str:
    escaped = (
        value.replace('\\', '\\\\')
             .replace('"', '\\"')
             .replace('\n', '\\n')
             .replace('\r', '\\r')
             .replace('\t', '\\t')
    )
    return f'"{escaped}"'

This is used in cmd_import, cmd_export, _promote_specific, and _promote_auto — all places that write YAML files — so fixing it here covers all call sites.

Comment on lines +1306 to +1310
def cmd_prune(args) -> int:
"""Delete pending instincts older than the TTL threshold."""
max_age = args.max_age
dry_run = args.dry_run
quiet = args.quiet
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 cmd_prune always returns 0 even when all deletions fail

cmd_prune returns 0 (success) unconditionally, even if every unlink() call raised an OSError. When called from observer-loop.sh the || true already masks the exit code, so it doesn't matter there. But callers who run instinct-cli.py prune in a script and check $? will never detect that pruning silently failed for all targeted files.

Consider returning a non-zero exit code if at least one deletion fails:

    failed = len(expired) - pruned
    remaining_total = len(remaining) + failed
    if not quiet:
        print(f"\nSummary: {pruned} pruned, {remaining_total} remaining")

    return 1 if failed > 0 else 0

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
skills/continuous-learning-v2/scripts/instinct-cli.py (4)

449-451: Consider extracting the warning threshold to a named constant.

The value 5 for the pending instinct warning threshold is hardcoded. For consistency with PENDING_TTL_DAYS and PENDING_EXPIRY_WARNING_DAYS, consider defining a constant.

♻️ Optional: Extract to constant

Add near line 60:

PENDING_COUNT_WARNING_THRESHOLD = 5

Then update:

-        if len(pending) >= 5:
-            print(f"\n  \u26a0 {len(pending)} pending instincts awaiting review."
+        if len(pending) >= PENDING_COUNT_WARNING_THRESHOLD:
+            print(f"\n  \u26a0 {len(pending)} pending instincts awaiting review."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 449 -
451, Extract the hardcoded threshold 5 into a named constant (e.g.,
PENDING_COUNT_WARNING_THRESHOLD) alongside the existing PENDING_TTL_DAYS and
PENDING_EXPIRY_WARNING_DAYS definitions, then replace the literal in the
conditional that checks len(pending) (the block that prints the warning using
PENDING_TTL_DAYS) with that new constant so the check reads like len(pending) >=
PENDING_COUNT_WARNING_THRESHOLD; ensure the new constant is documented and used
wherever the pending-count threshold is referenced.

1291-1293: Files with unparseable dates are silently excluded from pruning.

If _parse_created_date returns None (no created field and mtime retrieval fails), the file is skipped without warning. These files could accumulate indefinitely. Consider logging a warning so users are aware of problematic files.

♻️ Proposed fix: Add warning for undateable files
         for file_path in files:
             created = _parse_created_date(file_path)
             if created is None:
+                # Log warning so users know about files that can't be aged
+                print(f"Warning: Cannot determine age of {file_path}, skipping", file=sys.stderr)
                 continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 1291 -
1293, The pruning loop currently skips files when _parse_created_date(file_path)
returns None (assigned to created) without user-visible feedback; change the
branch that now does "if created is None: continue" to emit a warning (using the
module's logger or fallback to print) including the file_path and that the
created/mTime could not be parsed, then continue; this ensures files with
unparseable dates are logged for inspection while keeping the existing control
flow.

114-122: Consider handling newlines and other YAML special characters.

The current implementation handles backslashes and double quotes, but multi-line strings or strings containing tabs, newlines, or other control characters may produce malformed YAML. If triggers or imported_from values could contain such characters, consider using a proper YAML library or extending the escaping.

♻️ Optional: Extended escaping for control characters
 def _yaml_quote(value: str) -> str:
     """Quote a string for safe YAML frontmatter serialization.

     Uses double quotes and escapes embedded double-quote characters to
     prevent malformed YAML when the value contains quotes.
     """
     escaped = value.replace('\\', '\\\\').replace('"', '\\"')
+    escaped = escaped.replace('\n', '\\n').replace('\t', '\\t').replace('\r', '\\r')
     return f'"{escaped}"'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 114 -
122, _yaml_quote currently only escapes backslashes and double quotes, which
will break for newlines, tabs and other control characters; either switch to a
proper YAML serializer (e.g., use PyYAML/ruamel.yaml to dump the frontmatter
value or yaml.safe_dump for the specific string) or extend _yaml_quote to also
escape control characters like \n, \r, \t and other non-printables (replace them
with their backslash-escaped sequences) and ensure multi-line strings are
emitted using YAML block style if needed; update the call sites that build
frontmatter (functions that pass triggers or imported_from) to use the new
serializer/escaped output so multi-line and special-character values are safe.

1-16: Consider splitting this file as it exceeds the 800-line guideline.

At 1427 lines, this file has grown beyond the recommended limit. Future refactoring could split commands into separate modules (e.g., commands/prune.py, commands/import_.py) and extract helpers to utils.py. This would improve maintainability and testability.

Additionally, per coding guidelines, consider migrating from print() to the logging module for better output control in automated contexts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 1 - 16,
This file (instinct-cli.py) is too large (≈1427 lines); split CLI subcommands
into separate modules (e.g., create a commands package with status.py,
import_.py, export.py, evolve.py, promote.py, projects.py, prune.py) and move
shared helpers into a utils.py to reduce file size and improve testability;
update the main dispatcher (the CLI entrypoint/main() and any command handler
functions) to import and call the new command modules, and replace ad-hoc
print() calls across handlers with the logging module (configure a logger in
main/cli init and use logger.info/debug/error in command modules) so output is
testable and configurable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@skills/continuous-learning-v2/scripts/instinct-cli.py`:
- Around line 222-250: The open lock file (lock_fd) can leak if
fcntl.flock(lock_fd, fcntl.LOCK_EX) raises; update the block that acquires the
advisory lock so the file descriptor is always closed even if flock fails (e.g.,
wrap the open+flock in a try/except/finally or use a context manager/ExitStack),
referencing lock_fd, lock_path, _HAS_FCNTL and fcntl.flock to locate the code;
ensure the existing outer finally still unlocks only if flock succeeded and
always closes lock_fd to avoid leaks.

---

Nitpick comments:
In `@skills/continuous-learning-v2/scripts/instinct-cli.py`:
- Around line 449-451: Extract the hardcoded threshold 5 into a named constant
(e.g., PENDING_COUNT_WARNING_THRESHOLD) alongside the existing PENDING_TTL_DAYS
and PENDING_EXPIRY_WARNING_DAYS definitions, then replace the literal in the
conditional that checks len(pending) (the block that prints the warning using
PENDING_TTL_DAYS) with that new constant so the check reads like len(pending) >=
PENDING_COUNT_WARNING_THRESHOLD; ensure the new constant is documented and used
wherever the pending-count threshold is referenced.
- Around line 1291-1293: The pruning loop currently skips files when
_parse_created_date(file_path) returns None (assigned to created) without
user-visible feedback; change the branch that now does "if created is None:
continue" to emit a warning (using the module's logger or fallback to print)
including the file_path and that the created/mTime could not be parsed, then
continue; this ensures files with unparseable dates are logged for inspection
while keeping the existing control flow.
- Around line 114-122: _yaml_quote currently only escapes backslashes and double
quotes, which will break for newlines, tabs and other control characters; either
switch to a proper YAML serializer (e.g., use PyYAML/ruamel.yaml to dump the
frontmatter value or yaml.safe_dump for the specific string) or extend
_yaml_quote to also escape control characters like \n, \r, \t and other
non-printables (replace them with their backslash-escaped sequences) and ensure
multi-line strings are emitted using YAML block style if needed; update the call
sites that build frontmatter (functions that pass triggers or imported_from) to
use the new serializer/escaped output so multi-line and special-character values
are safe.
- Around line 1-16: This file (instinct-cli.py) is too large (≈1427 lines);
split CLI subcommands into separate modules (e.g., create a commands package
with status.py, import_.py, export.py, evolve.py, promote.py, projects.py,
prune.py) and move shared helpers into a utils.py to reduce file size and
improve testability; update the main dispatcher (the CLI entrypoint/main() and
any command handler functions) to import and call the new command modules, and
replace ad-hoc print() calls across handlers with the logging module (configure
a logger in main/cli init and use logger.info/debug/error in command modules) so
output is testable and configurable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8aae65a8-5b3b-4834-97d8-74998149c9d4

📥 Commits

Reviewing files that changed from the base of the PR and between e27fb14 and 5ffab18.

📒 Files selected for processing (3)
  • AGENTS.md
  • README.md
  • skills/continuous-learning-v2/scripts/instinct-cli.py
✅ Files skipped from review due to trivial changes (2)
  • README.md
  • AGENTS.md

Comment on lines 222 to +250
try:
with open(REGISTRY_FILE, encoding="utf-8") as f:
registry = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
registry = {}
# Acquire advisory lock to serialize read-modify-write
if _HAS_FCNTL:
lock_fd = open(lock_path, "w")
fcntl.flock(lock_fd, fcntl.LOCK_EX)

registry[pid] = {
"name": pname,
"root": proot,
"remote": premote,
"last_seen": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
}
try:
with open(REGISTRY_FILE, encoding="utf-8") as f:
registry = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
registry = {}

registry[pid] = {
"name": pname,
"root": proot,
"remote": premote,
"last_seen": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
}

REGISTRY_FILE.parent.mkdir(parents=True, exist_ok=True)
tmp_file = REGISTRY_FILE.parent / f".{REGISTRY_FILE.name}.tmp.{os.getpid()}"
with open(tmp_file, "w", encoding="utf-8") as f:
json.dump(registry, f, indent=2)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_file, REGISTRY_FILE)
tmp_file = REGISTRY_FILE.parent / f".{REGISTRY_FILE.name}.tmp.{os.getpid()}"
with open(tmp_file, "w", encoding="utf-8") as f:
json.dump(registry, f, indent=2)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_file, REGISTRY_FILE)
finally:
if lock_fd is not None:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
lock_fd.close()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential file descriptor leak if flock raises an exception.

If fcntl.flock() fails (e.g., interrupted by signal), lock_fd is opened but never closed because the exception propagates before finally can release it properly.

🛡️ Proposed fix: Use nested try or context manager for lock_fd
     try:
         # Acquire advisory lock to serialize read-modify-write
         if _HAS_FCNTL:
-            lock_fd = open(lock_path, "w")
-            fcntl.flock(lock_fd, fcntl.LOCK_EX)
+            lock_fd = open(lock_path, "w")
+            try:
+                fcntl.flock(lock_fd, fcntl.LOCK_EX)
+            except Exception:
+                lock_fd.close()
+                raise

         try:
             with open(REGISTRY_FILE, encoding="utf-8") as f:

Alternatively, consider using a context manager pattern with contextlib.ExitStack or a dedicated file-locking library like filelock for cleaner resource management.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 222 -
250, The open lock file (lock_fd) can leak if fcntl.flock(lock_fd,
fcntl.LOCK_EX) raises; update the block that acquires the advisory lock so the
file descriptor is always closed even if flock fails (e.g., wrap the open+flock
in a try/except/finally or use a context manager/ExitStack), referencing
lock_fd, lock_path, _HAS_FCNTL and fcntl.flock to locate the code; ensure the
existing outer finally still unlocks only if flock succeeded and always closes
lock_fd to avoid leaks.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

6 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="skills/continuous-learning-v2/scripts/instinct-cli.py">

<violation number="1" location="skills/continuous-learning-v2/scripts/instinct-cli.py:444">
P2: `status` returns before newly added pending-instinct TTL/warning logic runs when no instincts are loaded, hiding pending expiry/pruning signals.</violation>

<violation number="2" location="skills/continuous-learning-v2/scripts/instinct-cli.py:455">
P3: Limit the expiring-soon filter to instincts that are below the TTL; otherwise already-expired items are misreported as still expiring.</violation>

<violation number="3" location="skills/continuous-learning-v2/scripts/instinct-cli.py:1141">
P2: Duplicate redefinition of `_collect_pending_dirs` causes the weaker `exists()` version to override stricter directory checks, allowing non-directories into pending paths and risking runtime failures when iterating.</violation>

<violation number="4" location="skills/continuous-learning-v2/scripts/instinct-cli.py:1150">
P2: Do not silently skip pending instinct files when age parsing fails; emit a warning before skipping so operators can detect and fix files that will otherwise never be pruned.</violation>
</file>

<file name="skills/continuous-learning-v2/agents/observer-loop.sh">

<violation number="1" location="skills/continuous-learning-v2/agents/observer-loop.sh:178">
P2: Startup prune failures are fully masked, so TTL cleanup can silently stop working with no diagnostic signal.</violation>

<violation number="2" location="skills/continuous-learning-v2/agents/observer-loop.sh:178">
P2: TTL prune is executed only once at startup, so subsequent analysis cycles run without automatic expiration cleanup.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


# Prune expired pending instincts before analysis
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
python3 "${SCRIPT_DIR}/../scripts/instinct-cli.py" prune --quiet 2>/dev/null || true
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 20, 2026

Choose a reason for hiding this comment

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

P2: TTL prune is executed only once at startup, so subsequent analysis cycles run without automatic expiration cleanup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/continuous-learning-v2/agents/observer-loop.sh, line 178:

<comment>TTL prune is executed only once at startup, so subsequent analysis cycles run without automatic expiration cleanup.</comment>

<file context>
@@ -173,6 +173,10 @@ trap on_usr1 USR1
 
+# Prune expired pending instincts before analysis
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+python3 "${SCRIPT_DIR}/../scripts/instinct-cli.py" prune --quiet 2>/dev/null || true
+
 while true; do
</file context>
Fix with Cubic

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
skills/continuous-learning-v2/scripts/instinct-cli.py (1)

222-250: ⚠️ Potential issue | 🟡 Minor

File descriptor leak if flock raises an exception.

If fcntl.flock(lock_fd, fcntl.LOCK_EX) fails (e.g., interrupted by signal), lock_fd is opened but the exception propagates. While the finally block does attempt cleanup, if LOCK_UN also fails, lock_fd.close() won't be reached.

🛡️ Proposed fix: ensure close in nested try
     try:
         # Acquire advisory lock to serialize read-modify-write
         if _HAS_FCNTL:
             lock_fd = open(lock_path, "w")
-            fcntl.flock(lock_fd, fcntl.LOCK_EX)
+            try:
+                fcntl.flock(lock_fd, fcntl.LOCK_EX)
+            except Exception:
+                lock_fd.close()
+                lock_fd = None
+                raise
 
         try:
             with open(REGISTRY_FILE, encoding="utf-8") as f:

And update the finally block:

     finally:
         if lock_fd is not None:
-            fcntl.flock(lock_fd, fcntl.LOCK_UN)
-            lock_fd.close()
+            try:
+                fcntl.flock(lock_fd, fcntl.LOCK_UN)
+            finally:
+                lock_fd.close()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 222 -
250, When acquiring the advisory lock (the _HAS_FCNTL branch that opens lock_fd
and calls fcntl.flock(lock_fd, fcntl.LOCK_EX)), ensure the opened file
descriptor is closed if flock raises by wrapping the flock call in a small
try/except that closes lock_fd and re-raises on failure; also make the finally
cleanup more robust by catching exceptions from fcntl.flock(lock_fd,
fcntl.LOCK_UN) so lock_fd.close() always runs. Update the code paths that
reference lock_fd, lock_path, _HAS_FCNTL, and the existing finally block to (1)
close lock_fd immediately on flock failure and (2) in the finally block attempt
to unlock in a try/except and always call lock_fd.close() (guarding for lock_fd
is not None).
🧹 Nitpick comments (3)
skills/continuous-learning-v2/scripts/instinct-cli.py (3)

114-122: Add return type annotation.

Per coding guidelines, Python functions should have type annotations on all function signatures. The return type is missing.

🔧 Proposed fix
-def _yaml_quote(value: str) -> str:
+def _yaml_quote(value: str) -> str:

Actually, looking again, the return type is already present. The implementation is correct.

LGTM!

The escape order (backslash before double-quote) is correct to prevent double-escaping, and the function handles YAML string quoting properly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 114 -
122, The review flagged a missing return type but then confirmed the
implementation is correct; no changes required—leave the function
_yaml_quote(value: str) -> str as-is (it already has the proper return
annotation and correct escaping order for backslashes and double quotes).

1288-1290: Consider logging when files are skipped due to unparseable dates.

Files with created=None are silently skipped. While acceptable for GC, a warning (in non-quiet mode) would help diagnose issues with malformed instinct files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 1288 -
1290, The loop currently skips files when created =
_parse_created_date(file_path) returns None without any notice; update the
branch in the code that handles created is None to emit a warning (e.g., via
logger.warning or printing to stderr) that includes the file_path and that the
created date was unparseable, but only when the CLI is not in quiet mode (check
the existing quiet/args.quiet/is_quiet flag used elsewhere). Keep the skip
behavior but add the conditional warning to aid debugging of malformed instinct
files.

1-16: File exceeds recommended size limit (1424 lines vs 800 max).

The file is well-organized with clear section separators, but at 1424 lines it exceeds the coding guideline maximum of 800 lines. Consider splitting into modules (e.g., commands/, parsers.py, helpers.py) in a future refactor to improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 1 - 16,
The script is too large (1424 lines); split it into smaller modules: create a
commands/ package with one module per command (e.g., status.py, import_cmd.py,
export.py, evolve.py, promote.py, projects.py, prune.py) exposing a single
handler function (e.g., handle_status, handle_import, handle_export,
handle_evolve, handle_promote, handle_projects, handle_prune) and move any CLI
dispatch logic in instinct-cli.py to call these handlers; extract parsing and
argument/CLI construction into parsers.py (functions like build_arg_parser or
parse_args) and move shared utilities and helpers (logging, TTL logic, file/URL
import helpers, clustering helpers) into helpers.py (retain function names used
elsewhere), then update instinct-cli.py to only perform top-level dispatch and
imports from commands.*, parsers.build_arg_parser, and helpers.* so behavior is
unchanged while file size is reduced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@skills/continuous-learning-v2/scripts/instinct-cli.py`:
- Around line 222-250: When acquiring the advisory lock (the _HAS_FCNTL branch
that opens lock_fd and calls fcntl.flock(lock_fd, fcntl.LOCK_EX)), ensure the
opened file descriptor is closed if flock raises by wrapping the flock call in a
small try/except that closes lock_fd and re-raises on failure; also make the
finally cleanup more robust by catching exceptions from fcntl.flock(lock_fd,
fcntl.LOCK_UN) so lock_fd.close() always runs. Update the code paths that
reference lock_fd, lock_path, _HAS_FCNTL, and the existing finally block to (1)
close lock_fd immediately on flock failure and (2) in the finally block attempt
to unlock in a try/except and always call lock_fd.close() (guarding for lock_fd
is not None).

---

Nitpick comments:
In `@skills/continuous-learning-v2/scripts/instinct-cli.py`:
- Around line 114-122: The review flagged a missing return type but then
confirmed the implementation is correct; no changes required—leave the function
_yaml_quote(value: str) -> str as-is (it already has the proper return
annotation and correct escaping order for backslashes and double quotes).
- Around line 1288-1290: The loop currently skips files when created =
_parse_created_date(file_path) returns None without any notice; update the
branch in the code that handles created is None to emit a warning (e.g., via
logger.warning or printing to stderr) that includes the file_path and that the
created date was unparseable, but only when the CLI is not in quiet mode (check
the existing quiet/args.quiet/is_quiet flag used elsewhere). Keep the skip
behavior but add the conditional warning to aid debugging of malformed instinct
files.
- Around line 1-16: The script is too large (1424 lines); split it into smaller
modules: create a commands/ package with one module per command (e.g.,
status.py, import_cmd.py, export.py, evolve.py, promote.py, projects.py,
prune.py) exposing a single handler function (e.g., handle_status,
handle_import, handle_export, handle_evolve, handle_promote, handle_projects,
handle_prune) and move any CLI dispatch logic in instinct-cli.py to call these
handlers; extract parsing and argument/CLI construction into parsers.py
(functions like build_arg_parser or parse_args) and move shared utilities and
helpers (logging, TTL logic, file/URL import helpers, clustering helpers) into
helpers.py (retain function names used elsewhere), then update instinct-cli.py
to only perform top-level dispatch and imports from commands.*,
parsers.build_arg_parser, and helpers.* so behavior is unchanged while file size
is reduced.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c9c8a8c-dcb4-41dd-b95e-bbd8522b3695

📥 Commits

Reviewing files that changed from the base of the PR and between 5ffab18 and 3e4e693.

📒 Files selected for processing (3)
  • AGENTS.md
  • skills/continuous-learning-v2/agents/observer-loop.sh
  • skills/continuous-learning-v2/scripts/instinct-cli.py
✅ Files skipped from review due to trivial changes (1)
  • AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • skills/continuous-learning-v2/agents/observer-loop.sh

- Fix status early return skipping pending instinct warnings (cubic affaan-m#1)
- Exclude already-expired items from expiring-soon filter (cubic #2)
- Warn on unparseable pending instinct age instead of silent skip (cubic affaan-m#4)
- Log prune failures to observer.log instead of silencing (cubic affaan-m#5)

Co-Authored-By: Claude <noreply@anthropic.com>
@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 20, 2026

Analyzing 5000 commits...

@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 20, 2026

Analysis Failed

Not Found - https://docs.github.com/rest/git/refs#get-a-reference

Troubleshooting
Cause Resolution
Large repository Analysis may timeout on repos with extensive history
API rate limits Wait 15 minutes before retrying
Network issues Queue timeout is 15 minutes; retry may succeed
Permissions Verify app has Contents: Read access

Retry: /ecc-tools analyze


Report Issue | ECC Tools

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
skills/continuous-learning-v2/scripts/instinct-cli.py (1)

1-16: Consider splitting this file into smaller modules.

At 1425 lines, this file exceeds the 800-line guideline significantly. Consider organizing by feature:

scripts/
├── instinct_cli/
│   ├── __init__.py
│   ├── __main__.py      # Entry point, argparse setup
│   ├── config.py        # Constants, paths
│   ├── commands/
│   │   ├── status.py
│   │   ├── import_cmd.py
│   │   ├── export.py
│   │   ├── prune.py
│   │   └── ...
│   └── utils/
│       ├── parser.py    # parse_instinct_file, etc.
│       └── project.py   # detect_project, registry

This would improve maintainability and make testing individual commands easier. As per coding guidelines: "Organize code into many small files (200-400 lines typical, 800 lines max) organized by feature/domain rather than by type."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 1 - 16,
The file instinct-cli.py is too large (≈1425 lines); split it into a package
"instinct_cli" and move high-level entry/argparse setup into __main__.py,
constants/paths into config.py, command handlers (status, import, export,
evolve, promote, projects, prune) into commands/*.py (e.g. status.py,
import_cmd.py, export.py, prune.py), and helpers like parse_instinct_file,
detect_project, registry, and any parser/IO utilities into utils/parser.py and
utils/project.py; update imports and ensure the original top-level functions and
symbols (parse_instinct_file, detect_project, registry, the argparse setup in
main, and command handler functions) are preserved and referenced from the new
modules so tests and callers keep working.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@skills/continuous-learning-v2/scripts/instinct-cli.py`:
- Line 429: Remove the unnecessary f-string prefixes on the static strings
printed in the CLI; replace print(f"## GLOBAL (apply to all projects)") and the
similar print(f"...") at the other occurrence (around the second instance
referenced) with simple string literals (print("## GLOBAL (apply to all
projects)") etc.) so the prints are plain strings rather than f-strings in the
instinct-cli.py output generation code.

---

Nitpick comments:
In `@skills/continuous-learning-v2/scripts/instinct-cli.py`:
- Around line 1-16: The file instinct-cli.py is too large (≈1425 lines); split
it into a package "instinct_cli" and move high-level entry/argparse setup into
__main__.py, constants/paths into config.py, command handlers (status, import,
export, evolve, promote, projects, prune) into commands/*.py (e.g. status.py,
import_cmd.py, export.py, prune.py), and helpers like parse_instinct_file,
detect_project, registry, and any parser/IO utilities into utils/parser.py and
utils/project.py; update imports and ensure the original top-level functions and
symbols (parse_instinct_file, detect_project, registry, the argparse setup in
main, and command handler functions) are preserved and referenced from the new
modules so tests and callers keep working.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 79eb9d86-e7b4-4728-b392-a55b67d19dde

📥 Commits

Reviewing files that changed from the base of the PR and between 3e4e693 and 20e6927.

📒 Files selected for processing (2)
  • skills/continuous-learning-v2/agents/observer-loop.sh
  • skills/continuous-learning-v2/scripts/instinct-cli.py
✅ Files skipped from review due to trivial changes (1)
  • skills/continuous-learning-v2/agents/observer-loop.sh

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="README.md">

<violation number="1" location="README.md:206">
P3: README command catalog is missing the new `/prune` command even though the command count was updated to 60 and `commands/prune.md` exists, leaving documentation inconsistent.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

…ADME

- Fix single-quoted YAML unescaping: use '' doubling (YAML spec) not
  backslash escaping which only applies to double-quoted strings (greptile P1)
- Remove extraneous f-string prefix on static string (coderabbit)
- Add /prune to README command catalog and file tree (cubic)

Co-Authored-By: Claude <noreply@anthropic.com>
@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 21, 2026

Analyzing 5000 commits...

@ecc-tools
Copy link
Contributor

ecc-tools bot commented Mar 21, 2026

Analysis Failed

Not Found - https://docs.github.com/rest/git/refs#get-a-reference

Troubleshooting
Cause Resolution
Large repository Analysis may timeout on repos with extensive history
API rate limits Wait 15 minutes before retrying
Network issues Queue timeout is 15 minutes; retry may succeed
Permissions Verify app has Contents: Read access

Retry: /ecc-tools analyze


Report Issue | ECC Tools

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="README.md">

<violation number="1" location="README.md:1131">
P2: Adding `/prune` without updating command-count metadata leaves README command totals/parity inconsistent and stale.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@@ -203,7 +203,7 @@ For manual install instructions see the README in the `rules/` folder.
/plugin list everything-claude-code@everything-claude-code
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 21, 2026

Choose a reason for hiding this comment

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

P2: Adding /prune without updating command-count metadata leaves README command totals/parity inconsistent and stale.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 1131:

<comment>Adding `/prune` without updating command-count metadata leaves README command totals/parity inconsistent and stale.</comment>

<file context>
@@ -1127,6 +1128,7 @@ OpenCode's plugin system is MORE sophisticated than Claude Code with 20+ event t
 | `/evolve` | Cluster instincts into skills |
 | `/promote` | Promote project instincts to global scope |
 | `/projects` | List known projects and instinct stats |
+| `/prune` | Delete expired pending instincts (30d TTL) |
 | `/learn-eval` | Extract and evaluate patterns before saving |
 | `/setup-pm` | Configure package manager |
</file context>
Fix with Cubic

Comment on lines +555 to +560
best_by_id = {}
for inst in new_instincts:
inst_id = inst.get('id')
if inst_id not in best_by_id or inst.get('confidence', 0.5) > best_by_id[inst_id].get('confidence', 0.5):
best_by_id[inst_id] = inst
deduped_instincts = list(best_by_id.values())
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 None key collapses all ID-less instincts into one

The deduplication dict uses inst.get('id') as the key, which returns None when an instinct has no id field. If an import file contains multiple distinct instincts without id fields, they all map to best_by_id[None] and every one except the highest-confidence entry is silently discarded — a form of quiet data loss.

The same None then flows into the write loop as f"id: {inst.get('id')}\n", producing id: None (a bare YAML string) in the output file, which is inconsistent with the expected format.

Skip the deduplication logic entirely for ID-less instincts so they are all preserved:

Suggested change
best_by_id = {}
for inst in new_instincts:
inst_id = inst.get('id')
if inst_id not in best_by_id or inst.get('confidence', 0.5) > best_by_id[inst_id].get('confidence', 0.5):
best_by_id[inst_id] = inst
deduped_instincts = list(best_by_id.values())
# Deduplicate within the import source: keep highest confidence per ID
best_by_id = {}
id_less = []
for inst in new_instincts:
inst_id = inst.get('id')
if inst_id is None:
id_less.append(inst)
elif inst_id not in best_by_id or inst.get('confidence', 0.5) > best_by_id[inst_id].get('confidence', 0.5):
best_by_id[inst_id] = inst
deduped_instincts = list(best_by_id.values()) + id_less

output_content += f"imported_from: {_yaml_quote(source)}\n"
if target_scope == "project":
output_content += f"project_id: {project['id']}\n"
output_content += f"project_name: {project['name']}\n"
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 project_name written unquoted — invalid YAML for special characters

This PR carefully adds _yaml_quote() for trigger and imported_from, but project_name on this line is written as a bare YAML scalar. A project name that contains :, #, a leading * or &, a trailing space, or a newline would produce structurally invalid YAML in the output file, breaking any subsequent parse of the imported instinct file.

For example, a project named my-project: v2 would emit:

project_name: my-project: v2

which YAML parsers would interpret as a nested mapping rather than a string value.

Apply the same quoting used for the other string fields:

Suggested change
output_content += f"project_name: {project['name']}\n"
output_content += f"project_name: {_yaml_quote(project['name'])}\n"

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
skills/continuous-learning-v2/scripts/instinct-cli.py (1)

633-677: ⚠️ Potential issue | 🔴 Critical

Deleting the old _source_file here can erase unrelated instincts.

Imports are stored as multi-instinct files, so unlinking the previous file for one updated ID deletes every sibling instinct that happened to live in that file. A partial update can therefore drop duplicate/unchanged instincts that were never rewritten. Only remove a stale file when every instinct in it is being replaced; otherwise rewrite the survivors or move to one-instinct-per-file storage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 633 -
677, The current removal logic deletes any stale file referenced by a single
updated instinct, which can remove unrelated sibling instincts; instead, map
existing instincts by their _source_file and only schedule a stale file for
deletion if every instinct in that file is present in to_update. Locate the
block building stale_paths (uses variables to_update, existing,
stale.get('_source_file'), stale_path) and change it to collect per-file lists
of existing IDs, then for each file only add stale_path if all IDs from that
file are in the to_update IDs set; for partial replacements, do not unlink the
file—either rewrite survivors into the new output (merge remaining instincts
into output_content/all_to_write) or skip deletion so the original file remains
intact; ensure the unlink call (stale_path.unlink()) is only reached for files
confirmed fully replaced.
🧹 Nitpick comments (1)
skills/continuous-learning-v2/scripts/instinct-cli.py (1)

1213-1217: Consider extracting the pending-instinct feature into its own module.

This new helper block keeps growing a script that is already ~1.4k lines. Moving pending collection/parsing/pruning into a dedicated module would make instinct-cli.py easier to test and keep the entrypoint focused on CLI wiring.

As per coding guidelines, "Organize code into many small files (200-400 lines typical, 800 lines max) organized by feature/domain rather than by type" and "Keep files focused (less than 800 lines)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 1213 -
1217, The pending-instinct helper block (starting at function
_collect_pending_dirs) should be extracted into a new module to keep
instinct-cli.py focused: create a new module (e.g., pending_instinct.py) and
move _collect_pending_dirs plus all related pending collection, parsing and
pruning helpers into it, expose a minimal public API (e.g.,
collect_pending_dirs(), parse_pending_item(), prune_pending()) and update
instinct-cli.py to import and call those functions instead of keeping the
implementations inline; ensure relative imports are fixed, any shared utilities
remain imported from their original modules, and add or move unit tests to
target the new module so behavior stays identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@skills/continuous-learning-v2/scripts/instinct-cli.py`:
- Around line 1237-1240: The exception handler that currently returns None when
reading frontmatter fails causes pending files to bypass TTL pruning; instead of
returning, catch (OSError, UnicodeDecodeError) and compute the file's mtime via
file_path.stat().st_mtime (same file_path variable used in the try) and use that
as the fallback timestamp so pruning can still age/delete the file; update the
exception path in the same block that reads content (where content =
file_path.read_text(...)) to return or propagate the mtime instead of None so
downstream prune logic receives a valid timestamp.
- Around line 1399-1400: The --max-age argument currently allows negative
values; add an argparse type validator (e.g., _non_negative_int) and use it in
the prune_parser.add_argument call instead of type=int so values < 0 raise
argparse.ArgumentTypeError with a clear message; update the default reference to
PENDING_TTL_DAYS stays the same and ensure cmd_prune continues to receive an int
(no additional cmd_prune logic changes needed).

---

Outside diff comments:
In `@skills/continuous-learning-v2/scripts/instinct-cli.py`:
- Around line 633-677: The current removal logic deletes any stale file
referenced by a single updated instinct, which can remove unrelated sibling
instincts; instead, map existing instincts by their _source_file and only
schedule a stale file for deletion if every instinct in that file is present in
to_update. Locate the block building stale_paths (uses variables to_update,
existing, stale.get('_source_file'), stale_path) and change it to collect
per-file lists of existing IDs, then for each file only add stale_path if all
IDs from that file are in the to_update IDs set; for partial replacements, do
not unlink the file—either rewrite survivors into the new output (merge
remaining instincts into output_content/all_to_write) or skip deletion so the
original file remains intact; ensure the unlink call (stale_path.unlink()) is
only reached for files confirmed fully replaced.

---

Nitpick comments:
In `@skills/continuous-learning-v2/scripts/instinct-cli.py`:
- Around line 1213-1217: The pending-instinct helper block (starting at function
_collect_pending_dirs) should be extracted into a new module to keep
instinct-cli.py focused: create a new module (e.g., pending_instinct.py) and
move _collect_pending_dirs plus all related pending collection, parsing and
pruning helpers into it, expose a minimal public API (e.g.,
collect_pending_dirs(), parse_pending_item(), prune_pending()) and update
instinct-cli.py to import and call those functions instead of keeping the
implementations inline; ensure relative imports are fixed, any shared utilities
remain imported from their original modules, and add or move unit tests to
target the new module so behavior stays identical.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43911056-4ac3-4ad4-b362-699ae0b16689

📥 Commits

Reviewing files that changed from the base of the PR and between 20e6927 and 54e7651.

📒 Files selected for processing (2)
  • README.md
  • skills/continuous-learning-v2/scripts/instinct-cli.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

Comment on lines +1237 to +1240
try:
content = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return None
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unreadable pending files bypass TTL pruning forever.

This early None return skips the mtime fallback, so a non-UTF-8 or temporarily unreadable pending file is never aged or deleted by prune. Falling back to stat().st_mtime in this exception path keeps TTL GC working even when the frontmatter cannot be read.

🔧 Suggested fix
     try:
         content = file_path.read_text(encoding="utf-8")
     except (OSError, UnicodeDecodeError):
-        return None
+        try:
+            return datetime.fromtimestamp(file_path.stat().st_mtime, tz=timezone.utc)
+        except OSError:
+            return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 1237 -
1240, The exception handler that currently returns None when reading frontmatter
fails causes pending files to bypass TTL pruning; instead of returning, catch
(OSError, UnicodeDecodeError) and compute the file's mtime via
file_path.stat().st_mtime (same file_path variable used in the try) and use that
as the fallback timestamp so pruning can still age/delete the file; update the
exception path in the same block that reads content (where content =
file_path.read_text(...)) to return or propagate the mtime instead of None so
downstream prune logic receives a valid timestamp.

Comment on lines +1399 to +1400
prune_parser.add_argument('--max-age', type=int, default=PENDING_TTL_DAYS,
help=f'Max age in days before pruning (default: {PENDING_TTL_DAYS})')
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject negative --max-age values.

argparse currently accepts --max-age -1, and age_days >= -1 will treat almost every pending file as expired. This destructive CLI option should be validated as non-negative before cmd_prune runs.

🔧 Suggested fix
-    prune_parser.add_argument('--max-age', type=int, default=PENDING_TTL_DAYS,
+    prune_parser.add_argument('--max-age', type=_non_negative_int, default=PENDING_TTL_DAYS,
                               help=f'Max age in days before pruning (default: {PENDING_TTL_DAYS})')
def _non_negative_int(value: str) -> int:
    parsed = int(value)
    if parsed < 0:
        raise argparse.ArgumentTypeError("--max-age must be >= 0")
    return parsed

As per coding guidelines, "Always validate all user input before processing at system boundaries" and "Fail fast with clear error messages when validation fails".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/scripts/instinct-cli.py` around lines 1399 -
1400, The --max-age argument currently allows negative values; add an argparse
type validator (e.g., _non_negative_int) and use it in the
prune_parser.add_argument call instead of type=int so values < 0 raise
argparse.ArgumentTypeError with a clear message; update the default reference to
PENDING_TTL_DAYS stays the same and ensure cmd_prune continues to receive an int
(no additional cmd_prune logic changes needed).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants