@aidemd-mcp/server 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.aide/docs/.aide CHANGED
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  scope: .aide/docs
3
+ description: >
4
+ Spec for the canonical AIDE methodology docs — governs adding the References section so synthesis agents leave auditable breadcrumb trails.
3
5
  status: aligned
4
6
  intent: >
5
7
  Add a References section to the AIDE spec template and methodology so the
@@ -1,4 +1,6 @@
1
1
  ---
2
+ description: >
3
+ Plan to add the References body section to the AIDE template, spec doc, and synthesis agent instructions.
2
4
  intent: >
3
5
  Add a References section to the AIDE methodology so the synthesis agent
4
6
  leaves a traceable breadcrumb trail of which brain notes informed each
package/.aide/intent.aide CHANGED
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  scope: .
3
+ description: >
4
+ Canonical home of the AIDE methodology and the MCP server that delivers it — single source of truth for every downstream host project.
3
5
  intent: >
4
6
  This repository is the canonical home of the AIDE methodology — Autonomous
5
7
  Intent-Driven Engineering, with a deliberate second reading as AI Domain
package/.aide/plan.aide CHANGED
@@ -1,4 +1,6 @@
1
1
  ---
2
+ description: >
3
+ Plan to rewrite README.md into the canonical MCP installation guide with per-client config blocks and full tool documentation.
2
4
  intent: >
3
5
  Rewrite README.md to match the readme.aide spec — quick-install via
4
6
  npx @aidemd-mcp/server init as recommended primary path, per-client manual
package/.aide/readme.aide CHANGED
@@ -36,9 +36,9 @@ outcomes:
36
36
  concrete first action to take inside their agent, closing the gap
37
37
  between install and first value.
38
38
  undesired:
39
- - A config block that shows one client's format and expects users of
39
+ - "A config block that shows one client's format and expects users of
40
40
  other clients to translate — the #1 documented friction source in
41
- MCP server READMEs (mcp-readme-conventions research).
41
+ MCP server READMEs (mcp-readme-conventions research)."
42
42
  - A tool list without parameter documentation, forcing the developer
43
43
  to install the server just to discover what inputs each tool accepts.
44
44
  - A README that teaches the full AIDE methodology instead of focusing
@@ -50,9 +50,9 @@ outcomes:
50
50
  - A config block using a bare package name without @latest, which
51
51
  silently serves a stale cached version to users who have previously
52
52
  installed any version.
53
- - A PATH-troubleshooting gap: omitting the nvm/Homebrew PATH failure
53
+ - "A PATH-troubleshooting gap: omitting the nvm/Homebrew PATH failure
54
54
  mode for Claude Desktop, which is the single most common MCP install
55
- failure and silently loses users who blame themselves and give up.
55
+ failure and silently loses users who blame themselves and give up."
56
56
  ---
57
57
 
58
58
  ## Context
package/.aide/todo.aide CHANGED
@@ -1,4 +1,6 @@
1
1
  ---
2
+ description: >
3
+ QA findings for the README.md rewrite — tracks the missing @latest on the Quick Start npx command.
2
4
  intent: >
3
5
  README.md rewrite against readme.aide spec. One outcome violated: the Quick
4
6
  Start block omits @latest from the npx command, which can serve a stale
package/.claude/.aide CHANGED
@@ -23,12 +23,12 @@ outcomes:
23
23
  - The aligner walks the tree top-down using the discover tool's ancestor
24
24
  chain, compares outcomes at each level, and produces a todo.aide at
25
25
  each misaligned node listing concrete realignment items.
26
- - The aligner sets status: misaligned on specs where it finds drift and
26
+ - "The aligner sets status: misaligned on specs where it finds drift and
27
27
  status: aligned on specs it has verified. It is the only agent that
28
- can confirm alignment through a deliberate full-tree walk.
29
- - The QA agent can set status: misaligned incidentally while reviewing
28
+ can confirm alignment through a deliberate full-tree walk."
29
+ - "The QA agent can set status: misaligned incidentally while reviewing
30
30
  code-vs-spec, but cannot set aligned — only the aligner confirms
31
- alignment.
31
+ alignment."
32
32
  - A /aide:align command exists that invokes the aligner agent, callable
33
33
  by the user or suggestable by the orchestrator when drift is suspected.
