diff --git a/multi-repo-development/.claude/commands/project/close.md b/multi-repo-development/.claude/commands/project/close.md index 764f06d5..7969030d 100644 --- a/multi-repo-development/.claude/commands/project/close.md +++ b/multi-repo-development/.claude/commands/project/close.md @@ -10,57 +10,41 @@ 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. Determine project name** - -Handle the argument using the same cases as `/project:resume`: - -**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. +### 1a. Resolve project name -**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. +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 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. +- **`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. -**1b. Validate project exists** +### 1b. Check current status -Check that `projects//` exists. If not: -- Show an error: "Project `` not found." -- List all available projects and ask the user to pick one. +If `project.frontmatter.status` is `done`: -**1c. Check current status** - -Read the project's `CLAUDE.md` and parse the frontmatter. If the -status is already `done`: - Inform the user: "Project `` 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: @@ -69,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//CLAUDE.md` file. -**3b. Update frontmatter fields** +### 3b. Update frontmatter fields Using the Edit tool, update the YAML frontmatter: @@ -82,10 +66,12 @@ Using the Edit tool, update the YAML frontmatter: 2. Add a `closed: ` 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"): +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 @@ -103,7 +89,7 @@ _Closed YYYY-MM-DD_ Display a brief confirmation: -``` +```text Project `` marked as done. ``` diff --git a/multi-repo-development/.claude/commands/project/new.md b/multi-repo-development/.claude/commands/project/new.md index a9f9d8c1..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//`. @@ -100,23 +100,44 @@ 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** +### 3c. Generate .gitignore Write a `.gitignore` at `projects//.gitignore` with: -``` +```gitignore # Large files that shouldn't be committed *.log *.txt.gz *.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: @@ -124,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 @@ -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,228 @@ 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 +421,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..a5a330c8 100644 --- a/multi-repo-development/.claude/commands/project/resume.md +++ b/multi-repo-development/.claude/commands/project/resume.md @@ -5,170 +5,106 @@ 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 +```text +## Project: <P.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** - -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: +```text +| Repo | Source | Path | +|------|--------|------| +| <repo> | <source> | `<path>` | ``` -## 📂 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."> -``` +Add: "Repo context files will be loaded on demand when you work on a +specific repo." -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). +## Step 4: Task Selection -## Step 4: Suggest Next Steps +**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. -Based on the project state, provide actionable suggestions: +**4b.** Present via AskUserQuestion with options like: -**4a. Next checklist item** +- "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..90b90757 --- /dev/null +++ b/multi-repo-development/scripts/resume-project.py @@ -0,0 +1,357 @@ +#!/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("- ") + if not isinstance(result.get(current_list_key), list): + result[current_list_key] = [] + result[current_list_key].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|]+\|\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*- \[([ xX])\] (.+)$", line) + if item_match: + done = item_match.group(1).lower() == "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)}, + 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, subprocess.TimeoutExpired): + 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) + 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 { + "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") or [], + }, + "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()