From 99f0f73aa9188b4b21efbac58d45b82c16a62fee Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla Date: Tue, 28 Apr 2026 12:32:10 +0200 Subject: [PATCH 1/7] Update /project commands from upstream tnf-dev-env Sync the /project:new, /project:resume, and /project:close commands with the latest versions from fonta-rh/tnf-dev-env. Key changes: - Extract project resolution logic into shared resume-project.py script - Introduce lean index pattern for project CLAUDE.md (~50-80 lines) - Add detail file templates (investigation.md, ci-runs.md, etc.) - Implement lazy repo context loading in /project:resume - Add Reference Files table as manifest for detail file discovery Co-Authored-By: Claude Opus 4.6 --- .../.claude/commands/project/close.md | 46 +-- .../.claude/commands/project/new.md | 260 ++++++++++--- .../.claude/commands/project/resume.md | 207 ++++------ .../scripts/resume-project.py | 354 ++++++++++++++++++ 4 files changed, 654 insertions(+), 213 deletions(-) create mode 100755 multi-repo-development/scripts/resume-project.py diff --git a/multi-repo-development/.claude/commands/project/close.md b/multi-repo-development/.claude/commands/project/close.md index 764f06d5..966983ad 100644 --- a/multi-repo-development/.claude/commands/project/close.md +++ b/multi-repo-development/.claude/commands/project/close.md @@ -15,41 +15,23 @@ Everything after "close" in `$ARGUMENTS` is parsed as follows: ## Step 1: Select Project -**1a. Determine project name** +**1a. Resolve project name** -Handle the argument using the same cases as `/project:resume`: +Extract the first token from `$ARGUMENTS`. Run +`scripts/resume-project.py ` via Bash (omit the token if +none was provided). Parse the JSON and handle by `status`: -**Case A — Numeric shorthand** (e.g., `/project:close 2`): -If the first token is a plain integer N, look in your conversation -context for the numbered "Recent projects" table produced by the -SessionStart hook. Pick the project name on row N from that table. -If the table is not in context, fall back to running -`scripts/recent-projects.py --names` and pick the Nth line. -If N is out of range, show an error and fall through to Case C. +- **`ok`** — use `project.name` as the target. Proceed to 1b. +- **`no_argument`** — present the first 3 `alternatives` as + AskUserQuestion options plus "See all projects". Re-run with the + chosen name. +- **`not_found`** / **`out_of_range`** — show `error_message`, present + `alternatives` as a picker, re-run with chosen name. +- **`no_projects`** — show `error_message` and stop. -**Case B — Project name** (e.g., `/project:close OCPBUGS-74679`): -If the first token is a non-numeric string, use it as the target -project name. +**1b. Check current status** -**Case C — No argument** (`/project:close`): -Look in your conversation context for the "Recent projects" table. -If present, extract project names that have a non-done status and -present them as AskUserQuestion options. Include a "See all projects" -option. If no table is in context, run `scripts/recent-projects.py ---names` and present those instead. If the user picks "See all -projects", list all project directories and present as a second -AskUserQuestion. - -**1b. Validate project exists** - -Check that `projects//` exists. If not: -- Show an error: "Project `` not found." -- List all available projects and ask the user to pick one. - -**1c. Check current status** - -Read the project's `CLAUDE.md` and parse the frontmatter. If the -status is already `done`: +If `project.frontmatter.status` is `done`: - Inform the user: "Project `` is already marked as done." - Ask if they'd like to update the closing notes anyway. If no, stop. @@ -86,6 +68,8 @@ Using the Edit tool, update the YAML frontmatter: If the user provided closing notes (non-empty, not "no"): +Closing Notes always go in CLAUDE.md (the index), not in detail files. + 1. Check if a `## Closing Notes` section already exists in the file. 2. If it exists, replace its content with the new notes. 3. If it doesn't exist, add a `## Closing Notes` section at the end diff --git a/multi-repo-development/.claude/commands/project/new.md b/multi-repo-development/.claude/commands/project/new.md index a9f9d8c1..9094a26d 100644 --- a/multi-repo-development/.claude/commands/project/new.md +++ b/multi-repo-development/.claude/commands/project/new.md @@ -100,10 +100,14 @@ Additional subdirectories by type: | Documentation | `drafts/` | | Analysis/review | `docs/` | -**3b. Generate CLAUDE.md** +**3b. Generate CLAUDE.md (lean index)** -Write the CLAUDE.md file at `projects//CLAUDE.md` using the -Write tool. The content MUST follow the template for the detected type +Write a **lean index** CLAUDE.md (~50-80 lines) at +`projects//CLAUDE.md` using the Write tool. This file is +an index, not a document — it orients Claude on what the project is and +where to look. All detailed content goes into separate files (Step 3d). + +The content MUST follow the lean template for the detected type (see [CLAUDE.md Templates](#claudemd-templates) below). **3c. Generate .gitignore** @@ -117,6 +121,23 @@ Write a `.gitignore` at `projects//.gitignore` with: *.tar.gz ``` +**3d. Create starter detail files** + +Create type-specific starter files alongside CLAUDE.md. Use the Write +tool for each file. Every file created MUST have a corresponding row in +the CLAUDE.md Reference Files table (generated in Step 3b). + +| Type | Starter files | +|------|--------------| +| Bug investigation | `investigation.md`, `ci-runs.md`, `source-code-map.md` | +| Feature development | `design.md`, `source-code-map.md` | +| CI/testing | `ci-runs.md`, `test-failures.md` | +| Documentation | `drafts.md` | +| Analysis/review | `findings.md` | + +Use the templates in the [Detail File Templates](#detail-file-templates) +section below for the starter content of each file. + ## Step 4: Suggest Skills and Next Steps After creating the project, provide a summary: @@ -140,8 +161,10 @@ After creating the project, provide a summary: ## CLAUDE.md Templates -All generated CLAUDE.md files start with YAML frontmatter for machine -readability, followed by type-specific sections. +CLAUDE.md is an **index**, not a document. It has just enough to orient +Claude on what the project is and where to look. All detailed content +lives in separate files (created in Step 3d) that are loaded on demand +when resuming the project. ### Common Frontmatter @@ -163,69 +186,221 @@ related_links: ### Template Structure -Every project CLAUDE.md follows this structure. Generate the full -markdown using the common frontmatter above, then these sections in -order: +Every project CLAUDE.md follows this structure. The total file should +be ~50-80 lines. Generate using the common frontmatter above, then +these sections in order: 1. **`# `** — from JIRA ticket or user description -2. **`## <Type> Summary`** — heading varies by type (see below), - followed by the user's description and metadata bullet list -3. **Type-specific middle sections** — unique to each type (see below) -4. **`## Progress`** — checklist starting with `- [x] Project created`, - then type-specific items (see below, all unchecked) -5. **`## Related Source Code`** — table with columns: Repo, Key Path, - Purpose (populate from repo context files, or leave as TODO) -6. **`## Suggested Skills`** — populate from the type-to-skill mapping - in Step 4 + +2. **`## <Type> Summary`** — heading varies by type (see below). + Write a 2-3 sentence description of the task, then a short metadata + bullet list (Jira link, Assignee if known). NO inline investigation + details, timelines, or findings — those go in detail files. + +3. **`## Reference Files`** — table with columns `| File | Content |`. + One row per detail file created in Step 3d. This is the manifest — + it is how future sessions discover detail files. During the project + lifecycle, new detail files may be created organically (e.g., + `adversarial-reviews.md`, `jira-comment-root-cause.md`). When + creating a new detail file, always add a row here. + +4. **`## <Plan Section>`** — type-specific heading (see below) with + a checklist of action items. Stays in CLAUDE.md because it is + compact and action-oriented. + +5. **`## Progress`** — high-level checklist starting with + `- [x] Project created`, then type-specific milestone items + (see below, all unchecked). Stays in CLAUDE.md because + `/project:resume` reads it to suggest next steps. ### Type-Specific Content +For each type below, the specification defines: +- The summary heading name +- Metadata bullets to include in the summary +- Which detail files to create (→ rows in Reference Files table) +- The plan section heading and checklist items +- The progress checklist items + **Bug Investigation** (`type: bug`) - Summary heading: `## Bug Summary` -- Metadata: Jira, Priority (TBD), Component, Affected Version (TBD), - Assignee (TBD) -- Sections: `## Attachments` (file/description table), - `## Timeline` (code block for event reconstruction), - `## Investigation Findings`, `## Root Cause`, - `## Fix Plan` (checklist: identify root cause, determine approach, - implement fix, test on cluster, submit PR) +- Metadata: Jira, Assignee (TBD) +- Detail files: `investigation.md`, `ci-runs.md`, `source-code-map.md` +- Plan heading: `## Fix Plan` +- Plan items: Identify root cause, Determine fix approach, Implement + fix, Test on cluster, Submit PR - Progress: Bug details captured, Logs collected and analyzed, Root cause identified, Fix implemented, PR submitted **Feature Development** (`type: feature`) - Summary heading: `## Feature Summary` -- Metadata: Jira, Target Version (TBD), Enhancement (link if applicable) -- Sections: `## Design Notes` (with `### Architecture` and - `### API Changes` subsections), - `## Implementation Plan` (checklist: review enhancement doc, design - approach, implement changes, write tests, submit PRs), - `## Related PRs` (PR/repo/status/description table) +- Metadata: Jira, Target Version (TBD) +- Detail files: `design.md`, `source-code-map.md` +- Plan heading: `## Implementation Plan` +- Plan items: Review enhancement doc, Design approach, Implement + changes, Write tests, Submit PRs - Progress: Design documented, Implementation started, Tests written, PR(s) submitted, PR(s) merged **CI/Testing** (`type: ci-testing`) - Summary heading: `## Test Summary` -- Metadata: Jira, CI Job(s), Test Suite -- Sections: `## CI Job Links` (job/status/link table), - `## Test Failures` (with `### Failure Analysis` table: - test/error/root cause/fix), `## Scripts` +- Metadata: Jira, CI Job(s) (TBD) +- Detail files: `ci-runs.md`, `test-failures.md` +- Plan heading: `## Test Plan` +- Plan items: Identify failing jobs, Analyze failures, Implement fixes, + Validate CI passing - Progress: CI jobs identified, Failures analyzed, Fixes implemented, CI passing **Documentation** (`type: docs`) - Summary heading: `## Doc Summary` - Metadata: Jira, Target (which docs are created/updated) -- Sections: `## Target Documents` (document/repo path/status table), - `## Review Notes` +- Detail files: `drafts.md` +- Plan heading: `## Outline` +- Plan items: Research and outline, Write draft, Technical review, + Editorial review, Submit PR - Progress: Draft written, Technical review, Editorial review, PR submitted **Analysis/Review** (`type: analysis`) - Summary heading: `## Analysis Summary` - Metadata: Jira, Scope (what is being analyzed/reviewed) -- Sections: `## Findings`, `## Recommendations` -- Progress: Analysis started, Findings documented, Recommendations made, - Actions taken +- Detail files: `findings.md` +- Plan heading: `## Analysis Plan` +- Plan items: Define scope, Gather data, Analyze findings, + Write recommendations +- Progress: Analysis started, Findings documented, Recommendations + made, Actions taken + +--- + +## Detail File Templates + +Use these templates when creating starter detail files in Step 3d. +Each file should have a heading and minimal structure — enough to guide +where content goes, but not so much that it feels like boilerplate. + +### `investigation.md` (bug) + +```markdown +# Investigation + +## Failure Analysis + +_Describe the observed failure and symptoms._ + +## Root Cause + +_Root cause goes here once identified._ + +## Proposed Fix + +| Option | Description | Pros | Cons | +|--------|-------------|------|------| +``` + +### `ci-runs.md` (bug, ci-testing) + +```markdown +# CI Runs + +<!-- Add a section per CI run analyzed. Template: --> +<!-- ## Run <ID> (<short description>) --> +<!-- --> +<!-- **Job:** `<job name>` --> +<!-- **Date:** <YYYY-MM-DD> --> +<!-- --> +<!-- | Artifact | Description | --> +<!-- |----------|-------------| --> +<!-- --> +<!-- **Timeline:** --> +<!-- ``` --> +<!-- <chronological events> --> +<!-- ``` --> +``` + +### `source-code-map.md` (bug, feature) + +```markdown +# Source Code Map + +| Repo | Key Path | Purpose | +|------|----------|---------| +``` + +When populating this file: +- For each selected repo, check `repos/<repo>/CLAUDE.md` or + `presets/*/context/<repo>.md` for "Key paths", "Key files", + or similar sections. +- If found, add 1-3 most relevant paths to the table. +- If not found, add the repo name with an empty path and a TODO + comment like "TODO: fill in relevant paths". + +### `design.md` (feature) + +```markdown +# Design + +## Architecture + +_High-level design and component interactions._ + +## API Changes + +_New or modified APIs._ + +## Related PRs + +| PR | Repo | Status | Description | +|----|------|--------|-------------| +``` + +### `test-failures.md` (ci-testing) + +```markdown +# Test Failures + +| Test | Error | Root Cause | Fix | Status | +|------|-------|------------|-----|--------| +``` + +### `drafts.md` (docs) + +```markdown +# Drafts + +## Target Documents + +| Document | Path | Status | +|----------|------|--------| + +## Outline + +_Document outline goes here._ + +## Review Notes + +_Technical and editorial review feedback._ +``` + +### `findings.md` (analysis) + +```markdown +# Findings + +## Scope + +_What is being analyzed and why._ + +## Findings + +_Analysis results._ + +## Recommendations + +| # | Recommendation | Priority | Status | +|---|----------------|----------|--------| +``` --- @@ -239,10 +414,3 @@ order: arguments, minimize questions — only ask what's truly missing - The YAML frontmatter `status` field should always start as `active` - Use today's date for the `created` field -- When populating the "Related Source Code" table: - - For each selected repo, check `repos/<repo>/CLAUDE.md` or - `presets/*/context/<repo>.md` for "Key paths", "Key files", - or similar sections - - If found, add 1-3 most relevant paths to the table - - If not found, add the repo name with an empty path and a TODO - comment like "TODO: fill in relevant paths" diff --git a/multi-repo-development/.claude/commands/project/resume.md b/multi-repo-development/.claude/commands/project/resume.md index 8e0f306c..02f0d9dc 100644 --- a/multi-repo-development/.claude/commands/project/resume.md +++ b/multi-repo-development/.claude/commands/project/resume.md @@ -5,170 +5,105 @@ argument-hint: [name-or-number] # Resume Project Workspace -You are helping a developer resume work on an existing project workspace. -Projects live under the `projects/` directory. Your job is to reload -context and get the developer back up to speed quickly. +Resume work on an existing project. Projects live under `projects/`. -Everything after "resume" in `$ARGUMENTS` is an optional project name -to resume directly. +## Step 1: Resolve Project -## Step 1: Select Project +Run `scripts/resume-project.py $ARGUMENTS` via Bash. Parse the JSON output +and handle by `status`: -**1a. Determine project name** +- **`ok`** — proceed to Step 2. +- **`no_argument`** — present the first 3 `alternatives` as AskUserQuestion + options plus "See all projects" (which shows the full list). Re-run the + script with the chosen name. +- **`not_found`** or **`out_of_range`** — show `error_message`, present + `alternatives` as a picker, re-run with the chosen name. +- **`no_projects`** — show `error_message` and stop. -Handle the argument in `$ARGUMENTS` using these cases: +Store the `project` object from the JSON as `P` for the remaining steps. -**Case A — Numeric shorthand** (e.g., `/project:resume 1`): -If the argument is a plain integer N, look in your conversation context -for the numbered "📂 Recent projects" table produced by the SessionStart -hook. Pick the project name on row N from that table. This avoids an -unnecessary shell call since the hook output is already in context. -If the table is not in context (e.g., session was cleared), fall back to -running `scripts/recent-projects.py --names` and pick the Nth line. -If N is out of range, show an error like "Only M projects exist." and -fall through to Case C (interactive picker). +## Step 2: Load Project Index -**Case B — Project name** (e.g., `/project:resume OCPBUGS-74679`): -If the argument is a non-numeric string, use it directly as the target -project name (current behavior). +1. Read `P.context_file` using the Read tool (skip if null). +2. **Do NOT read `P.repo_context_files` yet.** Store the list for on-demand + loading (see Step 5). -**Case C — No argument** (`/project:resume`): -Look in your conversation context for the "📂 Recent projects" table. -If present, extract the project names from it (up to 3) and present -them as AskUserQuestion options, plus a "See all projects" option. -If the table is not in context, run -`scripts/recent-projects.sh --names | head -3` to get the names instead. -If the user picks "See all projects", run `ls projects/` and present -the full list as a second AskUserQuestion. +## Step 3: Present Summary -**1b. Validate project exists** +Display a structured summary: -Check that `projects/<name>/` exists. If it does not: -- Show an error: "Project `<name>` not found." -- List all available projects from `projects/` -- Ask the user to pick from the list or provide a corrected name - -## Step 2: Load Project Context - -Read whatever context file the project has, in priority order: - -**2a. Try `projects/<name>/CLAUDE.md`** - -If the file exists, read it in full. Then check if it starts with YAML -frontmatter (a line that is exactly `---` followed by YAML content and -closed by another `---`): -- **Has frontmatter**: Parse the YAML to extract `project`, `type`, - `created`, `status`, `jira`, `repos`, and `related_links` fields. -- **No frontmatter**: Treat the entire file as free-form context. Infer - the project type from headings or content if possible (e.g., "Bug - Summary" → bug, "Feature Summary" → feature). - -**2b. Fall back to `projects/<name>/README.md`** - -If no CLAUDE.md exists but README.md does, read it in full. Infer the -project type from headings or content if possible. - -**2c. No context file** - -If neither CLAUDE.md nor README.md exists: -- List all files in the project directory (see Step 2d) -- Ask the user: "This project has no CLAUDE.md or README.md. Can you - briefly describe what this project is about so I can help you - continue?" - -**2d. List project files** - -In all cases, list all files in the project directory (recursively) using -the Bash tool (`find projects/<name>/ -type f | sort`). This gives both -you and the user a picture of what's in the project. - -**2e. Auto-load repo context** +``` +## Project: <P.name> -If the project's frontmatter contains a `repos` list (non-empty), load -context for each repo to prime your understanding of the codebase: +| Field | Value | +|-------|-------| +| **Type** | <P.frontmatter.type or "Unknown"> | +| **Created** | <P.frontmatter.created or "Unknown"> | +| **Status** | <P.frontmatter.status or "Unknown"> | +| **JIRA** | <P.frontmatter.jira or "None"> | +| **Repos** | <comma-separated P.frontmatter.repos, or "None specified"> | +``` -1. For each repo name in the `repos` list: - a. First, check if `repos/<repo>/CLAUDE.md` exists. If so, read it. - b. Otherwise, search for `presets/*/context/<repo>.md`. If found, - read the first match. - c. If neither exists, skip silently (the repo may not have context - files yet). -2. After loading, briefly note to the user which repo context files - were loaded (e.g., "Loaded context for: cluster-etcd-operator, - installer"). -3. Do NOT load context for repos not listed in the project's - frontmatter — only load what's relevant to this project. +**If `P.has_reference_files`:** +Show the reference files table from `P.reference_files`. If +`P.unregistered_files` is non-empty, note them. Show checklist progress +as `P.checklist.checked`/`P.checklist.total`. Add: "Detail files will +be loaded based on what you choose to work on." -## Step 3: Present Project Summary +**If not:** Show `P.all_files` list. Add: "Full project context loaded." -Display a structured summary using this format: +**If `P.repo_context_files` is non-empty:** +Show an "Available Repo Context" table: ``` -## 📂 Project: <name> - -| Field | Value | -|-------|-------| -| **Type** | <type from frontmatter, or inferred, or "Unknown"> | -| **Created** | <date from frontmatter, or "Unknown"> | -| **Status** | <status from frontmatter, or "Unknown"> | -| **JIRA** | <URL from frontmatter, or "None"> | -| **Repos** | <comma-separated list, or "None specified"> | - -### Files -<list all files found in Step 2d> - -### Progress -<If the context file contains checklist items (`- [x]` and `- [ ]`), -show a summary line like: "3/6 items completed" and list the checklist -items. If no checklist items found, say "No progress checklist found."> +| Repo | Source | Path | +|------|--------|------| +| <repo> | <source> | `<path>` | ``` -After the summary table, confirm that the full CLAUDE.md or README.md -content has been read into context (it was read in Step 2 — just note -this to the user so they know the context is loaded). +Add: "Repo context files will be loaded on demand when you work on a +specific repo." -## Step 4: Suggest Next Steps +## Step 4: Task Selection -Based on the project state, provide actionable suggestions: +**4a.** Build a task menu from `P.checklist.unchecked_items`. For each +item, match its text and `section` against `P.reference_files` descriptions +to determine which detail files are relevant. -**4a. Next checklist item** +**4b.** Present via AskUserQuestion with options like: +- "Next: <task text> (loads: file1.md, file2.md)" +- "Review all project notes (loads: all detail files)" +- "Something else" -If the context file has a Progress section with checklist items, find -the first unchecked item (`- [ ]`) and suggest it as the immediate next -action. For example: -> "Based on your progress checklist, the next step is: **Logs collected -> and analyzed**. Would you like to start on that?" +Skip file annotations if `P.has_reference_files` is false (monolithic +project — all content is already in context from Step 2). -**4b. Skill suggestions** +**4c.** After the user picks, read the mapped detail files using Read. +Confirm what was loaded. -Suggest relevant skills based on the project type: +**4d.** Suggest relevant skills from `P.skill_suggestions`. -| Type | Skills to suggest | -|------|-------------------| -| bug | `/prow-job:analyze-test-failure`, `/prow-job:analyze-install-failure`, `/prow-job:extract-must-gather`, `/feature-dev:feature-dev` | -| feature | `/feature-dev:feature-dev`, `/pr-review-toolkit:review-pr` | -| ci-testing | `/prow-job:analyze-test-failure`, `/prow-job:analyze-install-failure`, `/prow-job:analyze-resource`, `/prow-job:extract-must-gather` | -| docs | `/feature-dev:feature-dev` | -| analysis | `/pr-review-toolkit:review-pr`, `/prow-job:analyze-test-failure`, `/feature-dev:feature-dev` | +**4e.** Remind: "If you create new detail files during this session, add +them to the Reference Files table in CLAUDE.md." -If the type is unknown, suggest `/feature-dev:feature-dev` as a general -starting point. +## Step 5: Lazy Repo Context Loading -**4c. Ask what to work on** +**Do NOT load repo context files until needed.** You have the manifest from +`P.repo_context_files` — use it reactively: -End by asking the user what they'd like to work on. Use AskUserQuestion -with contextually relevant options based on the project state. Always -include a "Something else" option. For example, for a bug investigation -with unchecked items: -- "Work on next checklist item: <item>" -- "Review/update project notes" -- "Something else" +- When the user's query involves a specific repo, **read its context file + then** (from `P.repo_context_files` matching that repo name). +- When a task from Step 4 maps to specific repos, load their context at + that point. +- If the user asks to "load all context", comply — but default to lazy. + +This keeps the context window lean for multi-repo projects where you +typically work in one repo at a time. --- -## Important Notes +## Notes -- Always use the Write tool to read/create files, never echo/cat via Bash -- Use Bash tool for `ls`, `find`, and `mkdir -p` operations -- If the project has no context file, don't try to fabricate one — ask - the user for context instead +- Always use the Read tool for files, never cat via Bash +- Use Bash for `ls`, `find`, and `mkdir -p` operations +- If no context file exists, ask the user for context — don't fabricate one diff --git a/multi-repo-development/scripts/resume-project.py b/multi-repo-development/scripts/resume-project.py new file mode 100755 index 00000000..c8ae0ccd --- /dev/null +++ b/multi-repo-development/scripts/resume-project.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +"""Resolve a project and return structured context for /project:resume and /project:close. + +Usage: resume-project.py [project-name-or-number] +Output: JSON to stdout (see resolve_project for schema). +""" + +import glob +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +SKILL_SUGGESTIONS: dict[str, list[str]] = { + "bug": [ + "/prow-job:analyze-test-failure", + "/prow-job:analyze-install-failure", + "/prow-job:extract-must-gather", + "/feature-dev:feature-dev", + ], + "feature": [ + "/feature-dev:feature-dev", + "/pr-review-toolkit:review-pr", + ], + "ci-testing": [ + "/prow-job:analyze-test-failure", + "/prow-job:analyze-install-failure", + "/prow-job:analyze-resource", + "/prow-job:extract-must-gather", + ], + "docs": [ + "/feature-dev:feature-dev", + ], + "analysis": [ + "/pr-review-toolkit:review-pr", + "/prow-job:analyze-test-failure", + "/feature-dev:feature-dev", + ], +} +DEFAULT_SKILLS = ["/feature-dev:feature-dev"] + +ALWAYS_PRESENT = {"CLAUDE.md", "README.md", ".gitignore"} + + +def parse_frontmatter(path: Path) -> dict[str, str | list[str]]: + """Extract YAML frontmatter, handling both scalar values and lists.""" + try: + lines = path.read_text().splitlines() + except OSError: + return {} + + if not lines or lines[0].strip() != "---": + return {} + + result: dict[str, str | list[str]] = {} + current_list_key: str | None = None + + for line in lines[1:]: + if line.strip() == "---": + break + + if line.startswith(" - ") and current_list_key: + val = line.strip().removeprefix("- ") + lst = result[current_list_key] + if isinstance(lst, list): + lst.append(val) + continue + + current_list_key = None + + if ":" not in line: + continue + + key, _, value = line.partition(":") + key = key.strip() + value = value.strip() + + if value == "[]": + result[key] = [] + continue + + if not value: + result[key] = [] + current_list_key = key + continue + + result[key] = value + else: + return {} + + return result + + +def parse_reference_files(text: str) -> list[dict[str, str]]: + """Parse the Reference Files markdown table into [{filename, description}].""" + in_section = False + found_header = False + skipped_separator = False + results = [] + + for line in text.splitlines(): + if re.match(r"^##\s+Reference Files", line, re.IGNORECASE): + in_section = True + continue + + if in_section and not found_header: + if "|" in line and "File" in line: + found_header = True + elif line.startswith("## "): + break + continue + + if found_header and not skipped_separator: + if re.match(r"^\|[-\s|]+\|$", line): + skipped_separator = True + continue + + if skipped_separator: + if not line.strip() or line.startswith("## "): + break + cells = [c.strip() for c in line.split("|")] + cells = [c for c in cells if c] + if len(cells) >= 2: + filename = cells[0].strip("`") + description = cells[1] + results.append({"filename": filename, "description": description}) + + return results + + +def list_project_files(project_dir: Path) -> list[str]: + """Recursively list files relative to project_dir, sorted.""" + files = [] + for root, dirs, filenames in os.walk(project_dir): + dirs[:] = [d for d in dirs if d != ".git"] + for f in filenames: + rel = os.path.relpath(os.path.join(root, f), project_dir) + files.append(rel) + files.sort() + return files + + +def find_unregistered_files( + all_files: list[str], manifest_files: list[dict[str, str]] +) -> list[str]: + """Files on disk not in the Reference Files manifest or ALWAYS_PRESENT.""" + known = ALWAYS_PRESENT | {m["filename"] for m in manifest_files} + return [f for f in all_files if f not in known and not f.startswith(".")] + + +def extract_checklist(text: str) -> dict: + """Extract checked/unchecked items with their section headings.""" + current_section = "" + checked_items = [] + unchecked_items = [] + + for line in text.splitlines(): + heading_match = re.match(r"^#{2,3}\s+(.+)", line) + if heading_match: + current_section = heading_match.group(1).strip() + continue + + item_match = re.match(r"^\s*- \[([ x])\] (.+)$", line) + if item_match: + done = item_match.group(1) == "x" + entry = {"text": item_match.group(2).strip(), "section": current_section} + if done: + checked_items.append(entry) + else: + unchecked_items.append(entry) + + return { + "checked": len(checked_items), + "unchecked": len(unchecked_items), + "total": len(checked_items) + len(unchecked_items), + "unchecked_items": unchecked_items, + "checked_items": checked_items, + } + + +def resolve_repo_context(repos: list[str], root: Path) -> list[dict[str, str]]: + """For each repo, find the best context file (repo CLAUDE.md or preset context).""" + results = [] + for repo in repos: + repo_claude = root / "repos" / repo / "CLAUDE.md" + if repo_claude.is_file(): + results.append({ + "repo": repo, + "path": str(repo_claude.relative_to(root)), + "source": "repo", + }) + continue + + matches = glob.glob(str(root / "presets" / "*" / "context" / f"{repo}.md")) + if matches: + results.append({ + "repo": repo, + "path": str(Path(matches[0]).relative_to(root)), + "source": "preset", + }) + + return results + + +def get_recent_names(root: Path) -> list[str]: + """Get recent non-done project names via recent-projects.py --names.""" + script = root / "scripts" / "recent-projects.py" + if not script.is_file(): + return _fallback_project_names(root) + try: + result = subprocess.run( + [sys.executable, str(script), "--names"], + capture_output=True, text=True, + env={**os.environ, "CLAUDE_PROJECT_DIR": str(root)}, + ) + if result.returncode != 0: + return _fallback_project_names(root) + return [l.strip() for l in result.stdout.splitlines() if l.strip()] + except OSError: + return _fallback_project_names(root) + + +def _fallback_project_names(root: Path) -> list[str]: + """Fallback: list project directories sorted alphabetically.""" + projects_dir = root / "projects" + if not projects_dir.is_dir(): + return [] + return sorted(d.name for d in projects_dir.iterdir() if d.is_dir()) + + +def resolve_project(arg: str | None, root: Path) -> dict: + """Resolve a project argument and return full structured context. + + Returns a dict with: + status: "ok" | "not_found" | "no_projects" | "out_of_range" | "no_argument" + error_message: str (when status != "ok") + alternatives: list[str] (when status != "ok") + project: dict (only when status == "ok") + """ + projects_dir = root / "projects" + + if not projects_dir.is_dir(): + return {"status": "no_projects", "error_message": "No projects/ directory found.", "alternatives": []} + + if arg is None: + names = get_recent_names(root) + if not names: + return {"status": "no_projects", "error_message": "No active projects found.", "alternatives": []} + return {"status": "no_argument", "alternatives": names} + + if arg.isdigit(): + names = get_recent_names(root) + idx = int(arg) - 1 + if idx < 0 or idx >= len(names): + return { + "status": "out_of_range", + "error_message": f"Only {len(names)} projects exist.", + "alternatives": names, + } + project_name = names[idx] + else: + project_name = arg + + project_dir = projects_dir / project_name + if not project_dir.is_dir(): + all_names = sorted(d.name for d in projects_dir.iterdir() if d.is_dir()) + return { + "status": "not_found", + "error_message": f"Project '{project_name}' not found.", + "alternatives": all_names, + } + + claude_md = project_dir / "CLAUDE.md" + readme = project_dir / "README.md" + + if claude_md.is_file(): + context_file = str(claude_md.relative_to(root)) + context_type = "claude_md" + try: + text = claude_md.read_text() + except OSError: + text = "" + elif readme.is_file(): + context_file = str(readme.relative_to(root)) + context_type = "readme" + try: + text = readme.read_text() + except OSError: + text = "" + else: + context_file = None + context_type = "none" + text = "" + + fm = parse_frontmatter(claude_md) if claude_md.is_file() else {} + has_frontmatter = bool(fm) + + repos_list = fm.get("repos", []) + if isinstance(repos_list, str): + repos_list = [repos_list] if repos_list else [] + + ref_files = parse_reference_files(text) if text else [] + all_files = list_project_files(project_dir) + unregistered = find_unregistered_files(all_files, ref_files) if ref_files else [] + checklist = extract_checklist(text) if text else { + "checked": 0, "unchecked": 0, "total": 0, + "unchecked_items": [], "checked_items": [], + } + repo_context = resolve_repo_context(repos_list, root) + + project_type = fm.get("type", "") + if isinstance(project_type, list): + project_type = project_type[0] if project_type else "" + suggestions = SKILL_SUGGESTIONS.get(project_type, DEFAULT_SKILLS) + + return { + "status": "ok", + "project": { + "name": project_name, + "dir": str(project_dir.relative_to(root)), + "context_file": context_file, + "context_type": context_type, + "has_frontmatter": has_frontmatter, + "frontmatter": { + "project": fm.get("project", ""), + "type": fm.get("type", ""), + "created": fm.get("created", ""), + "status": fm.get("status", ""), + "jira": fm.get("jira", ""), + "repos": repos_list, + "related_links": fm.get("related_links", []), + }, + "reference_files": ref_files, + "has_reference_files": bool(ref_files), + "all_files": all_files, + "unregistered_files": unregistered, + "checklist": checklist, + "repo_context_files": repo_context, + "skill_suggestions": suggestions, + }, + } + + +def main(): + root = Path(os.environ.get("CLAUDE_PROJECT_DIR", Path(__file__).resolve().parent.parent)) + arg = sys.argv[1] if len(sys.argv) > 1 else None + result = resolve_project(arg, root) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() From 044c9eb229b8c15df96cd16e82a317c2e27befc3 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla <pfontani@redhat.com> Date: Tue, 28 Apr 2026 14:22:30 +0200 Subject: [PATCH 2/7] Apply CodeRabbit suggestions from PR #101 - Tolerate trailing whitespace in markdown table separator regex - Accept uppercase [X] in checklist parsing - Rename ambiguous variable `l` to `line` (ruff E741) Co-Authored-By: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- multi-repo-development/scripts/resume-project.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/multi-repo-development/scripts/resume-project.py b/multi-repo-development/scripts/resume-project.py index c8ae0ccd..88636624 100755 --- a/multi-repo-development/scripts/resume-project.py +++ b/multi-repo-development/scripts/resume-project.py @@ -113,7 +113,7 @@ def parse_reference_files(text: str) -> list[dict[str, str]]: continue if found_header and not skipped_separator: - if re.match(r"^\|[-\s|]+\|$", line): + if re.match(r"^\|[-\s|]+\|\s*$", line): skipped_separator = True continue @@ -162,9 +162,9 @@ def extract_checklist(text: str) -> dict: current_section = heading_match.group(1).strip() continue - item_match = re.match(r"^\s*- \[([ x])\] (.+)$", line) + item_match = re.match(r"^\s*- \[([ xX])\] (.+)$", line) if item_match: - done = item_match.group(1) == "x" + done = item_match.group(1).lower() == "x" entry = {"text": item_match.group(2).strip(), "section": current_section} if done: checked_items.append(entry) @@ -217,7 +217,7 @@ def get_recent_names(root: Path) -> list[str]: ) if result.returncode != 0: return _fallback_project_names(root) - return [l.strip() for l in result.stdout.splitlines() if l.strip()] + return [line.strip() for line in result.stdout.splitlines() if line.strip()] except OSError: return _fallback_project_names(root) From 776941e40fc9addb495eadd319a3dae80a8547b1 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla <pfontani@redhat.com> Date: Thu, 30 Apr 2026 13:47:27 +0200 Subject: [PATCH 3/7] Fix markdownlint violations in /project command specs Convert bold step labels to ### subheadings (MD036), add blank lines around lists (MD032), add language specifiers to fenced code blocks (MD040), indent table under ordered list item to preserve numbering (MD029), and escape angle brackets in inline examples (MD033). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .../.claude/commands/project/close.md | 18 ++++---- .../.claude/commands/project/new.md | 41 +++++++++++-------- .../.claude/commands/project/resume.md | 7 ++-- 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/multi-repo-development/.claude/commands/project/close.md b/multi-repo-development/.claude/commands/project/close.md index 966983ad..7969030d 100644 --- a/multi-repo-development/.claude/commands/project/close.md +++ b/multi-repo-development/.claude/commands/project/close.md @@ -10,12 +10,13 @@ done. This updates the project's CLAUDE.md frontmatter and optionally records closing notes. Everything after "close" in `$ARGUMENTS` is parsed as follows: + - The **first token** is an optional project name or numeric shorthand. - Everything after the first token is treated as **closing notes**. ## Step 1: Select Project -**1a. Resolve project name** +### 1a. Resolve project name Extract the first token from `$ARGUMENTS`. Run `scripts/resume-project.py <first-token>` via Bash (omit the token if @@ -29,20 +30,21 @@ none was provided). Parse the JSON and handle by `status`: `alternatives` as a picker, re-run with chosen name. - **`no_projects`** — show `error_message` and stop. -**1b. Check current status** +### 1b. Check current status If `project.frontmatter.status` is `done`: + - Inform the user: "Project `<name>` is already marked as done." - Ask if they'd like to update the closing notes anyway. If no, stop. ## Step 2: Gather Closing Notes -**2a. Extract notes from arguments** +### 2a. Extract notes from arguments If there is text after the project identifier in `$ARGUMENTS`, use it as the closing notes. -**2b. Ask for notes** +### 2b. Ask for notes If no notes were provided in the arguments, ask the user: @@ -51,11 +53,11 @@ If no notes were provided in the arguments, ask the user: ## Step 3: Update Project CLAUDE.md -**3a. Read the current CLAUDE.md** +### 3a. Read the current CLAUDE.md Read the full `projects/<name>/CLAUDE.md` file. -**3b. Update frontmatter fields** +### 3b. Update frontmatter fields Using the Edit tool, update the YAML frontmatter: @@ -64,7 +66,7 @@ Using the Edit tool, update the YAML frontmatter: 2. Add a `closed: <YYYY-MM-DD>` field (today's date) after the `status` line. If a `closed:` field already exists, update it. -**3c. Add closing notes section** +### 3c. Add closing notes section If the user provided closing notes (non-empty, not "no"): @@ -87,7 +89,7 @@ _Closed YYYY-MM-DD_ Display a brief confirmation: -``` +```text Project `<name>` marked as done. ``` diff --git a/multi-repo-development/.claude/commands/project/new.md b/multi-repo-development/.claude/commands/project/new.md index 9094a26d..ea7a5cb3 100644 --- a/multi-repo-development/.claude/commands/project/new.md +++ b/multi-repo-development/.claude/commands/project/new.md @@ -18,14 +18,14 @@ Ask the user questions to understand what they're working on. Use the AskUserQuestion tool for structured questions and encourage free-text descriptions. -**1a. Task Description** +### 1a. Task Description If the user provided a description after "new" in the arguments, use that. Otherwise, ask: > "What task are you working on? Please describe it in a sentence or two." -**1b. Task Type** +### 1b. Task Type Based on the description, suggest a task type and confirm with the user. Use AskUserQuestion with these options: @@ -38,13 +38,13 @@ Use AskUserQuestion with these options: | Documentation | Description mentions docs, writing, documenting, guide | | Analysis/review | Description mentions reviewing, analyzing, investigating (without a specific bug), understanding | -**1c. JIRA Ticket (optional)** +### 1c. JIRA Ticket (optional) Ask: "Do you have a JIRA ticket for this task? If so, paste the URL (e.g., https://issues.redhat.com/browse/OCPBUGS-12345). Otherwise, just say 'no'." -**1d. Related Repositories** +### 1d. Related Repositories Ask which repos from this workspace are relevant. **Dynamically load the repo list** from `dev-env.yaml` at the workspace root: @@ -57,7 +57,7 @@ the repo list** from `dev-env.yaml` at the workspace root: and note that no repos are configured (the user can add them later by editing the project's CLAUDE.md frontmatter). -**1e. Additional Context (optional)** +### 1e. Additional Context (optional) Ask: "Any additional context? (PR URLs, Prow job URLs, related projects, etc.) Say 'no' to skip." @@ -85,7 +85,7 @@ Based on the gathered information: Create the project directory and generate files based on the task type. -**3a. Create directory structure** +### 3a. Create directory structure Use the Bash tool to create directories. The base is always `projects/<folder-name>/`. @@ -100,7 +100,7 @@ Additional subdirectories by type: | Documentation | `drafts/` | | Analysis/review | `docs/` | -**3b. Generate CLAUDE.md (lean index)** +### 3b. Generate CLAUDE.md (lean index) Write a **lean index** CLAUDE.md (~50-80 lines) at `projects/<folder-name>/CLAUDE.md` using the Write tool. This file is @@ -110,18 +110,18 @@ where to look. All detailed content goes into separate files (Step 3d). The content MUST follow the lean template for the detected type (see [CLAUDE.md Templates](#claudemd-templates) below). -**3c. Generate .gitignore** +### 3c. Generate .gitignore Write a `.gitignore` at `projects/<folder-name>/.gitignore` with: -``` +```gitignore # Large files that shouldn't be committed *.log *.txt.gz *.tar.gz ``` -**3d. Create starter detail files** +### 3d. Create starter detail files Create type-specific starter files alongside CLAUDE.md. Use the Write tool for each file. Every file created MUST have a corresponding row in @@ -145,13 +145,13 @@ After creating the project, provide a summary: 1. List the files and directories created 2. Suggest relevant skills based on the task type: -| Type | Skills to suggest | -|------|-------------------| -| bug | `/prow-job:analyze-test-failure`, `/prow-job:analyze-install-failure`, `/prow-job:extract-must-gather`, `/feature-dev:feature-dev` | -| feature | `/feature-dev:feature-dev`, `/pr-review-toolkit:review-pr` | -| ci-testing | `/prow-job:analyze-test-failure`, `/prow-job:analyze-install-failure`, `/prow-job:analyze-resource`, `/prow-job:extract-must-gather` | -| docs | `/feature-dev:feature-dev` | -| analysis | `/pr-review-toolkit:review-pr`, `/prow-job:analyze-test-failure`, `/feature-dev:feature-dev` | + | Type | Skills to suggest | + |------|-------------------| + | bug | `/prow-job:analyze-test-failure`, `/prow-job:analyze-install-failure`, `/prow-job:extract-must-gather`, `/feature-dev:feature-dev` | + | feature | `/feature-dev:feature-dev`, `/pr-review-toolkit:review-pr` | + | ci-testing | `/prow-job:analyze-test-failure`, `/prow-job:analyze-install-failure`, `/prow-job:analyze-resource`, `/prow-job:extract-must-gather` | + | docs | `/feature-dev:feature-dev` | + | analysis | `/pr-review-toolkit:review-pr`, `/prow-job:analyze-test-failure`, `/feature-dev:feature-dev` | 3. Suggest concrete next steps for starting the work 4. Remind the user they can resume this project later with @@ -216,6 +216,7 @@ these sections in order: ### Type-Specific Content For each type below, the specification defines: + - The summary heading name - Metadata bullets to include in the summary - Which detail files to create (→ rows in Reference Files table) @@ -223,6 +224,7 @@ For each type below, the specification defines: - The progress checklist items **Bug Investigation** (`type: bug`) + - Summary heading: `## Bug Summary` - Metadata: Jira, Assignee (TBD) - Detail files: `investigation.md`, `ci-runs.md`, `source-code-map.md` @@ -233,6 +235,7 @@ For each type below, the specification defines: Root cause identified, Fix implemented, PR submitted **Feature Development** (`type: feature`) + - Summary heading: `## Feature Summary` - Metadata: Jira, Target Version (TBD) - Detail files: `design.md`, `source-code-map.md` @@ -243,6 +246,7 @@ For each type below, the specification defines: PR(s) submitted, PR(s) merged **CI/Testing** (`type: ci-testing`) + - Summary heading: `## Test Summary` - Metadata: Jira, CI Job(s) (TBD) - Detail files: `ci-runs.md`, `test-failures.md` @@ -253,6 +257,7 @@ For each type below, the specification defines: CI passing **Documentation** (`type: docs`) + - Summary heading: `## Doc Summary` - Metadata: Jira, Target (which docs are created/updated) - Detail files: `drafts.md` @@ -263,6 +268,7 @@ For each type below, the specification defines: PR submitted **Analysis/Review** (`type: analysis`) + - Summary heading: `## Analysis Summary` - Metadata: Jira, Scope (what is being analyzed/reviewed) - Detail files: `findings.md` @@ -329,6 +335,7 @@ _Root cause goes here once identified._ ``` When populating this file: + - For each selected repo, check `repos/<repo>/CLAUDE.md` or `presets/*/context/<repo>.md` for "Key paths", "Key files", or similar sections. diff --git a/multi-repo-development/.claude/commands/project/resume.md b/multi-repo-development/.claude/commands/project/resume.md index 02f0d9dc..a5a330c8 100644 --- a/multi-repo-development/.claude/commands/project/resume.md +++ b/multi-repo-development/.claude/commands/project/resume.md @@ -32,7 +32,7 @@ Store the `project` object from the JSON as `P` for the remaining steps. Display a structured summary: -``` +```text ## Project: <P.name> | Field | Value | @@ -55,7 +55,7 @@ be loaded based on what you choose to work on." **If `P.repo_context_files` is non-empty:** Show an "Available Repo Context" table: -``` +```text | Repo | Source | Path | |------|--------|------| | <repo> | <source> | `<path>` | @@ -71,7 +71,8 @@ item, match its text and `section` against `P.reference_files` descriptions to determine which detail files are relevant. **4b.** Present via AskUserQuestion with options like: -- "Next: <task text> (loads: file1.md, file2.md)" + +- "Next: \<task text\> (loads: file1.md, file2.md)" - "Review all project notes (loads: all detail files)" - "Something else" From 3496f1379d05e5304aae3b25a46a8b118920e007 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla <pfontani@redhat.com> Date: Thu, 30 Apr 2026 15:05:58 +0200 Subject: [PATCH 4/7] Apply CodeRabbit suggestions from PR #110 - scripts/resume-project.py:116: Accept alignment colons in table separator regex Co-Authored-By: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- multi-repo-development/scripts/resume-project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi-repo-development/scripts/resume-project.py b/multi-repo-development/scripts/resume-project.py index 88636624..5582b64e 100755 --- a/multi-repo-development/scripts/resume-project.py +++ b/multi-repo-development/scripts/resume-project.py @@ -113,7 +113,7 @@ def parse_reference_files(text: str) -> list[dict[str, str]]: continue if found_header and not skipped_separator: - if re.match(r"^\|[-\s|]+\|\s*$", line): + if re.match(r"^\|[-:\s|]+\|\s*$", line): skipped_separator = True continue From e33b803732d73bce7ebbbdd01cb6841499ae8bd2 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla <pfontani@redhat.com> Date: Thu, 30 Apr 2026 17:21:58 +0200 Subject: [PATCH 5/7] Fix indexed project lookup returning out_of_range for empty list When get_recent_names() returns no projects, the digit-index branch now returns no_projects instead of out_of_range, preventing callers from entering an empty retry flow. Co-Authored-By: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- multi-repo-development/scripts/resume-project.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/multi-repo-development/scripts/resume-project.py b/multi-repo-development/scripts/resume-project.py index 5582b64e..591e6875 100755 --- a/multi-repo-development/scripts/resume-project.py +++ b/multi-repo-development/scripts/resume-project.py @@ -252,6 +252,8 @@ def resolve_project(arg: str | None, root: Path) -> dict: if arg.isdigit(): names = get_recent_names(root) + if not names: + return {"status": "no_projects", "error_message": "No recent projects found.", "alternatives": []} idx = int(arg) - 1 if idx < 0 or idx >= len(names): return { From 5cd2158fc6a3392f4ac9050a2f873d425ea017dd Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla <pfontani@redhat.com> Date: Wed, 6 May 2026 13:39:53 +0200 Subject: [PATCH 6/7] Apply CodeRabbit suggestions from PR #110 Auto-applied: - resume-project.py:84: Don't coerce empty scalar frontmatter fields to lists - resume-project.py:213: Add 5s timeout to recent-projects.py subprocess Co-Authored-By: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- multi-repo-development/scripts/resume-project.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/multi-repo-development/scripts/resume-project.py b/multi-repo-development/scripts/resume-project.py index 591e6875..719a0375 100755 --- a/multi-repo-development/scripts/resume-project.py +++ b/multi-repo-development/scripts/resume-project.py @@ -63,9 +63,9 @@ def parse_frontmatter(path: Path) -> dict[str, str | list[str]]: if line.startswith(" - ") and current_list_key: val = line.strip().removeprefix("- ") - lst = result[current_list_key] - if isinstance(lst, list): - lst.append(val) + if not isinstance(result.get(current_list_key), list): + result[current_list_key] = [] + result[current_list_key].append(val) continue current_list_key = None @@ -82,7 +82,7 @@ def parse_frontmatter(path: Path) -> dict[str, str | list[str]]: continue if not value: - result[key] = [] + result[key] = "" current_list_key = key continue @@ -214,11 +214,12 @@ def get_recent_names(root: Path) -> list[str]: [sys.executable, str(script), "--names"], capture_output=True, text=True, env={**os.environ, "CLAUDE_PROJECT_DIR": str(root)}, + timeout=5, ) if result.returncode != 0: return _fallback_project_names(root) return [line.strip() for line in result.stdout.splitlines() if line.strip()] - except OSError: + except (OSError, subprocess.TimeoutExpired): return _fallback_project_names(root) From f6a7acec373806fb1c330c7aa6241c4da06e83f2 Mon Sep 17 00:00:00 2001 From: Pablo Fontanilla <pfontani@redhat.com> Date: Wed, 6 May 2026 17:23:51 +0200 Subject: [PATCH 7/7] Fix related_links type inconsistency for empty frontmatter values Empty `related_links:` in frontmatter now returns "" (after the empty-scalar fix), but absent `related_links` returns []. Use `or []` to normalize both cases to a list. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- multi-repo-development/scripts/resume-project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi-repo-development/scripts/resume-project.py b/multi-repo-development/scripts/resume-project.py index 719a0375..90b90757 100755 --- a/multi-repo-development/scripts/resume-project.py +++ b/multi-repo-development/scripts/resume-project.py @@ -333,7 +333,7 @@ def resolve_project(arg: str | None, root: Path) -> dict: "status": fm.get("status", ""), "jira": fm.get("jira", ""), "repos": repos_list, - "related_links": fm.get("related_links", []), + "related_links": fm.get("related_links") or [], }, "reference_files": ref_files, "has_reference_files": bool(ref_files),