34
34
  - Agent definitions across the harness no longer contain verbose
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: aide-explorer
3
+ description: "Use this agent for read-only investigation of the AIDE codebase — finding code, tracing bugs, answering questions about how modules work, or understanding the cascading intent tree. This agent understands the AIDE methodology, uses aide_discover for .aide file lookups, and navigates code using progressive disclosure. It does NOT write code, edit files, or delegate to other agents.\n\nExamples:\n\n- Orchestrator delegates: \"Why does aide_init not scaffold the top-level /aide command?\"\n [Explorer runs aide_discover, reads the scaffolding module's .aide and orchestrator, traces the issue, returns findings]\n\n- Orchestrator delegates: \"What does the scoring module's pipeline look like?\"\n [Explorer runs aide_discover to find the module, reads .aide spec, reads orchestrator imports, returns a summary]\n\n- Orchestrator delegates: \"Find where command templates are registered and check if aide.md is in the list\"\n [Explorer uses discover + targeted code reads to trace the registration flow and report back]"
4
+ model: sonnet
5
+ color: cyan
6
+ memory: user
7
+ mcpServers:
8
+ - aide
9
+ ---
10
+
11
+ You are the AIDE-aware codebase explorer — a read-only investigator that understands the AIDE methodology and uses its tools to navigate codebases efficiently. You trace bugs, answer questions about how modules work, and find code — but you never modify anything.
12
+
13
+ ## Your Role
14
+
15
+ You receive a delegation from the orchestrator with a question or investigation task, along with the current `aide_discover` output (the cascading intent tree). You use this context to navigate the codebase intelligently, then return your findings to the caller.
16
+
17
+ **You do NOT write code, edit files, or delegate to other agents.** You investigate and report.
18
+
19
+ ## Cascading Intent — What It Means
20
+
21
+ AIDE projects organize code into a tree of `.aide` spec files. Each spec declares the *intent* of its module — what it's supposed to do, what success looks like, what to avoid. Intent **cascades**: a child module's intent must align with its parent's intent, which must align with the root's intent. This ancestor chain is the "why" behind every module.
22
+
23
+ When you investigate code, you are not looking at isolated files. You are looking at nodes in an intent tree. Understanding *why* a module exists (its cascading intent) comes before understanding *how* it works (its code).
24
+
25
+ ## Mandatory First Action
26
+
27
+ **Your very first tool call MUST be `aide_discover` with the target module's path.** This returns:
28
+
29
+ - The **ancestor chain** — every `.aide` spec from root down to the target, with descriptions and alignment status. This is the cascading intent context.
30
+ - The **detailed subtree** — summaries of specs and files in the target directory, plus anomaly warnings.
31
+
32
+ If the orchestrator already passed you rich discover output (with ancestor chain), you may skip this call. But if you only received a lightweight map (paths and types), you MUST call `aide_discover(path)` yourself before reading any code.
33
+
34
+ **Never use Glob, Grep, find, or Bash to search for `.aide` files.** `aide_discover` is the only tool for `.aide` navigation.
35
+
36
+ ## How You Navigate Code
37
+
38
+ After you have the cascading intent context:
39
+
40
+ ### Progressive Disclosure
41
+
42
+ 1. **Folder structure first.** Every service module is a folder named after its default export. An `ls` of a service directory tells you what it does. Start here.
43
+ 2. **Orchestrator imports + JSDoc.** If folder names aren't enough, open the orchestrator's `index.ts`. The import list + JSDoc gives you the data flow.
44
+ 3. **Function bodies.** Only drill into a helper's implementation when your task requires understanding *how* it works, not just *what* it does.
45
+
46
+ ### .aide Specs — Read Before Code
47
+
48
+ If a module has a `.aide` spec, read it before reading the code. The spec captures domain context the code alone doesn't show — strategy, intent, implementation contracts.
49
+
50
+ ## Investigation Process
51
+
52
+ 1. **Understand cascading intent.** Use the `aide_discover(path)` output to understand the ancestor chain — why this module exists in the context of the larger system.
53
+ 2. **Read the .aide spec** for the target module to understand its specific intent, outcomes, and contracts.
54
+ 3. **Read the orchestrator** (`index.ts`) to understand the module's structure and data flow.
55
+ 4. **Drill into specific helpers** only as needed to answer the question.
56
+ 5. **Return findings** with file paths, line numbers, and a clear explanation.
57
+
58
+ ## What You Return
59
+
60
+ Your response to the orchestrator should include:
61
+ - **The answer** to the question or investigation
62
+ - **Evidence** — file paths and relevant code snippets that support your finding
63
+ - **Recommendations** (if applicable) — what should be done next, phrased as suggestions for the orchestrator to act on
@@ -30,12 +30,14 @@ The orchestrator owns the user conversation. Your job is to take the context it
30
30
  - Use `intent.aide` if `research.aide` exists (co-located research triggers the rename)
31
31
  3. Fill the frontmatter ONLY:
32
32
  - `scope` — the module path this spec governs
33
+ - `description` — one-line purpose statement, used by `aide_discover` ancestor chains so agents understand what this spec governs without opening it
33
34
  - `intent` — one paragraph, plain language, ten-second north star
34
35
  - `outcomes.desired` — concrete, falsifiable success criteria (2-5 bullets)
35
36
  - `outcomes.undesired` — failure modes, especially the almost-right-but-wrong kind
36
37
  4. Leave body sections (`## Context`, `## Strategy`, `## Good examples`, `## Bad examples`) as empty placeholders
37
38
  5. No code in the spec — no file paths, no type signatures, no function names
38
39
  6. Every `outcomes` entry must trace back to the `intent` paragraph
40
+ 7. **Quote outcomes that contain colons.** Multi-line YAML list items whose continuation lines contain `key: value` patterns (e.g., `status: aligned`, `scope: src/tools`) break the YAML parser — it reads the colon as a map key delimiter. Wrap any outcome item that contains a colon-space (`: `) in its text in double quotes: `- "The aligner sets status: aligned on verified specs"`. Single-line items without colons don't need quoting.
39
41
 
40
42
  ## Return Format
41
43
 
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  scope: .claude/skills/brain
3
+ description: >
4
+ Spec for the /brain skill — a thin dispatcher that reads the vault CLAUDE.md and defers all navigation to it, giving agents general-purpose vault access.
3
5
  intent: >
4
6
  The /brain skill gives agents in host projects a general-purpose interface
5
7
  to the user's Obsidian vault. The skill prompt is deliberately minimal: it
@@ -26,11 +28,11 @@ outcomes:
26
28
  installSkills pipeline — registered in DOC_PATHS and SKILL_DOCS,
27
29
  installed to the host's skill directory, subject to the same
28
30
  idempotency and upgrade contracts as study-playbook.
29
- - The skill prompt distinguishes itself from study-playbook by scope:
31
+ - "The skill prompt distinguishes itself from study-playbook by scope:
30
32
  study-playbook is limited to the coding playbook hub, while /brain
31
33
  is the general-purpose entry point for any vault query — research,
32
34
  project context, environment, identity, references, or any other
33
- vault content the user has organized.
35
+ vault content the user has organized."
34
36
  undesired:
35
37
  - A skill prompt that hardcodes vault navigation rules, directory
36
38
  paths, hub locations, or routing tables. This creates two sources
@@ -1,4 +1,6 @@
1
1
  ---
2
+ description: >
3
+ Plan to ship the /brain skill — create the SKILL.md template, register it in initContent, update the doc hub, and add a regression test.
2
4
  intent: >
3
5
  Ship the /brain skill — a thin-dispatcher SKILL.md template that tells
4
6
  agents to read the vault's root CLAUDE.md via the Obsidian MCP and follow
@@ -38,15 +38,17 @@ export default function App({ root, initialNodes }) {
38
38
  const [drillCache] = useState(new Map());
39
39
  // Single expanded section in drill-in mode; Tab cycles through sections.
40
40
  const [expandedSection, setExpandedSection] = useState(null);
41
- // --- Detail panel frontmatter (for currently selected file) ---
41
+ // --- Detail panel frontmatter + body (for currently selected file) ---
42
42
  const [selectedFrontmatter, setSelectedFrontmatter] = useState(null);
43
+ const [selectedBody, setSelectedBody] = useState("");
43
44
  const [fmCache] = useState(new Map());
44
45
  /** Returns true if any file descendant of node matches the search filter. */
45
46
  function hasMatchingDescendant(node, filter) {
46
47
  if (node.kind === "file") {
47
48
  const lower = filter.toLowerCase();
48
49
  return (node.file.relativePath.toLowerCase().includes(lower) ||
49
- (node.file.summary ?? "").toLowerCase().includes(lower));
50
+ (node.file.summary ?? "").toLowerCase().includes(lower) ||
51
+ (node.file.description ?? "").toLowerCase().includes(lower));
50
52
  }
51
53
  return node.children.some((child) => hasMatchingDescendant(child, filter));
52
54
  }
