Skip to content

feat(mayfly): add depth-tiered research-campaign notebook - #247

Open
marcpickett1 wants to merge 2 commits into
mainfrom
marc/mayfly-notebook
Open

feat(mayfly): add depth-tiered research-campaign notebook#247
marcpickett1 wants to merge 2 commits into
mainfrom
marc/mayfly-notebook

Conversation

@marcpickett1

@marcpickett1 marcpickett1 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a new gpd-mayfly MCP server (8th built-in) managing a depth-tiered knowledge graph at GPD/mayfly/: FRONTIER → MAP → topics → epochs → raw sessions. Agents can read/write the notebook via standard MCP tool calls.
  • Adds a Stop hook (mayfly_capture.py) that fires after every Claude response, detects when GPD commands ran via the lineage ledger, and appends a raw capture record to session-log.jsonl — no LLM call, pure Python.
  • Adds <mayfly_maintenance> completion duties to execute-phase, research-phase, and discuss-phase so agents automatically update FRONTIER + knowledge graph as part of finishing work.
  • Wires everything into gpd install (builtin_servers, entry point, hook registration, uninstall cleanup).
  • Adds SKILL.md — the portable Mayfly v2 protocol spec (harness-agnostic reference).

How it runs automatically

After gpd install re-runs, Claude Code gets the gpd-mayfly MCP server in .mcp.json and the Stop hook in settings.json. No user action needed:

  1. First session on a project: Stop hook bootstraps GPD/mayfly/ structure silently
  2. During execute/research/discuss: agent calls upsert_knowledge + update_frontier + append_journal_row as part of command teardown
  3. Every session with GPD commands: Stop hook writes a capture record linking the full transcript path

Notebook structure

GPD/mayfly/
├── FRONTIER.md           ← current knowledge state (~1 page, read by agent first)
├── knowledge/
│   ├── MAP.md            ← topic navigation index
│   └── <topic>.md        ← per-topic synthesis with provenance links
├── epochs/               ← compressed session history (Summarizer writes)
├── sessions/             ← per-session raw notes (deepest corner)
├── JOURNAL.md            ← append-only session log
└── session-log.jsonl     ← raw automatic Stop-hook capture buffer

Separate from the existing GPD/knowledge/ system (K-* YAML format). Can be merged later if the formats converge.

Test plan

  • mayfly_server and mayfly_capture import cleanly
  • gpd-mayfly appears in build_mcp_servers_dict() output
  • claude_code.py imports ensure_mayfly_stop_hook without error
  • Full test suite: zero new failures vs main (pre-existing failures from unrelated gpd-result-solver agent issue)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced Mayfly: a research notebook system for depth-tiered knowledge management and session tracking
    • Added gpd-mcp-mayfly CLI command to access notebook tools
    • Integrated automatic notebook maintenance into discussion, research, and project closeout phases
  • Documentation

    • Added comprehensive Mayfly research protocol documentation

Integrates the Mayfly v2 protocol as an automatic background system that
tracks all research context without requiring explicit user commands.

## Components

**`gpd-mayfly` MCP server** (`src/gpd/mcp/servers/mayfly_server.py`)
8th built-in server, auto-registered by `gpd install`. Manages the
notebook at `GPD/mayfly/` with a depth-tiered hierarchy:
  FRONTIER.md (surface) → knowledge/ (topics) → epochs/ → sessions/ (raw)
Tools: bootstrap_mayfly, read_frontier, update_frontier, read_map,
update_map, upsert_knowledge, read_knowledge, append_journal_row,
write_session_notes, write_epoch_summary, search_notebook, read_session_log

**Stop hook** (`src/gpd/hooks/mayfly_capture.py`)
Pure Python, no LLM. Fires after each Claude response. When GPD commands
have run since the last capture (detected via execution-lineage.jsonl),
appends a structured record to `GPD/mayfly/session-log.jsonl` containing
the session ID, commands run, and transcript path. Also bootstraps the
notebook directory structure on first use.

