@gethmy/mcp 2.1.0 → 2.1.1
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/README.md +5 -0
- package/dist/cli.js +177 -14
- package/dist/index.js +2 -0
- package/dist/lib/api-client.js +2 -0
- package/dist/lib/tui/docs.js +207 -21
- package/package.json +5 -4
- package/src/api-client.ts +6 -0
- package/src/tui/docs.ts +241 -28
package/README.md
CHANGED
|
@@ -20,6 +20,11 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor, Windsurf) to intera
|
|
|
20
20
|
- **Smart Setup** - one command configures everything
|
|
21
21
|
- **API Key Authentication** - no database credentials required
|
|
22
22
|
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- [Node.js](https://nodejs.org) >= 20 or [Bun](https://bun.sh) >= 1.0
|
|
26
|
+
- A [Harmony](https://gethmy.com) account with an API key
|
|
27
|
+
|
|
23
28
|
## Quick Start
|
|
24
29
|
|
|
25
30
|
### 1. Get an API Key
|
package/dist/cli.js
CHANGED
|
@@ -26301,6 +26301,8 @@ class HarmonyApiClient {
|
|
|
26301
26301
|
params.set("summary", "true");
|
|
26302
26302
|
if (options?.includeArchived)
|
|
26303
26303
|
params.set("include_archived", "true");
|
|
26304
|
+
if (options?.labelName)
|
|
26305
|
+
params.set("label_name", options.labelName);
|
|
26304
26306
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
26305
26307
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
26306
26308
|
}
|
|
@@ -31520,13 +31522,8 @@ function detectAgents(cwd = process.cwd()) {
|
|
|
31520
31522
|
}
|
|
31521
31523
|
|
|
31522
31524
|
// src/tui/docs.ts
|
|
31523
|
-
import {
|
|
31524
|
-
|
|
31525
|
-
readFileSync as readFileSync3,
|
|
31526
|
-
readdirSync as readdirSync2,
|
|
31527
|
-
statSync
|
|
31528
|
-
} from "node:fs";
|
|
31529
|
-
import { join as join4 } from "node:path";
|
|
31525
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
31526
|
+
import { isAbsolute, join as join4, resolve, sep as sep2 } from "node:path";
|
|
31530
31527
|
|
|
31531
31528
|
// src/tui/theme.ts
|
|
31532
31529
|
var pc = __toESM(require_picocolors(), 1);
|
|
@@ -31932,16 +31929,52 @@ function generateArchitectureMd(info, _cwd) {
|
|
|
31932
31929
|
return lines.join(`
|
|
31933
31930
|
`);
|
|
31934
31931
|
}
|
|
31932
|
+
var VAGUE_STANDARDS = [
|
|
31933
|
+
"follow best practices",
|
|
31934
|
+
"use best practices",
|
|
31935
|
+
"keep it clean",
|
|
31936
|
+
"write clean code",
|
|
31937
|
+
"maintain code quality",
|
|
31938
|
+
"ensure quality",
|
|
31939
|
+
"use proper naming",
|
|
31940
|
+
"follow conventions",
|
|
31941
|
+
"be consistent"
|
|
31942
|
+
];
|
|
31935
31943
|
function verifyDocs(cwd) {
|
|
31936
31944
|
const issues = [];
|
|
31937
31945
|
const claudeMd = readText(join4(cwd, "CLAUDE.md"));
|
|
31946
|
+
const agentsMd = readText(join4(cwd, "AGENTS.md"));
|
|
31947
|
+
const pkg = readJson(join4(cwd, "package.json"));
|
|
31948
|
+
const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
|
|
31949
|
+
const projectRoot = resolve(cwd);
|
|
31938
31950
|
if (claudeMd) {
|
|
31951
|
+
const importedFiles = [];
|
|
31939
31952
|
for (const line of claudeMd.split(`
|
|
31940
31953
|
`)) {
|
|
31941
31954
|
const match = line.match(/^@(.+)$/);
|
|
31942
31955
|
if (match) {
|
|
31943
31956
|
const refPath = match[1].trim();
|
|
31944
|
-
if (
|
|
31957
|
+
if (isAbsolute(refPath)) {
|
|
31958
|
+
issues.push({
|
|
31959
|
+
severity: "error",
|
|
31960
|
+
file: "CLAUDE.md",
|
|
31961
|
+
message: `@ reference uses an absolute path: ${refPath}`,
|
|
31962
|
+
fix: "Use a project-relative path under the repository root"
|
|
31963
|
+
});
|
|
31964
|
+
continue;
|
|
31965
|
+
}
|
|
31966
|
+
const resolvedPath = resolve(projectRoot, refPath);
|
|
31967
|
+
if (resolvedPath !== projectRoot && !resolvedPath.startsWith(projectRoot + sep2)) {
|
|
31968
|
+
issues.push({
|
|
31969
|
+
severity: "error",
|
|
31970
|
+
file: "CLAUDE.md",
|
|
31971
|
+
message: `@ reference escapes project root: ${refPath}`,
|
|
31972
|
+
fix: "Remove traversal segments (../) and keep references inside the repo"
|
|
31973
|
+
});
|
|
31974
|
+
continue;
|
|
31975
|
+
}
|
|
31976
|
+
importedFiles.push({ ref: refPath, resolved: resolvedPath });
|
|
31977
|
+
if (!existsSync4(resolvedPath)) {
|
|
31945
31978
|
issues.push({
|
|
31946
31979
|
severity: "error",
|
|
31947
31980
|
file: "CLAUDE.md",
|
|
@@ -31951,11 +31984,72 @@ function verifyDocs(cwd) {
|
|
|
31951
31984
|
}
|
|
31952
31985
|
}
|
|
31953
31986
|
}
|
|
31987
|
+
const claudeLines = claudeMd.split(`
|
|
31988
|
+
`).length;
|
|
31989
|
+
if (claudeLines > 100) {
|
|
31990
|
+
issues.push({
|
|
31991
|
+
severity: "warning",
|
|
31992
|
+
file: "CLAUDE.md",
|
|
31993
|
+
message: `CLAUDE.md is ${claudeLines} lines (recommended: under 100)`,
|
|
31994
|
+
fix: "Move detailed content to AGENTS.md or docs/ files and use @imports"
|
|
31995
|
+
});
|
|
31996
|
+
}
|
|
31997
|
+
if (importedFiles.length > 0) {
|
|
31998
|
+
const claudeHeadings = extractHeadings(claudeMd);
|
|
31999
|
+
for (const { ref: refPath, resolved: resolvedPath } of importedFiles) {
|
|
32000
|
+
const refContent = readText(resolvedPath);
|
|
32001
|
+
if (!refContent)
|
|
32002
|
+
continue;
|
|
32003
|
+
const refHeadings = extractHeadings(refContent);
|
|
32004
|
+
for (const heading of claudeHeadings) {
|
|
32005
|
+
if (refHeadings.has(heading)) {
|
|
32006
|
+
issues.push({
|
|
32007
|
+
severity: "warning",
|
|
32008
|
+
file: "CLAUDE.md",
|
|
32009
|
+
message: `Section "${heading}" duplicates content from @${refPath}`,
|
|
32010
|
+
fix: `Remove the "${heading}" section — it's already included via @import`
|
|
32011
|
+
});
|
|
32012
|
+
}
|
|
32013
|
+
}
|
|
32014
|
+
}
|
|
32015
|
+
}
|
|
31954
32016
|
}
|
|
31955
|
-
const agentsMd = readText(join4(cwd, "AGENTS.md"));
|
|
31956
|
-
const pkg = readJson(join4(cwd, "package.json"));
|
|
31957
|
-
const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
|
|
31958
32017
|
if (agentsMd) {
|
|
32018
|
+
const agentsLines = agentsMd.split(`
|
|
32019
|
+
`);
|
|
32020
|
+
const contextLines = [];
|
|
32021
|
+
let pastFirstHeading = false;
|
|
32022
|
+
let hitNextSection = false;
|
|
32023
|
+
for (const line of agentsLines) {
|
|
32024
|
+
if (!pastFirstHeading) {
|
|
32025
|
+
if (line.startsWith("# ")) {
|
|
32026
|
+
pastFirstHeading = true;
|
|
32027
|
+
}
|
|
32028
|
+
continue;
|
|
32029
|
+
}
|
|
32030
|
+
if (line.startsWith("## ")) {
|
|
32031
|
+
hitNextSection = true;
|
|
32032
|
+
break;
|
|
32033
|
+
}
|
|
32034
|
+
const trimmed = line.trim();
|
|
32035
|
+
if (trimmed)
|
|
32036
|
+
contextLines.push(trimmed);
|
|
32037
|
+
}
|
|
32038
|
+
if (pastFirstHeading && !hitNextSection && contextLines.length === 0) {
|
|
32039
|
+
issues.push({
|
|
32040
|
+
severity: "warning",
|
|
32041
|
+
file: "AGENTS.md",
|
|
32042
|
+
message: "Missing project context line after the title heading",
|
|
32043
|
+
fix: "Add a single-line description: stack + what the project does"
|
|
32044
|
+
});
|
|
32045
|
+
} else if (contextLines.length > 1) {
|
|
32046
|
+
issues.push({
|
|
32047
|
+
severity: "warning",
|
|
32048
|
+
file: "AGENTS.md",
|
|
32049
|
+
message: `Project context should be exactly 1 line, found ${contextLines.length}`,
|
|
32050
|
+
fix: "Condense to a single line: stack + what the project does"
|
|
32051
|
+
});
|
|
32052
|
+
}
|
|
31959
32053
|
const codeBlockRe = /```[\s\S]*?```/g;
|
|
31960
32054
|
let blockMatch;
|
|
31961
32055
|
while ((blockMatch = codeBlockRe.exec(agentsMd)) !== null) {
|
|
@@ -31964,7 +32058,16 @@ function verifyDocs(cwd) {
|
|
|
31964
32058
|
let cmdMatch;
|
|
31965
32059
|
while ((cmdMatch = cmdRe.exec(block)) !== null) {
|
|
31966
32060
|
const scriptName = cmdMatch[1];
|
|
31967
|
-
const builtins = new Set([
|
|
32061
|
+
const builtins = new Set([
|
|
32062
|
+
"install",
|
|
32063
|
+
"init",
|
|
32064
|
+
"create",
|
|
32065
|
+
"exec",
|
|
32066
|
+
"dlx",
|
|
32067
|
+
"x",
|
|
32068
|
+
"test",
|
|
32069
|
+
"start"
|
|
32070
|
+
]);
|
|
31968
32071
|
if (builtins.has(scriptName))
|
|
31969
32072
|
continue;
|
|
31970
32073
|
if (Object.keys(pkgScripts).length > 0 && !(scriptName in pkgScripts)) {
|
|
@@ -31977,6 +32080,34 @@ function verifyDocs(cwd) {
|
|
|
31977
32080
|
}
|
|
31978
32081
|
}
|
|
31979
32082
|
}
|
|
32083
|
+
const standardsSection = extractSection(agentsMd, "Code Standards");
|
|
32084
|
+
if (standardsSection) {
|
|
32085
|
+
const lower = standardsSection.toLowerCase();
|
|
32086
|
+
for (const phrase of VAGUE_STANDARDS) {
|
|
32087
|
+
if (lower.includes(phrase)) {
|
|
32088
|
+
issues.push({
|
|
32089
|
+
severity: "warning",
|
|
32090
|
+
file: "AGENTS.md",
|
|
32091
|
+
message: `Code Standards contains vague phrase: "${phrase}"`,
|
|
32092
|
+
fix: "Replace with specific, verifiable conventions derived from config files"
|
|
32093
|
+
});
|
|
32094
|
+
}
|
|
32095
|
+
}
|
|
32096
|
+
}
|
|
32097
|
+
if (Object.keys(pkgScripts).length > 0) {
|
|
32098
|
+
const hasTestScript = Object.keys(pkgScripts).some((k3) => k3 === "test" || k3.startsWith("test:"));
|
|
32099
|
+
if (!hasTestScript) {
|
|
32100
|
+
const mentionsNoTest = agentsMd.toLowerCase().includes("no test");
|
|
32101
|
+
if (!mentionsNoTest) {
|
|
32102
|
+
issues.push({
|
|
32103
|
+
severity: "warning",
|
|
32104
|
+
file: "AGENTS.md",
|
|
32105
|
+
message: "No test script in package.json and AGENTS.md doesn't mention it",
|
|
32106
|
+
fix: 'Add a note like "No test framework. Verify changes with `bun run build`."'
|
|
32107
|
+
});
|
|
32108
|
+
}
|
|
32109
|
+
}
|
|
32110
|
+
}
|
|
31980
32111
|
checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
|
|
31981
32112
|
}
|
|
31982
32113
|
const archMd = readText(join4(cwd, "docs", "architecture.md"));
|
|
@@ -31985,16 +32116,48 @@ function verifyDocs(cwd) {
|
|
|
31985
32116
|
}
|
|
31986
32117
|
return issues;
|
|
31987
32118
|
}
|
|
32119
|
+
function extractHeadings(content) {
|
|
32120
|
+
const headings = new Set;
|
|
32121
|
+
for (const line of content.split(`
|
|
32122
|
+
`)) {
|
|
32123
|
+
const match = line.match(/^#{2,3}\s+(.+)$/);
|
|
32124
|
+
if (match) {
|
|
32125
|
+
headings.add(match[1].trim());
|
|
32126
|
+
}
|
|
32127
|
+
}
|
|
32128
|
+
return headings;
|
|
32129
|
+
}
|
|
32130
|
+
function extractSection(content, heading) {
|
|
32131
|
+
const lines = content.split(`
|
|
32132
|
+
`);
|
|
32133
|
+
let capturing = false;
|
|
32134
|
+
const result = [];
|
|
32135
|
+
for (const line of lines) {
|
|
32136
|
+
if (capturing) {
|
|
32137
|
+
if (line.match(/^#{1,2}\s/))
|
|
32138
|
+
break;
|
|
32139
|
+
result.push(line);
|
|
32140
|
+
} else if (line.match(new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i"))) {
|
|
32141
|
+
capturing = true;
|
|
32142
|
+
}
|
|
32143
|
+
}
|
|
32144
|
+
return result.length > 0 ? result.join(`
|
|
32145
|
+
`) : null;
|
|
32146
|
+
}
|
|
31988
32147
|
function checkBacktickPaths(content, file2, cwd, issues) {
|
|
31989
32148
|
const pathRe = /`((?:src\/|packages\/|apps\/|supabase\/|docs\/)[^`]+)`/g;
|
|
31990
32149
|
let match;
|
|
31991
32150
|
const checked = new Set;
|
|
32151
|
+
const root = resolve(cwd);
|
|
31992
32152
|
while ((match = pathRe.exec(content)) !== null) {
|
|
31993
32153
|
const refPath = match[1].replace(/\/$/, "");
|
|
31994
32154
|
if (checked.has(refPath))
|
|
31995
32155
|
continue;
|
|
31996
32156
|
checked.add(refPath);
|
|
31997
|
-
|
|
32157
|
+
const resolvedRef = resolve(root, refPath);
|
|
32158
|
+
if (resolvedRef !== root && !resolvedRef.startsWith(root + sep2))
|
|
32159
|
+
continue;
|
|
32160
|
+
if (!existsSync4(resolvedRef)) {
|
|
31998
32161
|
issues.push({
|
|
31999
32162
|
severity: "warning",
|
|
32000
32163
|
file: file2,
|
|
@@ -32194,7 +32357,7 @@ async function writeFilesWithProgress(files, options = {}) {
|
|
|
32194
32357
|
result = writeFile(file2.path, file2.content, options);
|
|
32195
32358
|
}
|
|
32196
32359
|
results.push(result);
|
|
32197
|
-
await new Promise((
|
|
32360
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
32198
32361
|
}
|
|
32199
32362
|
spinner.stop("Files written");
|
|
32200
32363
|
for (const result of results) {
|
package/dist/index.js
CHANGED
|
@@ -24061,6 +24061,8 @@ class HarmonyApiClient {
|
|
|
24061
24061
|
params.set("summary", "true");
|
|
24062
24062
|
if (options?.includeArchived)
|
|
24063
24063
|
params.set("include_archived", "true");
|
|
24064
|
+
if (options?.labelName)
|
|
24065
|
+
params.set("label_name", options.labelName);
|
|
24064
24066
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
24065
24067
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
24066
24068
|
}
|
package/dist/lib/api-client.js
CHANGED
|
@@ -221,6 +221,8 @@ export class HarmonyApiClient {
|
|
|
221
221
|
params.set("summary", "true");
|
|
222
222
|
if (options?.includeArchived)
|
|
223
223
|
params.set("include_archived", "true");
|
|
224
|
+
if (options?.labelName)
|
|
225
|
+
params.set("label_name", options.labelName);
|
|
224
226
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
225
227
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
226
228
|
}
|
package/dist/lib/tui/docs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
4
|
import { colors, symbols } from "./theme.js";
|
|
5
5
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -141,8 +141,12 @@ export function scanProject(cwd) {
|
|
|
141
141
|
let framework = null;
|
|
142
142
|
if (pkg) {
|
|
143
143
|
const deps = {
|
|
144
|
-
...(typeof pkg.dependencies === "object"
|
|
145
|
-
|
|
144
|
+
...(typeof pkg.dependencies === "object"
|
|
145
|
+
? pkg.dependencies
|
|
146
|
+
: {}),
|
|
147
|
+
...(typeof pkg.devDependencies === "object"
|
|
148
|
+
? pkg.devDependencies
|
|
149
|
+
: {}),
|
|
146
150
|
};
|
|
147
151
|
if (deps.next) {
|
|
148
152
|
framework = "next";
|
|
@@ -305,7 +309,11 @@ function describeScript(name) {
|
|
|
305
309
|
* Generate a scaffold AGENTS.md from project metadata.
|
|
306
310
|
*/
|
|
307
311
|
export function generateAgentsMd(info, _cwd) {
|
|
308
|
-
const lang = info.language === "typescript"
|
|
312
|
+
const lang = info.language === "typescript"
|
|
313
|
+
? "TypeScript"
|
|
314
|
+
: info.language === "javascript"
|
|
315
|
+
? "JavaScript"
|
|
316
|
+
: info.language;
|
|
309
317
|
const frameworkLabel = info.framework ? `${info.framework} ` : "";
|
|
310
318
|
const monoLabel = info.monorepo ? " (monorepo)" : "";
|
|
311
319
|
const lines = [];
|
|
@@ -404,19 +412,63 @@ export function generateArchitectureMd(info, _cwd) {
|
|
|
404
412
|
return lines.join("\n");
|
|
405
413
|
}
|
|
406
414
|
// ── Verification ────────────────────────────────────────────────────────────
|
|
415
|
+
/** Vague phrases that provide no actionable guidance to agents. */
|
|
416
|
+
const VAGUE_STANDARDS = [
|
|
417
|
+
"follow best practices",
|
|
418
|
+
"use best practices",
|
|
419
|
+
"keep it clean",
|
|
420
|
+
"write clean code",
|
|
421
|
+
"maintain code quality",
|
|
422
|
+
"ensure quality",
|
|
423
|
+
"use proper naming",
|
|
424
|
+
"follow conventions",
|
|
425
|
+
"be consistent",
|
|
426
|
+
];
|
|
407
427
|
/**
|
|
408
|
-
* Verify existing docs for broken references, stale commands,
|
|
428
|
+
* Verify existing docs for broken references, stale commands, dead paths,
|
|
429
|
+
* and structural quality issues from the setup-agent-docs quality checks.
|
|
409
430
|
*/
|
|
410
431
|
export function verifyDocs(cwd) {
|
|
411
432
|
const issues = [];
|
|
412
|
-
// 1. CLAUDE.md — check @-references
|
|
413
433
|
const claudeMd = readText(join(cwd, "CLAUDE.md"));
|
|
434
|
+
const agentsMd = readText(join(cwd, "AGENTS.md"));
|
|
435
|
+
const pkg = readJson(join(cwd, "package.json"));
|
|
436
|
+
const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
|
|
437
|
+
? pkg.scripts
|
|
438
|
+
: {};
|
|
439
|
+
// ── CLAUDE.md checks ──────────────────────────────────────────────────
|
|
440
|
+
const projectRoot = resolve(cwd);
|
|
414
441
|
if (claudeMd) {
|
|
442
|
+
// Check @-references exist on disk (with path traversal protection)
|
|
443
|
+
const importedFiles = [];
|
|
415
444
|
for (const line of claudeMd.split("\n")) {
|
|
416
445
|
const match = line.match(/^@(.+)$/);
|
|
417
446
|
if (match) {
|
|
418
447
|
const refPath = match[1].trim();
|
|
419
|
-
|
|
448
|
+
// Reject absolute paths
|
|
449
|
+
if (isAbsolute(refPath)) {
|
|
450
|
+
issues.push({
|
|
451
|
+
severity: "error",
|
|
452
|
+
file: "CLAUDE.md",
|
|
453
|
+
message: `@ reference uses an absolute path: ${refPath}`,
|
|
454
|
+
fix: "Use a project-relative path under the repository root",
|
|
455
|
+
});
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
// Resolve and ensure the path stays inside the project root
|
|
459
|
+
const resolvedPath = resolve(projectRoot, refPath);
|
|
460
|
+
if (resolvedPath !== projectRoot &&
|
|
461
|
+
!resolvedPath.startsWith(projectRoot + sep)) {
|
|
462
|
+
issues.push({
|
|
463
|
+
severity: "error",
|
|
464
|
+
file: "CLAUDE.md",
|
|
465
|
+
message: `@ reference escapes project root: ${refPath}`,
|
|
466
|
+
fix: "Remove traversal segments (../) and keep references inside the repo",
|
|
467
|
+
});
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
importedFiles.push({ ref: refPath, resolved: resolvedPath });
|
|
471
|
+
if (!existsSync(resolvedPath)) {
|
|
420
472
|
issues.push({
|
|
421
473
|
severity: "error",
|
|
422
474
|
file: "CLAUDE.md",
|
|
@@ -426,26 +478,96 @@ export function verifyDocs(cwd) {
|
|
|
426
478
|
}
|
|
427
479
|
}
|
|
428
480
|
}
|
|
481
|
+
// Line count — CLAUDE.md should be under 100 lines (lean, @imports only)
|
|
482
|
+
const claudeLines = claudeMd.split("\n").length;
|
|
483
|
+
if (claudeLines > 100) {
|
|
484
|
+
issues.push({
|
|
485
|
+
severity: "warning",
|
|
486
|
+
file: "CLAUDE.md",
|
|
487
|
+
message: `CLAUDE.md is ${claudeLines} lines (recommended: under 100)`,
|
|
488
|
+
fix: "Move detailed content to AGENTS.md or docs/ files and use @imports",
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
// Duplication — check if CLAUDE.md duplicates content from imported files
|
|
492
|
+
if (importedFiles.length > 0) {
|
|
493
|
+
const claudeHeadings = extractHeadings(claudeMd);
|
|
494
|
+
for (const { ref: refPath, resolved: resolvedPath } of importedFiles) {
|
|
495
|
+
const refContent = readText(resolvedPath);
|
|
496
|
+
if (!refContent)
|
|
497
|
+
continue;
|
|
498
|
+
const refHeadings = extractHeadings(refContent);
|
|
499
|
+
// Flag if CLAUDE.md repeats section headings from imported files
|
|
500
|
+
for (const heading of claudeHeadings) {
|
|
501
|
+
if (refHeadings.has(heading)) {
|
|
502
|
+
issues.push({
|
|
503
|
+
severity: "warning",
|
|
504
|
+
file: "CLAUDE.md",
|
|
505
|
+
message: `Section "${heading}" duplicates content from @${refPath}`,
|
|
506
|
+
fix: `Remove the "${heading}" section — it's already included via @import`,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
429
512
|
}
|
|
430
|
-
//
|
|
431
|
-
const agentsMd = readText(join(cwd, "AGENTS.md"));
|
|
432
|
-
const pkg = readJson(join(cwd, "package.json"));
|
|
433
|
-
const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
|
|
434
|
-
? pkg.scripts
|
|
435
|
-
: {};
|
|
513
|
+
// ── AGENTS.md checks ──────────────────────────────────────────────────
|
|
436
514
|
if (agentsMd) {
|
|
437
|
-
//
|
|
515
|
+
// Project Context — first non-heading, non-blank line should be exactly one line
|
|
516
|
+
const agentsLines = agentsMd.split("\n");
|
|
517
|
+
const contextLines = [];
|
|
518
|
+
let pastFirstHeading = false;
|
|
519
|
+
let hitNextSection = false;
|
|
520
|
+
for (const line of agentsLines) {
|
|
521
|
+
if (!pastFirstHeading) {
|
|
522
|
+
if (line.startsWith("# ")) {
|
|
523
|
+
pastFirstHeading = true;
|
|
524
|
+
}
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
// Stop at next ## heading
|
|
528
|
+
if (line.startsWith("## ")) {
|
|
529
|
+
hitNextSection = true;
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
const trimmed = line.trim();
|
|
533
|
+
if (trimmed)
|
|
534
|
+
contextLines.push(trimmed);
|
|
535
|
+
}
|
|
536
|
+
if (pastFirstHeading && !hitNextSection && contextLines.length === 0) {
|
|
537
|
+
issues.push({
|
|
538
|
+
severity: "warning",
|
|
539
|
+
file: "AGENTS.md",
|
|
540
|
+
message: "Missing project context line after the title heading",
|
|
541
|
+
fix: "Add a single-line description: stack + what the project does",
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
else if (contextLines.length > 1) {
|
|
545
|
+
issues.push({
|
|
546
|
+
severity: "warning",
|
|
547
|
+
file: "AGENTS.md",
|
|
548
|
+
message: `Project context should be exactly 1 line, found ${contextLines.length}`,
|
|
549
|
+
fix: "Condense to a single line: stack + what the project does",
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
// Commands — check against package.json scripts
|
|
438
553
|
const codeBlockRe = /```[\s\S]*?```/g;
|
|
439
554
|
let blockMatch;
|
|
440
555
|
while ((blockMatch = codeBlockRe.exec(agentsMd)) !== null) {
|
|
441
556
|
const block = blockMatch[0];
|
|
442
|
-
// Match lines that look like package manager commands
|
|
443
557
|
const cmdRe = /(?:bun|npm|pnpm|yarn)\s+(?:run\s+)?(\S+)/g;
|
|
444
558
|
let cmdMatch;
|
|
445
559
|
while ((cmdMatch = cmdRe.exec(block)) !== null) {
|
|
446
560
|
const scriptName = cmdMatch[1];
|
|
447
|
-
|
|
448
|
-
|
|
561
|
+
const builtins = new Set([
|
|
562
|
+
"install",
|
|
563
|
+
"init",
|
|
564
|
+
"create",
|
|
565
|
+
"exec",
|
|
566
|
+
"dlx",
|
|
567
|
+
"x",
|
|
568
|
+
"test",
|
|
569
|
+
"start",
|
|
570
|
+
]);
|
|
449
571
|
if (builtins.has(scriptName))
|
|
450
572
|
continue;
|
|
451
573
|
if (Object.keys(pkgScripts).length > 0 && !(scriptName in pkgScripts)) {
|
|
@@ -458,27 +580,91 @@ export function verifyDocs(cwd) {
|
|
|
458
580
|
}
|
|
459
581
|
}
|
|
460
582
|
}
|
|
461
|
-
//
|
|
583
|
+
// Code Standards — flag vague, non-actionable phrases
|
|
584
|
+
const standardsSection = extractSection(agentsMd, "Code Standards");
|
|
585
|
+
if (standardsSection) {
|
|
586
|
+
const lower = standardsSection.toLowerCase();
|
|
587
|
+
for (const phrase of VAGUE_STANDARDS) {
|
|
588
|
+
if (lower.includes(phrase)) {
|
|
589
|
+
issues.push({
|
|
590
|
+
severity: "warning",
|
|
591
|
+
file: "AGENTS.md",
|
|
592
|
+
message: `Code Standards contains vague phrase: "${phrase}"`,
|
|
593
|
+
fix: "Replace with specific, verifiable conventions derived from config files",
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Missing test command — if no test script exists, AGENTS.md should say so
|
|
599
|
+
if (Object.keys(pkgScripts).length > 0) {
|
|
600
|
+
const hasTestScript = Object.keys(pkgScripts).some((k) => k === "test" || k.startsWith("test:"));
|
|
601
|
+
if (!hasTestScript) {
|
|
602
|
+
const mentionsNoTest = agentsMd.toLowerCase().includes("no test");
|
|
603
|
+
if (!mentionsNoTest) {
|
|
604
|
+
issues.push({
|
|
605
|
+
severity: "warning",
|
|
606
|
+
file: "AGENTS.md",
|
|
607
|
+
message: "No test script in package.json and AGENTS.md doesn't mention it",
|
|
608
|
+
fix: 'Add a note like "No test framework. Verify changes with `bun run build`."',
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Check backtick-quoted paths
|
|
462
614
|
checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
|
|
463
615
|
}
|
|
464
|
-
//
|
|
616
|
+
// ── docs/architecture.md checks ───────────────────────────────────────
|
|
465
617
|
const archMd = readText(join(cwd, "docs", "architecture.md"));
|
|
466
618
|
if (archMd) {
|
|
467
619
|
checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
|
|
468
620
|
}
|
|
469
621
|
return issues;
|
|
470
622
|
}
|
|
623
|
+
/** Extract ## headings from markdown content. */
|
|
624
|
+
function extractHeadings(content) {
|
|
625
|
+
const headings = new Set();
|
|
626
|
+
for (const line of content.split("\n")) {
|
|
627
|
+
const match = line.match(/^#{2,3}\s+(.+)$/);
|
|
628
|
+
if (match) {
|
|
629
|
+
headings.add(match[1].trim());
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return headings;
|
|
633
|
+
}
|
|
634
|
+
/** Extract content under a specific ## section heading. */
|
|
635
|
+
function extractSection(content, heading) {
|
|
636
|
+
const lines = content.split("\n");
|
|
637
|
+
let capturing = false;
|
|
638
|
+
const result = [];
|
|
639
|
+
for (const line of lines) {
|
|
640
|
+
if (capturing) {
|
|
641
|
+
// Stop at next ## heading
|
|
642
|
+
if (line.match(/^#{1,2}\s/))
|
|
643
|
+
break;
|
|
644
|
+
result.push(line);
|
|
645
|
+
}
|
|
646
|
+
else if (line.match(new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i"))) {
|
|
647
|
+
capturing = true;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return result.length > 0 ? result.join("\n") : null;
|
|
651
|
+
}
|
|
471
652
|
/** Scan markdown for backtick-quoted paths and check they exist. */
|
|
472
653
|
function checkBacktickPaths(content, file, cwd, issues) {
|
|
473
654
|
const pathRe = /`((?:src\/|packages\/|apps\/|supabase\/|docs\/)[^`]+)`/g;
|
|
474
655
|
let match;
|
|
475
656
|
const checked = new Set();
|
|
657
|
+
const root = resolve(cwd);
|
|
476
658
|
while ((match = pathRe.exec(content)) !== null) {
|
|
477
659
|
const refPath = match[1].replace(/\/$/, ""); // strip trailing slash
|
|
478
660
|
if (checked.has(refPath))
|
|
479
661
|
continue;
|
|
480
662
|
checked.add(refPath);
|
|
481
|
-
|
|
663
|
+
// Skip paths that escape the project root (e.g. src/../../etc/hosts)
|
|
664
|
+
const resolvedRef = resolve(root, refPath);
|
|
665
|
+
if (resolvedRef !== root && !resolvedRef.startsWith(root + sep))
|
|
666
|
+
continue;
|
|
667
|
+
if (!existsSync(resolvedRef)) {
|
|
482
668
|
issues.push({
|
|
483
669
|
severity: "warning",
|
|
484
670
|
file,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"coding-assistant"
|
|
47
47
|
],
|
|
48
48
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
49
|
+
"node": ">=20.0.0",
|
|
50
|
+
"bun": ">=1.0.0"
|
|
50
51
|
},
|
|
51
52
|
"scripts": {
|
|
52
53
|
"build": "bun build src/index.ts src/cli.ts --outdir dist --target node && tsc --outDir dist/lib --declaration false --skipLibCheck --noCheck",
|
|
@@ -68,7 +69,7 @@
|
|
|
68
69
|
"zod": "^4.3.6"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
|
-
"@types/node": "^25.
|
|
72
|
-
"typescript": "^
|
|
72
|
+
"@types/node": "^25.5.0",
|
|
73
|
+
"typescript": "^6.0.1"
|
|
73
74
|
}
|
|
74
75
|
}
|
package/src/api-client.ts
CHANGED
|
@@ -286,6 +286,7 @@ export class HarmonyApiClient {
|
|
|
286
286
|
columnId?: string;
|
|
287
287
|
summary?: boolean;
|
|
288
288
|
includeArchived?: boolean;
|
|
289
|
+
labelName?: string;
|
|
289
290
|
},
|
|
290
291
|
): Promise<{
|
|
291
292
|
project: unknown;
|
|
@@ -309,6 +310,7 @@ export class HarmonyApiClient {
|
|
|
309
310
|
if (options?.columnId) params.set("column_id", options.columnId);
|
|
310
311
|
if (options?.summary) params.set("summary", "true");
|
|
311
312
|
if (options?.includeArchived) params.set("include_archived", "true");
|
|
313
|
+
if (options?.labelName) params.set("label_name", options.labelName);
|
|
312
314
|
|
|
313
315
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
314
316
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
@@ -497,6 +499,10 @@ export class HarmonyApiClient {
|
|
|
497
499
|
currentTask?: string;
|
|
498
500
|
blockers?: string[];
|
|
499
501
|
estimatedMinutesRemaining?: number;
|
|
502
|
+
phase?: string;
|
|
503
|
+
filesChanged?: number;
|
|
504
|
+
costCents?: number;
|
|
505
|
+
recentActions?: { action: string; ts: string }[];
|
|
500
506
|
},
|
|
501
507
|
): Promise<{ session: unknown; created: boolean }> {
|
|
502
508
|
return this.request("POST", `/cards/${cardId}/agent-context`, data);
|
package/src/tui/docs.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
readFileSync,
|
|
4
|
-
readdirSync,
|
|
5
|
-
statSync,
|
|
6
|
-
} from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
8
3
|
import * as p from "@clack/prompts";
|
|
9
4
|
import { colors, symbols } from "./theme.js";
|
|
10
5
|
|
|
@@ -182,8 +177,12 @@ export function scanProject(cwd: string): ProjectInfo {
|
|
|
182
177
|
let framework: string | null = null;
|
|
183
178
|
if (pkg) {
|
|
184
179
|
const deps: Record<string, string> = {
|
|
185
|
-
...(typeof pkg.dependencies === "object"
|
|
186
|
-
|
|
180
|
+
...(typeof pkg.dependencies === "object"
|
|
181
|
+
? (pkg.dependencies as Record<string, string>)
|
|
182
|
+
: {}),
|
|
183
|
+
...(typeof pkg.devDependencies === "object"
|
|
184
|
+
? (pkg.devDependencies as Record<string, string>)
|
|
185
|
+
: {}),
|
|
187
186
|
};
|
|
188
187
|
|
|
189
188
|
if (deps.next) {
|
|
@@ -347,7 +346,12 @@ function describeScript(name: string): string {
|
|
|
347
346
|
* Generate a scaffold AGENTS.md from project metadata.
|
|
348
347
|
*/
|
|
349
348
|
export function generateAgentsMd(info: ProjectInfo, _cwd: string): string {
|
|
350
|
-
const lang =
|
|
349
|
+
const lang =
|
|
350
|
+
info.language === "typescript"
|
|
351
|
+
? "TypeScript"
|
|
352
|
+
: info.language === "javascript"
|
|
353
|
+
? "JavaScript"
|
|
354
|
+
: info.language;
|
|
351
355
|
const frameworkLabel = info.framework ? `${info.framework} ` : "";
|
|
352
356
|
const monoLabel = info.monorepo ? " (monorepo)" : "";
|
|
353
357
|
|
|
@@ -388,7 +392,8 @@ export function generateAgentsMd(info: ProjectInfo, _cwd: string): string {
|
|
|
388
392
|
lines.push("## Code Standards");
|
|
389
393
|
lines.push("");
|
|
390
394
|
|
|
391
|
-
const langLabel =
|
|
395
|
+
const langLabel =
|
|
396
|
+
info.language === "typescript" ? "TypeScript" : "JavaScript";
|
|
392
397
|
if (info.language === "typescript" || info.language === "javascript") {
|
|
393
398
|
lines.push(`- ${langLabel} with ES modules`);
|
|
394
399
|
}
|
|
@@ -442,7 +447,10 @@ export function generateClaudeMd(info: ProjectInfo): string {
|
|
|
442
447
|
/**
|
|
443
448
|
* Generate an architecture.md scaffold.
|
|
444
449
|
*/
|
|
445
|
-
export function generateArchitectureMd(
|
|
450
|
+
export function generateArchitectureMd(
|
|
451
|
+
info: ProjectInfo,
|
|
452
|
+
_cwd: string,
|
|
453
|
+
): string {
|
|
446
454
|
const lines: string[] = [];
|
|
447
455
|
lines.push("# Architecture");
|
|
448
456
|
lines.push("");
|
|
@@ -468,20 +476,74 @@ export function generateArchitectureMd(info: ProjectInfo, _cwd: string): string
|
|
|
468
476
|
|
|
469
477
|
// ── Verification ────────────────────────────────────────────────────────────
|
|
470
478
|
|
|
479
|
+
/** Vague phrases that provide no actionable guidance to agents. */
|
|
480
|
+
const VAGUE_STANDARDS = [
|
|
481
|
+
"follow best practices",
|
|
482
|
+
"use best practices",
|
|
483
|
+
"keep it clean",
|
|
484
|
+
"write clean code",
|
|
485
|
+
"maintain code quality",
|
|
486
|
+
"ensure quality",
|
|
487
|
+
"use proper naming",
|
|
488
|
+
"follow conventions",
|
|
489
|
+
"be consistent",
|
|
490
|
+
];
|
|
491
|
+
|
|
471
492
|
/**
|
|
472
|
-
* Verify existing docs for broken references, stale commands,
|
|
493
|
+
* Verify existing docs for broken references, stale commands, dead paths,
|
|
494
|
+
* and structural quality issues from the setup-agent-docs quality checks.
|
|
473
495
|
*/
|
|
474
496
|
export function verifyDocs(cwd: string): DocsIssue[] {
|
|
475
497
|
const issues: DocsIssue[] = [];
|
|
476
498
|
|
|
477
|
-
// 1. CLAUDE.md — check @-references
|
|
478
499
|
const claudeMd = readText(join(cwd, "CLAUDE.md"));
|
|
500
|
+
const agentsMd = readText(join(cwd, "AGENTS.md"));
|
|
501
|
+
const pkg = readJson(join(cwd, "package.json"));
|
|
502
|
+
const pkgScripts =
|
|
503
|
+
pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
|
|
504
|
+
? (pkg.scripts as Record<string, string>)
|
|
505
|
+
: {};
|
|
506
|
+
|
|
507
|
+
// ── CLAUDE.md checks ──────────────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
const projectRoot = resolve(cwd);
|
|
510
|
+
|
|
479
511
|
if (claudeMd) {
|
|
512
|
+
// Check @-references exist on disk (with path traversal protection)
|
|
513
|
+
const importedFiles: { ref: string; resolved: string }[] = [];
|
|
480
514
|
for (const line of claudeMd.split("\n")) {
|
|
481
515
|
const match = line.match(/^@(.+)$/);
|
|
482
516
|
if (match) {
|
|
483
517
|
const refPath = match[1].trim();
|
|
484
|
-
|
|
518
|
+
|
|
519
|
+
// Reject absolute paths
|
|
520
|
+
if (isAbsolute(refPath)) {
|
|
521
|
+
issues.push({
|
|
522
|
+
severity: "error",
|
|
523
|
+
file: "CLAUDE.md",
|
|
524
|
+
message: `@ reference uses an absolute path: ${refPath}`,
|
|
525
|
+
fix: "Use a project-relative path under the repository root",
|
|
526
|
+
});
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Resolve and ensure the path stays inside the project root
|
|
531
|
+
const resolvedPath = resolve(projectRoot, refPath);
|
|
532
|
+
if (
|
|
533
|
+
resolvedPath !== projectRoot &&
|
|
534
|
+
!resolvedPath.startsWith(projectRoot + sep)
|
|
535
|
+
) {
|
|
536
|
+
issues.push({
|
|
537
|
+
severity: "error",
|
|
538
|
+
file: "CLAUDE.md",
|
|
539
|
+
message: `@ reference escapes project root: ${refPath}`,
|
|
540
|
+
fix: "Remove traversal segments (../) and keep references inside the repo",
|
|
541
|
+
});
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
importedFiles.push({ ref: refPath, resolved: resolvedPath });
|
|
546
|
+
if (!existsSync(resolvedPath)) {
|
|
485
547
|
issues.push({
|
|
486
548
|
severity: "error",
|
|
487
549
|
file: "CLAUDE.md",
|
|
@@ -491,28 +553,99 @@ export function verifyDocs(cwd: string): DocsIssue[] {
|
|
|
491
553
|
}
|
|
492
554
|
}
|
|
493
555
|
}
|
|
556
|
+
|
|
557
|
+
// Line count — CLAUDE.md should be under 100 lines (lean, @imports only)
|
|
558
|
+
const claudeLines = claudeMd.split("\n").length;
|
|
559
|
+
if (claudeLines > 100) {
|
|
560
|
+
issues.push({
|
|
561
|
+
severity: "warning",
|
|
562
|
+
file: "CLAUDE.md",
|
|
563
|
+
message: `CLAUDE.md is ${claudeLines} lines (recommended: under 100)`,
|
|
564
|
+
fix: "Move detailed content to AGENTS.md or docs/ files and use @imports",
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Duplication — check if CLAUDE.md duplicates content from imported files
|
|
569
|
+
if (importedFiles.length > 0) {
|
|
570
|
+
const claudeHeadings = extractHeadings(claudeMd);
|
|
571
|
+
for (const { ref: refPath, resolved: resolvedPath } of importedFiles) {
|
|
572
|
+
const refContent = readText(resolvedPath);
|
|
573
|
+
if (!refContent) continue;
|
|
574
|
+
const refHeadings = extractHeadings(refContent);
|
|
575
|
+
// Flag if CLAUDE.md repeats section headings from imported files
|
|
576
|
+
for (const heading of claudeHeadings) {
|
|
577
|
+
if (refHeadings.has(heading)) {
|
|
578
|
+
issues.push({
|
|
579
|
+
severity: "warning",
|
|
580
|
+
file: "CLAUDE.md",
|
|
581
|
+
message: `Section "${heading}" duplicates content from @${refPath}`,
|
|
582
|
+
fix: `Remove the "${heading}" section — it's already included via @import`,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
494
588
|
}
|
|
495
589
|
|
|
496
|
-
//
|
|
497
|
-
const agentsMd = readText(join(cwd, "AGENTS.md"));
|
|
498
|
-
const pkg = readJson(join(cwd, "package.json"));
|
|
499
|
-
const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
|
|
500
|
-
? (pkg.scripts as Record<string, string>)
|
|
501
|
-
: {};
|
|
590
|
+
// ── AGENTS.md checks ──────────────────────────────────────────────────
|
|
502
591
|
|
|
503
592
|
if (agentsMd) {
|
|
504
|
-
//
|
|
593
|
+
// Project Context — first non-heading, non-blank line should be exactly one line
|
|
594
|
+
const agentsLines = agentsMd.split("\n");
|
|
595
|
+
const contextLines: string[] = [];
|
|
596
|
+
let pastFirstHeading = false;
|
|
597
|
+
let hitNextSection = false;
|
|
598
|
+
for (const line of agentsLines) {
|
|
599
|
+
if (!pastFirstHeading) {
|
|
600
|
+
if (line.startsWith("# ")) {
|
|
601
|
+
pastFirstHeading = true;
|
|
602
|
+
}
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
// Stop at next ## heading
|
|
606
|
+
if (line.startsWith("## ")) {
|
|
607
|
+
hitNextSection = true;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
const trimmed = line.trim();
|
|
611
|
+
if (trimmed) contextLines.push(trimmed);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (pastFirstHeading && !hitNextSection && contextLines.length === 0) {
|
|
615
|
+
issues.push({
|
|
616
|
+
severity: "warning",
|
|
617
|
+
file: "AGENTS.md",
|
|
618
|
+
message: "Missing project context line after the title heading",
|
|
619
|
+
fix: "Add a single-line description: stack + what the project does",
|
|
620
|
+
});
|
|
621
|
+
} else if (contextLines.length > 1) {
|
|
622
|
+
issues.push({
|
|
623
|
+
severity: "warning",
|
|
624
|
+
file: "AGENTS.md",
|
|
625
|
+
message: `Project context should be exactly 1 line, found ${contextLines.length}`,
|
|
626
|
+
fix: "Condense to a single line: stack + what the project does",
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Commands — check against package.json scripts
|
|
505
631
|
const codeBlockRe = /```[\s\S]*?```/g;
|
|
506
632
|
let blockMatch: RegExpExecArray | null;
|
|
507
633
|
while ((blockMatch = codeBlockRe.exec(agentsMd)) !== null) {
|
|
508
634
|
const block = blockMatch[0];
|
|
509
|
-
// Match lines that look like package manager commands
|
|
510
635
|
const cmdRe = /(?:bun|npm|pnpm|yarn)\s+(?:run\s+)?(\S+)/g;
|
|
511
636
|
let cmdMatch: RegExpExecArray | null;
|
|
512
637
|
while ((cmdMatch = cmdRe.exec(block)) !== null) {
|
|
513
638
|
const scriptName = cmdMatch[1];
|
|
514
|
-
|
|
515
|
-
|
|
639
|
+
const builtins = new Set([
|
|
640
|
+
"install",
|
|
641
|
+
"init",
|
|
642
|
+
"create",
|
|
643
|
+
"exec",
|
|
644
|
+
"dlx",
|
|
645
|
+
"x",
|
|
646
|
+
"test",
|
|
647
|
+
"start",
|
|
648
|
+
]);
|
|
516
649
|
if (builtins.has(scriptName)) continue;
|
|
517
650
|
if (Object.keys(pkgScripts).length > 0 && !(scriptName in pkgScripts)) {
|
|
518
651
|
issues.push({
|
|
@@ -525,11 +658,47 @@ export function verifyDocs(cwd: string): DocsIssue[] {
|
|
|
525
658
|
}
|
|
526
659
|
}
|
|
527
660
|
|
|
528
|
-
//
|
|
661
|
+
// Code Standards — flag vague, non-actionable phrases
|
|
662
|
+
const standardsSection = extractSection(agentsMd, "Code Standards");
|
|
663
|
+
if (standardsSection) {
|
|
664
|
+
const lower = standardsSection.toLowerCase();
|
|
665
|
+
for (const phrase of VAGUE_STANDARDS) {
|
|
666
|
+
if (lower.includes(phrase)) {
|
|
667
|
+
issues.push({
|
|
668
|
+
severity: "warning",
|
|
669
|
+
file: "AGENTS.md",
|
|
670
|
+
message: `Code Standards contains vague phrase: "${phrase}"`,
|
|
671
|
+
fix: "Replace with specific, verifiable conventions derived from config files",
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Missing test command — if no test script exists, AGENTS.md should say so
|
|
678
|
+
if (Object.keys(pkgScripts).length > 0) {
|
|
679
|
+
const hasTestScript = Object.keys(pkgScripts).some(
|
|
680
|
+
(k) => k === "test" || k.startsWith("test:"),
|
|
681
|
+
);
|
|
682
|
+
if (!hasTestScript) {
|
|
683
|
+
const mentionsNoTest = agentsMd.toLowerCase().includes("no test");
|
|
684
|
+
if (!mentionsNoTest) {
|
|
685
|
+
issues.push({
|
|
686
|
+
severity: "warning",
|
|
687
|
+
file: "AGENTS.md",
|
|
688
|
+
message:
|
|
689
|
+
"No test script in package.json and AGENTS.md doesn't mention it",
|
|
690
|
+
fix: 'Add a note like "No test framework. Verify changes with `bun run build`."',
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Check backtick-quoted paths
|
|
529
697
|
checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
|
|
530
698
|
}
|
|
531
699
|
|
|
532
|
-
//
|
|
700
|
+
// ── docs/architecture.md checks ───────────────────────────────────────
|
|
701
|
+
|
|
533
702
|
const archMd = readText(join(cwd, "docs", "architecture.md"));
|
|
534
703
|
if (archMd) {
|
|
535
704
|
checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
|
|
@@ -538,6 +707,44 @@ export function verifyDocs(cwd: string): DocsIssue[] {
|
|
|
538
707
|
return issues;
|
|
539
708
|
}
|
|
540
709
|
|
|
710
|
+
/** Extract ## headings from markdown content. */
|
|
711
|
+
function extractHeadings(content: string): Set<string> {
|
|
712
|
+
const headings = new Set<string>();
|
|
713
|
+
for (const line of content.split("\n")) {
|
|
714
|
+
const match = line.match(/^#{2,3}\s+(.+)$/);
|
|
715
|
+
if (match) {
|
|
716
|
+
headings.add(match[1].trim());
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return headings;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** Extract content under a specific ## section heading. */
|
|
723
|
+
function extractSection(content: string, heading: string): string | null {
|
|
724
|
+
const lines = content.split("\n");
|
|
725
|
+
let capturing = false;
|
|
726
|
+
const result: string[] = [];
|
|
727
|
+
|
|
728
|
+
for (const line of lines) {
|
|
729
|
+
if (capturing) {
|
|
730
|
+
// Stop at next ## heading
|
|
731
|
+
if (line.match(/^#{1,2}\s/)) break;
|
|
732
|
+
result.push(line);
|
|
733
|
+
} else if (
|
|
734
|
+
line.match(
|
|
735
|
+
new RegExp(
|
|
736
|
+
`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
|
|
737
|
+
"i",
|
|
738
|
+
),
|
|
739
|
+
)
|
|
740
|
+
) {
|
|
741
|
+
capturing = true;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return result.length > 0 ? result.join("\n") : null;
|
|
746
|
+
}
|
|
747
|
+
|
|
541
748
|
/** Scan markdown for backtick-quoted paths and check they exist. */
|
|
542
749
|
function checkBacktickPaths(
|
|
543
750
|
content: string,
|
|
@@ -549,12 +756,18 @@ function checkBacktickPaths(
|
|
|
549
756
|
let match: RegExpExecArray | null;
|
|
550
757
|
const checked = new Set<string>();
|
|
551
758
|
|
|
759
|
+
const root = resolve(cwd);
|
|
760
|
+
|
|
552
761
|
while ((match = pathRe.exec(content)) !== null) {
|
|
553
762
|
const refPath = match[1].replace(/\/$/, ""); // strip trailing slash
|
|
554
763
|
if (checked.has(refPath)) continue;
|
|
555
764
|
checked.add(refPath);
|
|
556
765
|
|
|
557
|
-
|
|
766
|
+
// Skip paths that escape the project root (e.g. src/../../etc/hosts)
|
|
767
|
+
const resolvedRef = resolve(root, refPath);
|
|
768
|
+
if (resolvedRef !== root && !resolvedRef.startsWith(root + sep)) continue;
|
|
769
|
+
|
|
770
|
+
if (!existsSync(resolvedRef)) {
|
|
558
771
|
issues.push({
|
|
559
772
|
severity: "warning",
|
|
560
773
|
file,
|