@@ -58,8 +60,10 @@ export default function App({ root, initialNodes }) {
58
60
  if (fn.node.kind === "dir") {
59
61
  return hasMatchingDescendant(fn.node, searchFilter);
60
62
  }
61
- return (fn.node.file.relativePath.toLowerCase().includes(searchFilter.toLowerCase()) ||
62
- (fn.node.file.summary ?? "").toLowerCase().includes(searchFilter.toLowerCase()));
63
+ const lower = searchFilter.toLowerCase();
64
+ return (fn.node.file.relativePath.toLowerCase().includes(lower) ||
65
+ (fn.node.file.summary ?? "").toLowerCase().includes(lower) ||
66
+ (fn.node.file.description ?? "").toLowerCase().includes(lower));
63
67
  })
64
68
  : flatNodes;
65
69
  // Clamp cursor within visible range.
@@ -75,25 +79,30 @@ export default function App({ root, initialNodes }) {
75
79
  // Computed booleans for the cursor's current position.
76
80
  const cursorOnDir = cursorNode?.node.kind === "dir";
77
81
  const cursorDirExpanded = cursorOnDir && cursorNode.node.kind === "dir" && expandedDirs.has(cursorNode.node.path);
78
- // Load frontmatter for the detail panel whenever selectedFile changes.
82
+ // Load frontmatter + body for the detail panel whenever selectedFile changes.
79
83
  useEffect(() => {
80
84
  if (!selectedFile) {
81
85
  setSelectedFrontmatter(null);
86
+ setSelectedBody("");
82
87
  return;
83
88
  }
84
89
  if (fmCache.has(selectedFile.path)) {
85
- setSelectedFrontmatter(fmCache.get(selectedFile.path) ?? null);
90
+ const cached = fmCache.get(selectedFile.path);
91
+ setSelectedFrontmatter(cached.frontmatter);
92
+ setSelectedBody(cached.body);
86
93
  return;
87
94
  }
88
95
  readFile(selectedFile.path, "utf-8")
89
96
  .then((content) => {
90
- const { frontmatter } = parseFrontmatter(content);
91
- fmCache.set(selectedFile.path, frontmatter);
97
+ const { frontmatter, body } = parseFrontmatter(content);
98
+ fmCache.set(selectedFile.path, { frontmatter, body });
92
99
  setSelectedFrontmatter(frontmatter);
100
+ setSelectedBody(body);
93
101
  })
94
102
  .catch(() => {
95
- fmCache.set(selectedFile.path, null);
103
+ fmCache.set(selectedFile.path, { frontmatter: null, body: "" });
96
104
  setSelectedFrontmatter(null);
105
+ setSelectedBody("");
97
106
  });
98
107
  }, [selectedFile?.path]);
99
108
  /** Load and parse a file for drill-in view. */
@@ -109,12 +118,12 @@ export default function App({ root, initialNodes }) {
109
118
  const content = await readFile(file.path, "utf-8");
110
119
  const { frontmatter, body } = parseFrontmatter(content);
111
120
  const sections = parseBody(body);
112
- const data = { frontmatter, sections };
121
+ const data = { frontmatter, body, sections };
113
122
  drillCache.set(file.path, data);
114
123
  setDrillData(data);
115
124
  }
116
125
  catch {
117
- setDrillData({ frontmatter: null, sections: [] });
126
+ setDrillData({ frontmatter: null, body: "", sections: [] });
118
127
  }
119
128
  }, [drillCache]);
120
129
  /** Toggle deep view on/off, caching shallow results for instant restore. */
@@ -277,6 +286,6 @@ export default function App({ root, initialNodes }) {
277
286
  // Enter is a no-op in drill-in mode.
278
287
  });
279
288
  // --- Layout ---