**Command maintenance sections**
`execute-phase`, `research-phase`, and `discuss-phase` now include a
`<mayfly_maintenance>` completion duty instructing the agent to call
upsert_knowledge + update_frontier + append_journal_row before returning.
Fails silently when gpd-mayfly is unavailable.

**Infrastructure**
- `MAYFLY_DIR_NAME` constant + 8 `ProjectLayout` properties in constants.py
- `ensure_mayfly_stop_hook()` in install_utils.py (same idempotent upsert
  pattern as ensure_update_hook, with uninstall cleanup)
- `gpd-mcp-mayfly` entry point in pyproject.toml
- SKILL.md documents the Mayfly v2 protocol (portable, harness-agnostic)

## What runs automatically

After `gpd install`: Stop hook and MCP server are wired into Claude Code.
On first session: notebook bootstrapped at GPD/mayfly/.
During execute/research/discuss: agent updates knowledge graph as part of
command completion (no user action required).
After every session with GPD commands: Stop hook writes a raw capture
record linking the transcript — even if agent maintenance was skipped.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Jun 4, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ cmaloney111
❌ marcpickett1
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f34eddf6-f9ec-4e01-aa3c-d0a019010a68

📥 Commits

Reviewing files that changed from the base of the PR and between dc034d6 and 139bcf2.

📒 Files selected for processing (8)
  • infra/gpd-mayfly.json
  • src/gpd/hooks/mayfly_capture.py
  • src/gpd/mcp/builtin_servers.py
  • src/gpd/mcp/servers/mayfly_server.py
  • src/gpd/specs/workflows/execute-phase/closeout.md
  • src/gpd/specs/workflows/research-phase/research-handoff.md
  • tests/README.md
  • tests/repo_graph_contract.json
✅ Files skipped from review due to trivial changes (3)
  • infra/gpd-mayfly.json
  • tests/repo_graph_contract.json
  • tests/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/gpd/mcp/builtin_servers.py
  • src/gpd/hooks/mayfly_capture.py
  • src/gpd/mcp/servers/mayfly_server.py

📝 Walkthrough

Walkthrough

This pull request introduces the complete Mayfly v2 research protocol: a fresh-context, disk-backed notebook system for structured research campaigns. Changes include specification documentation, MCP server for notebook management, session capture hook, hook wiring, command workflow integrations, and supporting infrastructure.

Changes

Mayfly Research Protocol and Integration

Layer / File(s) Summary
Protocol Specification and Project Layout
SKILL.md, src/gpd/core/constants.py
SKILL.md defines the Mayfly v2 protocol with step-based roles (PI, Researcher, Summarizer), notebook tier structure (frontier/knowledge/journal/sessions/epochs), read/write tool interfaces, campaign loop, role-specific prompts, and bootstrap schemas; constants.py adds MAYFLY_DIR_NAME and ProjectLayout path properties for the notebook directory tree.
Mayfly MCP Server Implementation
src/gpd/mcp/servers/mayfly_server.py
Implements the complete Mayfly notebook server with bootstrap tool, read tools across all tiers (frontier, map, knowledge, journal, sessions, epochs, session-log, search), and write tools (update/upsert frontier/map/knowledge, write session notes, append journal rows, write epoch summaries); all tools validate inputs, create directories atomically, and use stub fallback content.
MCP Server Registration and Configuration
src/gpd/mcp/builtin_servers.py, infra/gpd-mayfly.json
builtin_servers.py registers gpd-mayfly in _BUILTIN_SERVERS with Python -m invocation and metadata including capabilities, registry prefix, and schema health check; infra/gpd-mayfly.json defines the complete MCP transport configuration for CLI invocation.
Session Capture Hook Implementation
src/gpd/hooks/mayfly_capture.py
Implements a Claude Code stop hook that parses JSON payloads, locates the GPD project root, conditionally bootstraps Mayfly directories when GPD is initialized, reads the last capture timestamp from session-log.jsonl, scans execution-lineage.jsonl for recent commands, and appends a new session record when commands are detected; all operations are best-effort with graceful fallbacks.
Hook Installation and Stop Hook Management
src/gpd/adapters/claude_code.py, src/gpd/adapters/install_utils.py
claude_code.py wires mayfly_capture hook installation and registers its stop hook via ensure_mayfly_stop_hook; install_utils.py adds SettingsCleanupResult.removed_stop_hooks tracking, HOOK_SCRIPTS mapping for mayfly_capture, remove_stop_managed_hooks helper, and ensure_mayfly_stop_hook idempotent upsert to manage Claude settings.json stop hooks.
Command Phase Workflow Integration
src/gpd/commands/discuss-phase.md, src/gpd/specs/workflows/execute-phase/closeout.md, src/gpd/specs/workflows/research-phase/research-handoff.md
Each command phase adds post-phase mayfly_notebook_maintenance steps that invoke Mayfly tools to capture findings, update frontier/knowledge/journal, and maintain the research notebook; all integrations gracefully no-op when Mayfly tools are unavailable.
CLI Entry Point and Test Updates
pyproject.toml, tests/README.md, tests/repo_graph_contract.json
pyproject.toml registers gpd-mcp-mayfly console script; test files update inventory counts to reflect new modules.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as gpd-mcp-mayfly
    participant Server as mayfly_server.py
    participant Notebook as GPD/mayfly/
    
    CLI->>Server: bootstrap_mayfly(project_dir)
    Server->>Notebook: Create directory tree
    Server->>Notebook: Seed FRONTIER.md, MAP.md, JOURNAL.md
    
    CLI->>Server: read_frontier(project_dir)
    Notebook-->>Server: Return frontier content
    Server-->>CLI: frontier data
    
    CLI->>Server: upsert_knowledge(project_dir, topic, content)
    Server->>Notebook: Write knowledge/topic.md atomically
    Server-->>CLI: created_now boolean
    
    CLI->>Server: append_journal_row(project_dir, step, outcome, summary, ...)
    Server->>Notebook: Update or append JOURNAL.md row
    Server-->>CLI: success
Loading
sequenceDiagram
    participant Claude as Claude Code
    participant Hook as mayfly_capture.py
    participant Lineage as execution-lineage.jsonl
    participant SessionLog as session-log.jsonl
    
    Claude->>Hook: Call stop hook with JSON payload
    Hook->>Hook: Parse stdin JSON
    Hook->>Hook: Find GPD project root
    Hook->>Notebook: Check if mayfly/ exists
    alt Mayfly missing and GPD initialized
        Hook->>Hook: Bootstrap directories and files
    end
    Hook->>SessionLog: Read last capture timestamp
    Hook->>Lineage: Scan for commands since timestamp
    Lineage-->>Hook: Recent command entries
    Hook->>SessionLog: Append new session record
    Hook-->>Claude: Return 0 (success or silent skip)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • SergioHC95

Poem

🐰 A Mayfly lands in the lab notebook's nest,

Fresh context blooms across frontier, knowledge, and jest—

Tiers tier up the research, sessions captured true,

MCP tools dance with Claude, protocol v2! 📚✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(mayfly): add depth-tiered research-campaign notebook' clearly and specifically summarizes the main change—introduction of a new Mayfly notebook system with depth-tiered structure for research campaigns.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering what changed, why, and testing done. It includes summary, implementation details, notebook structure, and test verification.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch marc/mayfly-notebook

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.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
src/gpd/core/constants.py (1)

561-591: ⚡ Quick win

Add missing path helper for POST_IT.md.

POST_IT.md is a core protocol file referenced throughout SKILL.md (lines 106, 237, 397, 436-437, 532, 707, 718, 954) and used by every PI role invocation, but there's no mayfly_post_it path helper. This makes it harder for downstream code to consistently reference this file.