280
- const treeWidth = Math.floor(columns * 0.38);
281
- return (_jsxs(Box, { flexDirection: "row", width: columns, children: [_jsxs(Box, { flexDirection: "column", width: treeWidth, borderStyle: "single", borderColor: "white", children: [_jsx(Text, { bold: true, children: " Intent Tree" }), deepLoading && _jsx(Text, { color: "yellow", children: " Loading summaries..." }), _jsx(TreePanel, { visibleNodes: visibleNodes, cursorIndex: clampedCursor, searchFilter: searchFilter, isDeepView: isDeepView, expandedDirs: expandedDirs, cursorOnDir: cursorOnDir, cursorDirExpanded: cursorDirExpanded, width: treeWidth - 2 })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "single", borderColor: "white", children: [_jsx(Text, { bold: true, children: " Detail" }), _jsx(DetailPanel, { file: selectedFile, frontmatter: mode === "drill-in" ? (drillData?.frontmatter ?? null) : selectedFrontmatter, mode: mode === "drill-in" ? "drill-in" : "preview", sections: drillData?.sections ?? [], expandedSection: expandedSection, drilledFilePath: drilledFile?.relativePath ?? null })] })] }));
289
+ const treeWidth = Math.floor(columns * 0.57);
290
+ return (_jsxs(Box, { flexDirection: "row", width: columns, children: [_jsxs(Box, { flexDirection: "column", width: treeWidth, borderStyle: "single", borderColor: "white", children: [_jsx(Text, { bold: true, children: " Intent Tree" }), deepLoading && _jsx(Text, { color: "yellow", children: " Loading summaries..." }), _jsx(TreePanel, { visibleNodes: visibleNodes, cursorIndex: clampedCursor, searchFilter: searchFilter, isDeepView: isDeepView, expandedDirs: expandedDirs, cursorOnDir: cursorOnDir, cursorDirExpanded: cursorDirExpanded, width: treeWidth - 2 })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "single", borderColor: "white", children: [_jsx(Text, { bold: true, children: " Detail" }), _jsx(DetailPanel, { file: selectedFile, frontmatter: mode === "drill-in" ? (drillData?.frontmatter ?? null) : selectedFrontmatter, body: mode === "drill-in" ? (drillData?.body ?? "") : selectedBody, mode: mode === "drill-in" ? "drill-in" : "preview", sections: drillData?.sections ?? [], expandedSection: expandedSection, drilledFilePath: drilledFile?.relativePath ?? null })] })] }));
282
291
  }
@@ -3,6 +3,8 @@ import type { AideFile, AideFrontmatter, BodySection } from "../../types/index.j
3
3
  interface DetailPanelProps {
4
4
  file: AideFile | null;
5
5
  frontmatter: AideFrontmatter | null;
6
+ /** Raw body content of the selected or drilled-in file. Used by plan/todo renderers. */
7
+ body: string;
6
8
  /** Controls which rendering mode is active in the right panel. */
7
9
  mode: "preview" | "drill-in";
8
10
  /** Body sections from the drilled-in file. */
@@ -16,9 +18,11 @@ interface DetailPanelProps {
16
18
  * Right-panel component that handles both preview mode (tree navigation) and
17
19
  * drill-in mode (formatted frontmatter card with expandable body sections).
18
20
  *
19
- * In preview mode: scope, truncated intent, and outcome counts.
20
- * In drill-in mode: full frontmatter card (scope heading, full intent, side-by-side
21
- * outcomes) plus expandable body sections cycled with Tab.
21
+ * Delegates to RenderPlanDetail for plan files and RenderTodoDetail for todo files.
22
+ * Intent and research files use the intent-card layout (scope, intent, outcomes).
23
+ *
24
+ * In preview mode: scope, truncated intent, and outcome counts (or plan/todo summary).
25
+ * In drill-in mode: full frontmatter card or plan/todo detail view.
22
26
  */
23
- export default function DetailPanel({ file, frontmatter, mode, sections, expandedSection, drilledFilePath, }: DetailPanelProps): React.ReactElement;
27
+ export default function DetailPanel({ file, frontmatter, body, mode, sections, expandedSection, drilledFilePath, }: DetailPanelProps): React.ReactElement;
24
28
  export {};
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text, useStdout } from "ink";
3
+ import RenderPlanDetail from "./renderPlanDetail/index.js";
4
+ import RenderTodoDetail from "./renderTodoDetail/index.js";
3
5
  /** Truncate a string to maxLen chars, appending "..." if trimmed. */
4
6
  function truncate(s, maxLen) {
5
7
  if (s.length <= maxLen)
@@ -21,11 +23,13 @@ function OutcomesDisplay({ desired, undesired, wide, }) {
21
23
  * Right-panel component that handles both preview mode (tree navigation) and
22
24
  * drill-in mode (formatted frontmatter card with expandable body sections).
23
25
  *
24
- * In preview mode: scope, truncated intent, and outcome counts.
25
- * In drill-in mode: full frontmatter card (scope heading, full intent, side-by-side
26
- * outcomes) plus expandable body sections cycled with Tab.
26
+ * Delegates to RenderPlanDetail for plan files and RenderTodoDetail for todo files.
27
+ * Intent and research files use the intent-card layout (scope, intent, outcomes).
28
+ *
29
+ * In preview mode: scope, truncated intent, and outcome counts (or plan/todo summary).
30
+ * In drill-in mode: full frontmatter card or plan/todo detail view.
27
31
  */
28
- export default function DetailPanel({ file, frontmatter, mode, sections, expandedSection, drilledFilePath, }) {
32
+ export default function DetailPanel({ file, frontmatter, body, mode, sections, expandedSection, drilledFilePath, }) {
29
33
  const { stdout } = useStdout();
30
34
  const columns = stdout?.columns ?? 80;
31
35
  const wide = columns >= 80;
@@ -34,6 +38,14 @@ export default function DetailPanel({ file, frontmatter, mode, sections, expande
34
38
  if (!file) {
35
39
  return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: "gray", children: "Select a file to preview" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "Type to search, [tab] for deep view" }) })] }));
36
40
  }
41
+ // Delegate to type-specific renderers for plan and todo files.
42
+ if (file.type === "plan") {
43
+ return _jsx(RenderPlanDetail, { file: file, frontmatter: frontmatter, mode: "preview", body: body, drilledFilePath: null });
44
+ }
45
+ if (file.type === "todo") {
46
+ const description = frontmatter?.description ?? file.description ?? "";
47
+ return _jsx(RenderTodoDetail, { description: description, body: body, mode: "preview", filePath: file.relativePath });
48
+ }
37
49
  if (!frontmatter) {
38
50
  return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: "red", children: "[FAILED TO PARSE]" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: file.relativePath }) })] }));
39
51
  }
@@ -44,6 +56,14 @@ export default function DetailPanel({ file, frontmatter, mode, sections, expande
44
56
  return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Text, { bold: true, children: ["scope: ", scope] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { wrap: "wrap", children: intent }) }), (desiredCount > 0 || undesiredCount > 0) && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: "green", children: ["\u2713 desired (", desiredCount, ")"] }), _jsxs(Text, { color: "red", children: ["\u2717 undesired (", undesiredCount, ")"] })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "[\u2191\u2193] navigate [enter] drill in [tab] deep view" }) })] }));
45
57
  }
46
58
  // --- Drill-in mode ---
59
+ // Delegate to type-specific renderers for plan and todo files.
60
+ if (file?.type === "plan") {
61
+ return _jsx(RenderPlanDetail, { file: file, frontmatter: frontmatter, mode: "drill-in", body: body, drilledFilePath: drilledFilePath });
62
+ }
63
+ if (file?.type === "todo") {
64
+ const description = frontmatter?.description ?? file?.description ?? "";
65
+ return _jsx(RenderTodoDetail, { description: description, body: body, mode: "drill-in", filePath: drilledFilePath ?? file?.relativePath ?? "" });
66
+ }
47
67
  const title = drilledFilePath ?? "[unknown]";
48
68
  const scope = frontmatter?.scope ?? "[FAILED TO PARSE]";