➕ Proposed addition
     `@property`
     def mayfly_session_log(self) -> Path:
         return self.gpd / MAYFLY_DIR_NAME / "session-log.jsonl"
+
+    `@property`
+    def mayfly_post_it(self) -> Path:
+        return self.gpd / MAYFLY_DIR_NAME / "POST_IT.md"

     `@property`
     def research_map_dir(self) -> Path:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gpd/core/constants.py` around lines 561 - 591, Add a new Path helper
property named mayfly_post_it on the same object that defines
mayfly_dir/mayfly_knowledge_dir/etc.; implement it to return the canonical path
to the POST_IT.md file (e.g., return self.gpd / MAYFLY_DIR_NAME / "POST_IT.md"),
keeping naming and style consistent with existing properties like mayfly_journal
and mayfly_map so downstream code can reference self.mayfly_post_it.
src/gpd/hooks/mayfly_capture.py (2)

44-63: 💤 Low value

Consider optimizing for large session logs in the future.

The function reads the entire session-log.jsonl file to find the most recent timestamp. For long-running campaigns with many sessions, this could become slow. While the current best-effort design is acceptable, consider optimizing if performance degrades (e.g., maintaining a separate timestamp file or seeking from the end).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gpd/hooks/mayfly_capture.py` around lines 44 - 63, The current
_read_last_capture_ts(session_log) implementation reads the entire file into
memory which can be slow for very large session_log files; update it to avoid
full-file reads by either (A) reading from the end: open session_log in binary,
seek from the end and scan backwards to find the last newline(s) and parse only
the last JSONL line(s) until a valid "ts" is found, or (B) maintain and read a
lightweight companion file (e.g., session_log.with_suffix('.last_ts')) that
stores the most recent timestamp and update that file whenever you append
captures; implement one of these strategies in _read_last_capture_ts and ensure
it still handles missing files and JSON errors like the current function.

97-105: 💤 Low value

Note the limitation: no file locking for concurrent appends.

The function appends to session-log.jsonl without file locking. If multiple hook invocations run concurrently, writes could interleave. Given the best-effort design and the low likelihood of concurrent Stop hooks, this is acceptable. However, if concurrent sessions become common, consider adding file locking or buffering.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gpd/hooks/mayfly_capture.py` around lines 97 - 105, The
_append_session_log function appends JSON lines without file locking, risking
interleaved writes under concurrent hook runs; update _append_session_log to
acquire an exclusive file lock around the open/write (use a cross-platform
locking lib such as fcntl/portalocker/filelock) for session_log before writing
the JSONL line, fall back to the current best-effort behavior on lock
acquisition failure, and keep the OSError handling so this remains non-fatal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@SKILL.md`:
- Around line 104-116: The spec's directory name "attempts/" conflicts with the
code which uses "sessions/" (see constants.py mayfly_sessions_dir and the
hook/server usage), so update SKILL.md to match implementation: replace every
"attempts" reference (e.g., list_attempts, read_attempt, write_attempt_notes and
other occurrence points) with "sessions" and adjust related endpoint/command
names accordingly, and add the missing session-log.jsonl entry to the layout
diagram so the documented layout matches constants.py and the hook-managed
capture log.

In `@src/gpd/mcp/servers/mayfly_server.py`:
- Around line 80-86: _sanitize_epoch_name currently strips suffix with
name.strip().rstrip(".md") and enforces exactly 3 digits per side
(re.fullmatch(r"\d{3}-\d{3}", ...)), which rejects epoch names produced by
write_epoch_summary when values >=1000; change _sanitize_epoch_name to (1)
remove the literal ".md" suffix safely (use str.removesuffix(".md") or an
endswith check and slice) instead of rstrip(".md"), and (2) relax the regex to
allow three-or-more digits on each side (e.g. re.fullmatch(r"\d{3,}-\d{3,}",
stripped) or simply re.fullmatch(r"\d+-\d+", stripped)) so it accepts names
generated by write_epoch_summary; update _sanitize_epoch_name accordingly and
ensure write_epoch_summary remains compatible.
- Around line 431-436: Calculate the total match count before you truncate the
results: capture total_count = len(results) prior to the truncation block (using
the variables shown: results, _SEARCH_MAX_LINES, truncated), then perform the
slicing and append the truncation note but return stable_mcp_response with
"count": total_count (or alternatively add a separate "message" field for the
truncation note instead of appending it to results) so the count reflects actual
matches not the informational line.
- Around line 581-582: The _clean function currently only escapes pipe
characters but must also sanitize newlines so Markdown table rows don't break;
update def _clean(s: str) to replace newline and carriage-return characters with
a single space (e.g., s = s.replace("\r", " ").replace("\n", " ")), then perform
the existing pipe escape and strip, and ensure callers that pass outcome,
summary, knowledge_updated, files, metric continue to use _clean so all row
fields are normalized.
- Around line 170-177: The seeded JOURNAL.md table is missing the metric column
which causes malformed rows from append_journal_row; update the atomic_write
call that writes layout.mayfly_journal to include a metric header (e.g., add "|
metric |" to the header row and adjust the separator row to six columns) and
ensure append_journal_row (the function that formats journal lines) always emits
six columns (use an empty string for metric when not provided) so the table
structure is consistent.

---

Nitpick comments:
In `@src/gpd/core/constants.py`:
- Around line 561-591: Add a new Path helper property named mayfly_post_it on
the same object that defines mayfly_dir/mayfly_knowledge_dir/etc.; implement it
to return the canonical path to the POST_IT.md file (e.g., return self.gpd /
MAYFLY_DIR_NAME / "POST_IT.md"), keeping naming and style consistent with
existing properties like mayfly_journal and mayfly_map so downstream code can
reference self.mayfly_post_it.

In `@src/gpd/hooks/mayfly_capture.py`:
- Around line 44-63: The current _read_last_capture_ts(session_log)
implementation reads the entire file into memory which can be slow for very
large session_log files; update it to avoid full-file reads by either (A)
reading from the end: open session_log in binary, seek from the end and scan
backwards to find the last newline(s) and parse only the last JSONL line(s)
until a valid "ts" is found, or (B) maintain and read a lightweight companion
file (e.g., session_log.with_suffix('.last_ts')) that stores the most recent
timestamp and update that file whenever you append captures; implement one of
these strategies in _read_last_capture_ts and ensure it still handles missing
files and JSON errors like the current function.
- Around line 97-105: The _append_session_log function appends JSON lines
without file locking, risking interleaved writes under concurrent hook runs;
update _append_session_log to acquire an exclusive file lock around the
open/write (use a cross-platform locking lib such as fcntl/portalocker/filelock)
for session_log before writing the JSONL line, fall back to the current
best-effort behavior on lock acquisition failure, and keep the OSError handling
so this remains non-fatal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1420dd0e-4b42-4f2c-8586-ce3c662942ac

📥 Commits

Reviewing files that changed from the base of the PR and between 0f41769 and dc034d6.

📒 Files selected for processing (11)
  • SKILL.md
  • pyproject.toml
  • src/gpd/adapters/claude_code.py
  • src/gpd/adapters/install_utils.py
  • src/gpd/commands/discuss-phase.md
  • src/gpd/commands/execute-phase.md
  • src/gpd/commands/research-phase.md
  • src/gpd/core/constants.py
  • src/gpd/hooks/mayfly_capture.py
  • src/gpd/mcp/builtin_servers.py
  • src/gpd/mcp/servers/mayfly_server.py

Comment thread SKILL.md
Comment on lines +104 to +116
```
campaign/
├── POST_IT.md # active PI brief (overwritten each step)
├── FRONTIER.md # current knowledge state (researcher updates each step)
├── knowledge/
│ ├── MAP.md # navigable index: topics + epoch list + open threads
│ └── <topic-slug>.md # per-topic synthesis with links to supporting attempts
├── epochs/
│ └── <start>-<end>.md # compressed summary of K attempts (Summarizer writes)
├── JOURNAL.md # append-only one-row-per-attempt log (archive at scale)
└── attempts/
└── <NNN>.md # full per-attempt notes (deepest corner)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: Directory naming mismatch between spec and implementation.

Line 114 specifies the directory as attempts/, but the implementation consistently uses sessions/ (see constants.py line 570-571, mayfly_capture.py context snippet line 113, and mayfly_server.py bootstrap). This naming inconsistency propagates through the entire document (50+ references to "attempts", "list_attempts", "read_attempt", "write_attempt_notes", etc.) and will cause confusion when porting the protocol.

Additionally, session-log.jsonl is missing from this layout diagram but exists in the implementation (constants.py line 590-591) and is mentioned in the PR description as a hook-managed capture log.

📋 Recommended fixes

Option 1 (align spec to implementation): Replace all references to "attempts/" with "sessions/" throughout this document (lines 84, 114-115, 125, 328, 403, 405-406, 450-451, 567, 721, 736, 746, 763-764, 824, 848, 861).

Option 2 (align implementation to spec): Rename mayfly_sessions_dir to mayfly_attempts_dir in constants.py and update all downstream code (bootstrap, MCP server, hook) to use "attempts/" instead of "sessions/".

Additionally, add session-log.jsonl to the layout diagram:

 campaign/
 ├── POST_IT.md              # active PI brief (overwritten each step)
 ├── FRONTIER.md             # current knowledge state (researcher updates each step)
+├── session-log.jsonl       # hook-managed capture records (one per session)
 ├── knowledge/
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 104-104: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SKILL.md` around lines 104 - 116, The spec's directory name "attempts/"
conflicts with the code which uses "sessions/" (see constants.py
mayfly_sessions_dir and the hook/server usage), so update SKILL.md to match
implementation: replace every "attempts" reference (e.g., list_attempts,
read_attempt, write_attempt_notes and other occurrence points) with "sessions"
and adjust related endpoint/command names accordingly, and add the missing
session-log.jsonl entry to the layout diagram so the documented layout matches
constants.py and the hook-managed capture log.

Comment on lines +80 to +86
def _sanitize_epoch_name(name: str) -> str | None:
"""Return a safe filename for an epoch, or None if invalid."""
# Expect format: NNN-NNN (e.g. "000-019")
stripped = name.strip().rstrip(".md")
if not re.fullmatch(r"\d{3}-\d{3}", stripped):
return None
return stripped

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Epoch name validation rejects values >= 1000 written by write_epoch_summary.

The regex \d{3}-\d{3} requires exactly 3 digits on each side, but write_epoch_summary (line 637) uses {start:03d}-{end:03d} which produces 4+ digits for values >= 1000 (e.g., "1000-1019.md"). This means epochs written with start or end >= 1000 cannot be read back with read_epoch.

🔧 Proposed fix
 def _sanitize_epoch_name(name: str) -> str | None:
     """Return a safe filename for an epoch, or None if invalid."""
-    # Expect format: NNN-NNN (e.g. "000-019")
+    # Expect format: NNN-NNN or more digits (e.g. "000-019", "1000-1099")
     stripped = name.strip().rstrip(".md")
-    if not re.fullmatch(r"\d{3}-\d{3}", stripped):
+    if not re.fullmatch(r"\d{3,}-\d{3,}", stripped):
         return None
     return stripped
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _sanitize_epoch_name(name: str) -> str | None:
"""Return a safe filename for an epoch, or None if invalid."""
# Expect format: NNN-NNN (e.g. "000-019")
stripped = name.strip().rstrip(".md")
if not re.fullmatch(r"\d{3}-\d{3}", stripped):
return None
return stripped
def _sanitize_epoch_name(name: str) -> str | None:
"""Return a safe filename for an epoch, or None if invalid."""
# Expect format: NNN-NNN or more digits (e.g. "000-019", "1000-1099")
stripped = name.strip().rstrip(".md")
if not re.fullmatch(r"\d{3,}-\d{3,}", stripped):
return None
return stripped
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gpd/mcp/servers/mayfly_server.py` around lines 80 - 86,
_sanitize_epoch_name currently strips suffix with name.strip().rstrip(".md") and
enforces exactly 3 digits per side (re.fullmatch(r"\d{3}-\d{3}", ...)), which
rejects epoch names produced by write_epoch_summary when values >=1000; change
_sanitize_epoch_name to (1) remove the literal ".md" suffix safely (use
str.removesuffix(".md") or an endswith check and slice) instead of
rstrip(".md"), and (2) relax the regex to allow three-or-more digits on each
side (e.g. re.fullmatch(r"\d{3,}-\d{3,}", stripped) or simply
re.fullmatch(r"\d+-\d+", stripped)) so it accepts names generated by
write_epoch_summary; update _sanitize_epoch_name accordingly and ensure
write_epoch_summary remains compatible.

Comment on lines +170 to +177
# Seed JOURNAL.md
atomic_write(
layout.mayfly_journal,
"# Mayfly Research Journal\n\n"
"Append-only log of research sessions on this campaign. One row per session.\n\n"
"| step | outcome | summary | knowledge_updated | files |\n"
"|---:|---|---|---|---|\n",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Journal table header missing metric column, breaking structure when append_journal_row includes metrics.

The bootstrap seeds a 5-column table (step, outcome, summary, knowledge_updated, files), but append_journal_row (lines 584-587) conditionally produces:

  • 6 columns when metric is provided: | step | metric | outcome | summary | knowledge_updated | files |
  • 5 columns when metric is empty: | step | outcome | summary | knowledge_updated | files |

This creates an inconsistent markdown table that renders incorrectly.

🔧 Proposed fix: Always include metric column

Add a metric column header in the bootstrap table and always include it in append_journal_row (use empty string if not provided):

     atomic_write(
         layout.mayfly_journal,
         "# Mayfly Research Journal\n\n"
         "Append-only log of research sessions on this campaign. One row per session.\n\n"
-        "| step | outcome | summary | knowledge_updated | files |\n"
-        "|---:|---|---|---|---|\n",
+        "| step | metric | outcome | summary | knowledge_updated | files |\n"
+        "|---:|---|---|---|---|---|\n",
     )

Then in append_journal_row (lines 584-587), always use the 6-column format:

-    if metric:
-        new_row = f"| {step_str} | {_clean(metric)} | {_clean(outcome)} | {_clean(summary)} | {_clean(knowledge_updated)} | {_clean(files)} |\n"
-    else:
-        new_row = f"| {step_str} | {_clean(outcome)} | {_clean(summary)} | {_clean(knowledge_updated)} | {_clean(files)} |\n"
+    new_row = f"| {step_str} | {_clean(metric)} | {_clean(outcome)} | {_clean(summary)} | {_clean(knowledge_updated)} | {_clean(files)} |\n"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gpd/mcp/servers/mayfly_server.py` around lines 170 - 177, The seeded
JOURNAL.md table is missing the metric column which causes malformed rows from
append_journal_row; update the atomic_write call that writes
layout.mayfly_journal to include a metric header (e.g., add "| metric |" to the
header row and adjust the separator row to six columns) and ensure
append_journal_row (the function that formats journal lines) always emits six
columns (use an empty string for metric when not provided) so the table
structure is consistent.

Comment on lines +431 to +436
if len(results) > _SEARCH_MAX_LINES:
truncated = len(results) - _SEARCH_MAX_LINES
results = results[:_SEARCH_MAX_LINES]
results.append(f"... {truncated} more lines truncated. Refine your query.")

return stable_mcp_response({"results": results, "count": len(results)})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Minor: count includes truncation message line, making it ambiguous.

When results are truncated (lines 432-434), the truncation message is appended to results, then count is set to len(results). This makes count include the informational message as if it's a search result. Users cannot easily distinguish between the number of actual matches and the final array length.

💡 Proposed fix: Calculate count before truncation
+    original_count = len(results)
     if len(results) > _SEARCH_MAX_LINES:
         truncated = len(results) - _SEARCH_MAX_LINES
         results = results[:_SEARCH_MAX_LINES]
         results.append(f"... {truncated} more lines truncated. Refine your query.")
 
-    return stable_mcp_response({"results": results, "count": len(results)})
+    return stable_mcp_response({"results": results, "count": original_count})

Alternatively, put the truncation message in a separate "message" field instead of appending to results.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if len(results) > _SEARCH_MAX_LINES:
truncated = len(results) - _SEARCH_MAX_LINES
results = results[:_SEARCH_MAX_LINES]
results.append(f"... {truncated} more lines truncated. Refine your query.")
return stable_mcp_response({"results": results, "count": len(results)})
original_count = len(results)
if len(results) > _SEARCH_MAX_LINES:
truncated = len(results) - _SEARCH_MAX_LINES
results = results[:_SEARCH_MAX_LINES]
results.append(f"... {truncated} more lines truncated. Refine your query.")
return stable_mcp_response({"results": results, "count": original_count})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gpd/mcp/servers/mayfly_server.py` around lines 431 - 436, Calculate the
total match count before you truncate the results: capture total_count =
len(results) prior to the truncation block (using the variables shown: results,
_SEARCH_MAX_LINES, truncated), then perform the slicing and append the
truncation note but return stable_mcp_response with "count": total_count (or
alternatively add a separate "message" field for the truncation note instead of
appending it to results) so the count reflects actual matches not the
informational line.

Comment on lines +581 to +582
def _clean(s: str) -> str:
return s.replace("|", "\\|").strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Minor: Newlines in journal row fields break markdown table structure.

The _clean function escapes pipe characters but doesn't sanitize newlines. If any of the string parameters (outcome, summary, knowledge_updated, files, metric) contain newlines, the table row will span multiple lines and break the markdown table.

🛡️ Proposed fix: Replace newlines with spaces
     def _clean(s: str) -> str:
-        return s.replace("|", "\\|").strip()
+        return s.replace("|", "\\|").replace("\n", " ").replace("\r", " ").strip()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _clean(s: str) -> str:
return s.replace("|", "\\|").strip()
def _clean(s: str) -> str:
return s.replace("|", "\\|").replace("\n", " ").replace("\r", " ").strip()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gpd/mcp/servers/mayfly_server.py` around lines 581 - 582, The _clean
function currently only escapes pipe characters but must also sanitize newlines
so Markdown table rows don't break; update def _clean(s: str) to replace newline
and carriage-return characters with a single space (e.g., s = s.replace("\r", "
").replace("\n", " ")), then perform the existing pipe escape and strip, and
ensure callers that pass outcome, summary, knowledge_updated, files, metric
continue to use _clean so all row fields are normalized.

- Sort imports in hooks/mayfly_capture.py and drop the unused
  MAYFLY_DIR_NAME import in mcp/servers/mayfly_server.py (ruff).
- Make the Mayfly capture hook docstring/comments runtime-agnostic so
  the non-adapter sources no longer hardcode a runtime name.
- Add the generated infra/gpd-mayfly.json public descriptor so the
  committed infra inventory matches build_public_descriptors().
- Regenerate the repo graph contract (hooks +1, mcp servers +1,
  infra/gpd-*.json now 9).
- Move the execute-phase and research-phase Mayfly notebook maintenance
  steps out of the thin staged command wrappers into their workflow
  closeout/handoff stages, keeping the feature while staying within the
  staged-command projection budgets.
@marcpickett1

Copy link
Copy Markdown
Collaborator Author

🤖 RoastBot: The author's GitHub display name is "Marctar The Mad." The PR title has the word "depth-tiered." These facts are related.

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.

3 participants