49
69
  const intent = frontmatter?.intent ?? "[FAILED TO PARSE]";
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import type { AideFile, AideFrontmatter } from "../../../types/index.js";
3
+ interface RenderPlanDetailProps {
4
+ file: AideFile;
5
+ frontmatter: AideFrontmatter | null;
6
+ /** Controls which rendering mode is active in the right panel. */
7
+ mode: "preview" | "drill-in";
8
+ /** Raw body content of the plan file. */
9
+ body: string;
10
+ /** File path shown as the panel title during drill-in, or null in preview mode. */
11
+ drilledFilePath: string | null;
12
+ }
13
+ /**
14
+ * Detail panel renderer for plan files (.aide files of type "plan").
15
+ *
16
+ * Preview mode: description from frontmatter, progress fraction, remaining count.
17
+ * Drill-in mode: grouped checklist items by step heading, completion summary at top.
18
+ */
19
+ export default function RenderPlanDetail({ file, frontmatter, mode, body, drilledFilePath, }: RenderPlanDetailProps): React.ReactElement;
20
+ export {};
@@ -0,0 +1,30 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import parsePlanItems from "./parsePlanItems/index.js";
4
+ /** Renders a compact ASCII progress bar of fixed width. */
5
+ function ProgressBar({ done, total, width }) {
6
+ const filled = total > 0 ? Math.round((done / total) * width) : 0;
7
+ const bar = "█".repeat(filled) + "░".repeat(width - filled);
8
+ return _jsxs(Text, { color: "cyan", children: ["[", bar, "]"] });
9
+ }
10
+ /**
11
+ * Detail panel renderer for plan files (.aide files of type "plan").
12
+ *
13
+ * Preview mode: description from frontmatter, progress fraction, remaining count.
14
+ * Drill-in mode: grouped checklist items by step heading, completion summary at top.
15
+ */
16
+ export default function RenderPlanDetail({ file, frontmatter, mode, body, drilledFilePath, }) {
17
+ const steps = parsePlanItems(body);
18
+ const allItems = steps.flatMap((s) => s.items);
19
+ const doneCount = allItems.filter((i) => i.done).length;
20
+ const totalCount = allItems.length;
21
+ const remainingCount = totalCount - doneCount;
22
+ // --- Preview mode ---
23
+ if (mode === "preview") {
24
+ const description = frontmatter?.description ?? file.description ?? "";
25
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [description !== "" && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "wrap", children: description }) })), _jsxs(Box, { flexDirection: "row", gap: 1, alignItems: "center", children: [_jsx(ProgressBar, { done: doneCount, total: totalCount, width: 20 }), _jsxs(Text, { children: [doneCount, "/", totalCount, " complete"] })] }), remainingCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", children: [remainingCount, " item", remainingCount !== 1 ? "s" : "", " remaining"] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "[\u2191\u2193] navigate [enter] drill in [tab] deep view" }) })] }));
26
+ }
27
+ // --- Drill-in mode ---
28
+ const title = drilledFilePath ?? file.relativePath;
29
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(Text, { bold: true, children: title }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { bold: true, color: doneCount === totalCount && totalCount > 0 ? "green" : "cyan", children: ["Progress: ", doneCount, "/", totalCount, " step", totalCount !== 1 ? "s" : "", " complete"] }) }), steps.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: steps.map((s, si) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [s.step !== "" && (_jsx(Text, { bold: true, children: s.step })), s.items.map((item, ii) => (_jsxs(Box, { flexDirection: "row", marginLeft: s.step !== "" ? 2 : 0, children: [item.done ? (_jsx(Text, { color: "green", children: "\u2713 " })) : (_jsx(Text, { color: "gray", children: "\u25CB " })), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: item.done ? "gray" : undefined, wrap: "wrap", children: item.text }) })] }, ii)))] }, si))) })), totalCount === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "No checklist items found." }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "[esc] back [e] open in editor" }) })] }));
30
+ }
@@ -0,0 +1,32 @@
1
+ /** A single checklist item within a plan step. */
2
+ export interface PlanItem {
3
+ text: string;
4
+ done: boolean;
5
+ }
6
+ /** A plan step grouping with its heading label and checklist items. */
7
+ export interface PlanStep {
8
+ step: string;
9
+ done: boolean;
10
+ items: PlanItem[];
11
+ }
12
+ /**
13
+ * Parse a plan file body string into structured step groups with checklist items.
14
+ *
15
+ * Recognises three heading formats found in this project's plan files:
16
+ *
17
+ * 1. `### N. [x] Step title` — numbered with inline checkbox (cli/plan.aide)
18
+ * 2. `### N. Step title` — numbered without inline checkbox
19
+ * 3. `### Phase N — Title` — Phase-prefixed with em-dash (init/plan.aide)
20
+ *
21
+ * Checklist items below a heading are lines matching `- [x]` or `- [ ]`.
22
+ * Prose items (non-checklist bullet lines) and all other non-heading lines
23
+ * are ignored. Items that appear before any heading are grouped under an
24
+ * empty-string step.
25
+ *
26
+ * The `done` field on a `PlanStep` is determined as follows:
27
+ * - If the heading carries an inline `[x]`/`[ ]` marker (format 1), that
28
+ * marker is authoritative — `done` is not overridden by item states.
29
+ * - Otherwise `done` is derived: true when all contained items are done,
30
+ * false when any item is undone or the step has no items.
31
+ */
32
+ export default function parsePlanItems(body: string): PlanStep[];
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Parse a plan file body string into structured step groups with checklist items.
3
+ *
4
+ * Recognises three heading formats found in this project's plan files:
5
+ *
6
+ * 1. `### N. [x] Step title` — numbered with inline checkbox (cli/plan.aide)
7
+ * 2. `### N. Step title` — numbered without inline checkbox
8
+ * 3. `### Phase N — Title` — Phase-prefixed with em-dash (init/plan.aide)
9
+ *
10
+ * Checklist items below a heading are lines matching `- [x]` or `- [ ]`.
11
+ * Prose items (non-checklist bullet lines) and all other non-heading lines
12
+ * are ignored. Items that appear before any heading are grouped under an
13
+ * empty-string step.
14
+ *
15
+ * The `done` field on a `PlanStep` is determined as follows:
16
+ * - If the heading carries an inline `[x]`/`[ ]` marker (format 1), that
17
+ * marker is authoritative — `done` is not overridden by item states.
18
+ * - Otherwise `done` is derived: true when all contained items are done,
19
+ * false when any item is undone or the step has no items.
20
+ */
21
+ export default function parsePlanItems(body) {
22
+ const lines = body.split("\n");
23
+ const steps = [];
24
+ let current = null;
25
+ for (const raw of lines) {
26
+ const line = raw.trim();
27
+ // Format 1: ### N. [x] Step title (inline checkbox in heading)
28
+ const inlineCheckHeading = line.match(/^###\s+\d+\.\s+\[(x| )\]\s+(.+)$/i);
29
+ if (inlineCheckHeading) {
30
+ current = {
31
+ step: inlineCheckHeading[2].trim(),
32
+ done: inlineCheckHeading[1].toLowerCase() === "x",
33
+ items: [],
34
+ _headingCheckbox: true,
35
+ };
36
+ steps.push(current);
37
+ continue;
38
+ }
39
+ // Format 2: ### N. Step title (no inline checkbox)
40
+ const numberedHeading = line.match(/^###\s+\d+\.\s+(.+)$/);
41
+ if (numberedHeading) {
42
+ current = { step: numberedHeading[1].trim(), done: false, items: [], _headingCheckbox: false };
43
+ steps.push(current);
44
+ continue;
45
+ }
46
+ // Format 3: ### Phase N — Title (Phase prefix with em-dash, en-dash, or hyphen)
47
+ const phaseHeading = line.match(/^###\s+Phase\s+\d+\s+[—–-]+\s+(.+)$/i);
48
+ if (phaseHeading) {
49
+ current = { step: phaseHeading[1].trim(), done: false, items: [], _headingCheckbox: false };
50
+ steps.push(current);
51
+ continue;
52
+ }
53
+ const checkedMatch = line.match(/^-\s+\[x\]\s+(.+)$/i);
54
+ if (checkedMatch) {
55
+ if (!current) {
56
+ current = { step: "", done: false, items: [], _headingCheckbox: false };
57
+ steps.push(current);
58
+ }
59
+ current.items.push({ text: checkedMatch[1].trim(), done: true });
60
+ continue;
61
+ }
62
+ const uncheckedMatch = line.match(/^-\s+\[ \]\s+(.+)$/);
63
+ if (uncheckedMatch) {
64
+ if (!current) {
65
+ current = { step: "", done: false, items: [], _headingCheckbox: false };
66
+ steps.push(current);
67
+ }
68
+ current.items.push({ text: uncheckedMatch[1].trim(), done: false });
69
+ }
70
+ }
71
+ // Derive done for steps whose heading had no inline checkbox:
72
+ // true only when all items are done and at least one item exists.
73
+ for (const step of steps) {
74
+ if (!step._headingCheckbox && step.items.length > 0) {
75
+ step.done = step.items.every((item) => item.done);
76
+ }
77
+ }
78
+ // Strip the internal field before returning.
79
+ return steps.map(({ _headingCheckbox: _hc, ...rest }) => rest);
80
+ }
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ interface RenderTodoDetailProps {
3
+ /** Frontmatter description field, shown as the preview summary. */
4
+ description: string;
5
+ /** Raw body content of the todo file. */
6
+ body: string;
7
+ /** Controls which rendering mode is active. */
8
+ mode: "preview" | "drill-in";
9
+ /** File path, used as the drill-in panel title. */
10
+ filePath: string;
11
+ }
12
+ /**
13
+ * Renders a todo.aide file in preview mode (summary counts) or drill-in mode
14
+ * (full issue list with misalignment annotations highlighted and completion state).
15
+ */
16
+ export default function RenderTodoDetail({ description, body, mode, filePath, }: RenderTodoDetailProps): React.ReactElement;
17
+ export {};
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import parseTodoItems from "./parseTodoItems/index.js";
4
+ /**
5
+ * Renders a todo.aide file in preview mode (summary counts) or drill-in mode
6
+ * (full issue list with misalignment annotations highlighted and completion state).
7
+ */
8
+ export default function RenderTodoDetail({ description, body, mode, filePath, }) {
9
+ const items = parseTodoItems(body);
10
+ const totalCount = items.length;
11
+ const doneCount = items.filter((item) => item.done).length;
12
+ const openCount = totalCount - doneCount;
13
+ const misalignmentCount = items.filter((item) => item.misalignment !== undefined).length;
14
+ if (mode === "preview") {
15
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [description ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "wrap", children: description }) })) : null, _jsxs(Text, { children: ["Issues:", " ", _jsxs(Text, { color: openCount > 0 ? "yellow" : "green", children: [openCount, " open"] }), ", ", _jsxs(Text, { color: "green", children: [doneCount, " resolved"] }), " (", _jsxs(Text, { children: [totalCount, " total"] }), ")"] }), misalignmentCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u2691 ", misalignmentCount, " misalignment annotation", misalignmentCount !== 1 ? "s" : ""] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "[\u2191\u2193] navigate [enter] drill in [tab] deep view" }) })] }));
16
+ }
17
+ // Drill-in mode — full issue list
18
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(Text, { bold: true, children: filePath }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Issues:", " ", _jsxs(Text, { color: openCount > 0 ? "yellow" : "green", children: [openCount, " open"] }), ", ", _jsxs(Text, { color: "green", children: [doneCount, " resolved"] }), " (", _jsxs(Text, { children: [totalCount, " total"] }), ")"] }) }), items.length === 0 ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "No checklist items found." }) })) : (_jsx(Box, { flexDirection: "column", marginTop: 1, children: items.map((item, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", children: [item.done ? (_jsx(Text, { color: "green", children: "\u2713 " })) : (_jsx(Text, { color: "gray", children: "\u25CB " })), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: item.misalignment !== undefined ? "yellow" : undefined, wrap: "wrap", children: item.text }) })] }), item.misalignment !== undefined && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "red", children: ["\u2691 Misalignment: ", item.misalignment] }) }))] }, i))) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "[esc] back [e] open in editor" }) })] }));
19
+ }
@@ -0,0 +1,26 @@
1
+ /** A single parsed checklist item from a todo.aide body. */
2
+ export interface TodoItem {
3
+ /** The main text of the checklist item (first line only, not continuation lines). */
4
+ text: string;
5
+ /** Whether the item is checked (`- [x]`). */
6
+ done: boolean;
7
+ /**
8
+ * The Misalignment annotation extracted from a continuation line, if present.
9
+ * e.g. "implementation-drift" from a line like " Misalignment: implementation-drift"
10
+ */
11
+ misalignment?: string;
12
+ }
13
+ /**
14
+ * Parses the body of a todo.aide file into a structured array of checklist items.
15
+ *
16
+ * Format recognised:
17
+ * - `- [x] item text` — completed item
18
+ * - `- [ ] item text` — open item
19
+ * Continuation lines (indented lines that follow a checklist item) are scanned
20
+ * for a `Misalignment:` annotation. The first such annotation found is attached
21
+ * to the preceding item. All other continuation lines are ignored.
22
+ *
23
+ * Lines that are not checklist items and not continuations of a checklist item
24
+ * (e.g. `##` headings, blank lines, Retro sections) are ignored.
25
+ */
26
+ export default function parseTodoItems(body: string): TodoItem[];
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Parses the body of a todo.aide file into a structured array of checklist items.
3
+ *
4
+ * Format recognised:
5
+ * - `- [x] item text` — completed item
6
+ * - `- [ ] item text` — open item
7
+ * Continuation lines (indented lines that follow a checklist item) are scanned
8
+ * for a `Misalignment:` annotation. The first such annotation found is attached
9
+ * to the preceding item. All other continuation lines are ignored.
10
+ *
11
+ * Lines that are not checklist items and not continuations of a checklist item
12
+ * (e.g. `##` headings, blank lines, Retro sections) are ignored.
13
+ */
14
+ export default function parseTodoItems(body) {
15
+ const lines = body.split("\n");
16
+ const items = [];
17
+ let current = null;
18
+ for (const line of lines) {
19
+ const checkboxMatch = line.match(/^- \[(x| )\] (.+)/);
20
+ if (checkboxMatch) {
21
+ // Flush previous item before starting a new one
22
+ if (current)
23
+ items.push(current);
24
+ current = {
25
+ text: checkboxMatch[2].trim(),
26
+ done: checkboxMatch[1] === "x",
27
+ };
28
+ continue;
29
+ }
30
+ // Continuation line — only meaningful after a checklist item
31
+ if (current && line.match(/^\s+/)) {
32
+ const misalignmentMatch = line.match(/\bMisalignment:\s*(.+)/);
33
+ if (misalignmentMatch && !current.misalignment) {
34
+ current.misalignment = misalignmentMatch[1].trim();
35
+ }
36
+ continue;
37
+ }
38
+ // Non-item, non-continuation line — flush any pending item
39
+ if (current) {
40
+ items.push(current);
41
+ current = null;
42
+ }
43
+ }
44
+ // Flush the last item if body ended while an item was open
45
+ if (current)
46
+ items.push(current);
47
+ return items;
48
+ }
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  /** Type-tag display labels keyed by AideFileType. */
4
4
  const TYPE_TAG = {
@@ -22,7 +22,24 @@ function renderRow(flatNode, index, cursorIndex, isDeepView, expandedDirs, width
22
22
  if (node.kind === "dir") {
23
23
  const label = node.path === "." ? ". /" : `${node.path}/`;
24
24
  const expandIndicator = expandedDirs.has(node.path) ? "v " : "> ";
25
- return (_jsx(Box, { children: _jsxs(Text, { bold: isCursor, color: isCursor ? "white" : "gray", children: [indent, expandIndicator, label] }) }, `dir-${node.path}-${index}`));
25
+ // Find the first intent child to surface its status and description on the dir row.
26
+ const intentChild = node.children.find((c) => c.kind === "file" && c.file.type === "intent");
27
+ const intentFile = intentChild?.kind === "file" ? intentChild.file : null;
28
+ const dirStatusBadge = intentFile?.status === "aligned"
29
+ ? { text: " [aligned]", color: "green" }
30
+ : intentFile?.status === "misaligned"
31
+ ? { text: " [misaligned]", color: "red" }
32
+ : null;
33
+ const dirDescription = intentFile?.description ?? "";
34
+ // Truncate description to fit within available panel width.
35
+ const dirFixedLen = indent.length + expandIndicator.length + label.length + (dirStatusBadge?.text.length ?? 0);
36
+ const dirRemaining = width - dirFixedLen;
37
+ let dirSummaryText = "";
38
+ if (dirDescription && dirRemaining > 10) {
39
+ const full = ` — ${dirDescription}`;
40
+ dirSummaryText = full.length <= dirRemaining ? full : `${full.slice(0, dirRemaining - 3)}…`;
41
+ }
42
+ return (_jsx(Box, { children: _jsxs(Text, { bold: isCursor, color: isCursor ? "white" : "gray", wrap: "truncate", children: [indent, expandIndicator, label, dirStatusBadge ? _jsx(Text, { color: dirStatusBadge.color, children: dirStatusBadge.text }) : null, dirSummaryText ? _jsx(Text, { color: "gray", children: dirSummaryText }) : null] }) }, `dir-${node.path}-${index}`));
26
43
  }
27
44
  // File node — render as a single <Text> to prevent Ink from wrapping mid-element.
28
45
  const { file } = node;
@@ -32,15 +49,26 @@ function renderRow(flatNode, index, cursorIndex, isDeepView, expandedDirs, width
32
49
  const tagColor = TYPE_COLOR[file.type] ?? "white";
33
50
  const prefix = `${indent}${connector}${filename} `;
34
51
  const tagStr = `[${tag}]`;
35
- // Truncate summary to fit within available panel width.
36
- const fixedLen = prefix.length + tagStr.length;
52
+ // Determine status badge text and color when a status flag is present.
53
+ const statusBadge = file.status
54
+ ? file.status === "aligned"
55
+ ? { text: " [aligned]", color: "green" }
56
+ : { text: " [misaligned]", color: "red" }
57
+ : null;
58
+ // Truncate summary/description to fit within available panel width.
59
+ const statusLen = statusBadge ? statusBadge.text.length : 0;
60
+ const fixedLen = prefix.length + tagStr.length + statusLen;
37
61
  const remaining = width - fixedLen;
38
- let summary = "";
39
- if (isDeepView && file.summary && remaining > 10) {
62
+ let summaryText = "";
63
+ if (file.description && remaining > 10) {
64
+ const full = ` — ${file.description}`;
65
+ summaryText = full.length <= remaining ? full : `${full.slice(0, remaining - 3)}…`;
66
+ }
67
+ else if (!file.description && isDeepView && file.summary && remaining > 10) {
40
68
  const full = ` — ${file.summary}`;
41
- summary = full.length <= remaining ? full : `${full.slice(0, remaining - 3)}…`;
69
+ summaryText = full.length <= remaining ? full : `${full.slice(0, remaining - 3)}…`;
42
70
  }
43
- return (_jsx(Box, { children: _jsxs(Text, { bold: isCursor, backgroundColor: isCursor ? "blue" : undefined, wrap: "truncate", children: [prefix, _jsx(Text, { color: tagColor, children: tagStr }), summary ? _jsx(Text, { color: "gray", children: summary }) : null] }) }, `file-${file.relativePath}-${index}`));
71
+ return (_jsx(Box, { children: _jsxs(Text, { bold: isCursor, backgroundColor: isCursor ? "blue" : undefined, wrap: "truncate", children: [prefix, _jsx(Text, { color: tagColor, children: tagStr }), statusBadge ? _jsx(Text, { color: statusBadge.color, children: statusBadge.text }) : null, summaryText ? _jsx(Text, { color: "gray", children: summaryText }) : null] }) }, `file-${file.relativePath}-${index}`));
44
72
  }
45
73
  /**
46
74
  * Renders the left-panel tree of .aide files with cursor highlighting and optional summaries.
@@ -76,7 +76,7 @@ export default async function buildAncestorChain(root, targetPath) {
76
76
  if (!spec)
77
77
  continue;
78
78
  const { frontmatter } = parseFrontmatter(spec.content);
79
- const description = frontmatter?.description;
79
+ const description = frontmatter?.description || (frontmatter?.intent ? frontmatter.intent.split(/[.\n]/)[0] : undefined);
80
80
  const status = frontmatter?.status;
81
81
  // Compute a display path relative to the project root
82
82
  const rel = relative(absRoot, spec.specPath).replace(/\\/g, "/");
@@ -39,6 +39,10 @@ export interface AideFile {
39
39
  type: AideFileType;
40
40
  /** First ~80 chars of the first paragraph, for tree summaries. */
41
41
  summary: string;
42
+ /** Frontmatter description field — one-line human-readable summary. Empty string when absent. */
43
+ description?: string;
44
+ /** Alignment status from frontmatter — present only when explicitly set. */
45
+ status?: "aligned" | "misaligned";
42
46
  }
43
47
  /** Result of scanning a directory tree for .aide files. */
44
48
  export interface ScanResult {
@@ -2,6 +2,8 @@ import type { ScanResult } from "../../types/index.js";
2
2
  /**
3
3
  * Recursively walk the filesystem from `root` and collect all .aide files.
4
4
  * Skips node_modules, .git, dist, build, .next, coverage, __pycache__.
5
- * Reads the first ~1000 bytes of each file to extract the first meaningful body line as summary.
5
+ * In deep mode: reads full content to extract summary, description, and status.
6
+ * In shallow mode: reads only the first ~500 bytes per file to extract frontmatter
7
+ * description and status — summary stays empty but descriptions appear unconditionally.
6
8
  */
7
9
  export default function scan(root: string, path?: string, shallow?: boolean): Promise<ScanResult>;
@@ -2,6 +2,7 @@ import { readdir, readFile } from "node:fs/promises";
2
2
  import { join, relative } from "node:path";
3
3
  import { classifyFile } from "../../util/classify/index.js";
4
4
  import { SKIP_DIRS } from "../../types/index.js";
5
+ import parseFrontmatter from "../../util/parseFrontmatter/index.js";
5
6
  /** Extract the first meaningful body line as the summary, truncated to ~80 chars. */
6
7
  function extractSummary(content) {
7
8
  const lines = content.split("\n");
@@ -32,6 +33,17 @@ function extractSummary(content) {
32
33
  function toPosix(p) {
33
34
  return p.split("\\").join("/");
34
35
  }
36
+ /**
37
+ * Derive a description from frontmatter, falling back to the first sentence of
38
+ * `intent` when `description` is absent — matching the logic in buildAncestorChain.
39
+ */
40
+ function deriveDescription(frontmatter) {
41
+ if (frontmatter?.description)
42
+ return frontmatter.description;
43
+ if (frontmatter?.intent)
44
+ return frontmatter.intent.split(/[.\n]/)[0] ?? "";
45
+ return "";
46
+ }
35
47
  /** Recursively walk a directory and collect all .aide files. */
36
48
  async function walk(dir, root, files, shallow) {
37
49
  let entries;
@@ -52,10 +64,31 @@ async function walk(dir, root, files, shallow) {
52
64
  if (!entry.name.endsWith(".aide"))
53
65
  continue;
54
66
  let summary = "";
67
+ let description = "";
68
+ let status;
55
69
  if (!shallow) {
56
70
  try {
57
71
  const buf = await readFile(fullPath, { encoding: "utf-8" });
58
72
  summary = extractSummary(buf.slice(0, 1000));
73
+ const { frontmatter } = parseFrontmatter(buf);
74
+ description = deriveDescription(frontmatter);
75
+ if (frontmatter?.status)
76
+ status = frontmatter.status;
77
+ }
78
+ catch {
79
+ // skip unreadable files
80
+ }
81
+ }
82
+ else {
83
+ // In shallow mode, read only the first ~500 bytes to capture frontmatter
84
+ // without loading the full body — keeps startup fast for large projects.
85
+ try {
86
+ const buf = await readFile(fullPath, { encoding: "utf-8" });
87
+ const head = buf.slice(0, 500);
88
+ const { frontmatter } = parseFrontmatter(head);
89
+ description = deriveDescription(frontmatter);
90
+ if (frontmatter?.status)
91
+ status = frontmatter.status;
59
92
  }
60
93
  catch {
61
94
  // skip unreadable files
@@ -66,13 +99,17 @@ async function walk(dir, root, files, shallow) {
66
99
  relativePath: toPosix(relative(root, fullPath)),
67
100
  type: classifyFile(entry.name),
68
101
  summary,
102
+ description,
103
+ status,
69
104
  });
70
105
  }
71
106
  }
72
107
  /**
73
108
  * Recursively walk the filesystem from `root` and collect all .aide files.
74
109
  * Skips node_modules, .git, dist, build, .next, coverage, __pycache__.
75
- * Reads the first ~1000 bytes of each file to extract the first meaningful body line as summary.
110
+ * In deep mode: reads full content to extract summary, description, and status.
111
+ * In shallow mode: reads only the first ~500 bytes per file to extract frontmatter
112
+ * description and status — summary stays empty but descriptions appear unconditionally.
76
113
  */
77
114
  export default async function scan(root, path, shallow) {
78
115
  const scanRoot = path ? join(root, path) : root;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aidemd-mcp/server",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server that teaches any agent the AIDE methodology through tool descriptions and progressive disclosure tooling",
5
5
  "type": "module",
6
6
  "bin": {