Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/cli-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,21 @@ jobs:
- name: Plugin suite
run: node scripts/check-tests-leave-git-alone.js -- node --test 'scripts/__tests__/**/*.test.js'

# A Windows commit's hooks (#948), less the three cli jobs that need cli/'s dependencies.
- name: Hooks - a commit's hooks pass on Windows
run: |
echo "# lefthook subject" > "$RUNNER_TEMP/msg"
status=0
LEFTHOOK_EXCLUDE=cli-biome,cli-architecture,cli-typecheck \
pnpm exec lefthook run pre-commit --all-files --no-auto-install --no-tty > "$RUNNER_TEMP/hooks.log" 2>&1 || status=1
pnpm exec lefthook run prepare-commit-msg "$RUNNER_TEMP/msg" message --no-auto-install --no-tty >> "$RUNNER_TEMP/hooks.log" 2>&1 || status=1
cat "$RUNNER_TEMP/hooks.log"
if grep -n "syntax error" "$RUNNER_TEMP/hooks.log"; then
echo "::error::lefthook handed sh a broken command line (#948)"
exit 1
fi
exit $status

- run: cd cli && pnpm install --frozen-lockfile
# A real global install, not `node dist/cli.js` by path: nothing else proves `aidd`
# resolves on this platform's PATH. Built here, ahead of the suites below, so this is
Expand Down
62 changes: 23 additions & 39 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pre-commit:
- "cli/tests/fixtures/**"
run: |
if ! command -v node >/dev/null 2>&1; then
echo "❌ node not available; cannot validate JSON"
echo '❌ node not available; cannot validate JSON'
exit 1
fi
node scripts/validate-json.mjs {files}
Expand All @@ -17,22 +17,22 @@ pre-commit:
- "plugins/*/skills/*/assets/**"
run: |
if ! command -v node >/dev/null 2>&1; then
echo "❌ node not available; cannot validate YAML"
echo '❌ node not available; cannot validate YAML'
exit 1
fi
node scripts/validate-yaml.mjs {files}
scripts-tests:
glob: "{scripts,plugins,cli/src}/**"
run: |
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping scripts-tests"
echo 'ℹ️ node not available; skipping scripts-tests'
exit 0
fi
# `node --test <dir>` fails on a directory, so a glob is required — and a glob
# matching nothing exits 0 in silence, so a renamed folder must be caught by a count.
found=$(ls scripts/__tests__/*.test.js 2>/dev/null | wc -l | tr -d ' ')
if [ "$found" -eq 0 ]; then
echo "❌ no test files under scripts/__tests__/; the runner is not wired"
if [ $found -eq 0 ]; then
echo '❌ no test files under scripts/__tests__/; the runner is not wired'
exit 1
fi
# Wrapped: a suite writing into this repository's own .git/hooks passes every
Expand All @@ -43,29 +43,29 @@ pre-commit:
glob: "plugins/*/skills/*/SKILL.md"
run: |
for f in {files}; do
[ -f "$f" ] || continue
if ! head -10 "$f" | grep -q "^name:"; then
echo "❌ missing 'name:' frontmatter: $f"
[ -f $f ] || continue
if ! head -10 $f | grep -q '^name:'; then
echo '❌ missing name: frontmatter:' $f
exit 1
fi
if ! head -10 "$f" | grep -q "^description:"; then
echo "❌ missing 'description:' frontmatter: $f"
if ! head -10 $f | grep -q '^description:'; then
echo '❌ missing description: frontmatter:' $f
exit 1
fi
done
check-skill-argument-hints:
glob: "plugins/*/skills/**"
run: |
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping check-skill-argument-hints"
echo 'ℹ️ node not available; skipping check-skill-argument-hints'
exit 0
fi
node scripts/check-skill-argument-hints.mjs
architecture-rules:
glob: "plugins/*/{skills,agents}/**/*.md"
run: |
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping architecture-rules"
echo 'ℹ️ node not available; skipping architecture-rules'
exit 0
fi
node scripts/check-architecture-rules.js {files}
Expand All @@ -76,7 +76,7 @@ pre-commit:
- "**/{CLAUDE,AGENTS,copilot-instructions}.md"
run: |
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping context-imports"
echo 'ℹ️ node not available; skipping context-imports'
exit 0
fi
node scripts/check-context-imports.js
Expand All @@ -89,15 +89,15 @@ pre-commit:
- "plugins/aidd-context/hooks/update_memory.js"
run: |
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping context-reference-form"
echo 'ℹ️ node not available; skipping context-reference-form'
exit 0
fi
node scripts/check-context-reference-form.js
referenced-paths:
glob: "{*.md,docs/**,aidd_docs/memory/**}"
run: |
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping referenced-paths"
echo 'ℹ️ node not available; skipping referenced-paths'
exit 0
fi
node scripts/check-referenced-paths.js
Expand All @@ -106,7 +106,7 @@ pre-commit:
glob: "**/*.md"
run: |
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping doc-duplication"
echo 'ℹ️ node not available; skipping doc-duplication'
exit 0
fi
node scripts/check-doc-duplication.js
Expand All @@ -115,33 +115,18 @@ pre-commit:
glob: "**/*.md"
run: |
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping markdown-links"
echo 'ℹ️ node not available; skipping markdown-links'
exit 0
fi
# Both ignored trees hold deliberately unresolvable targets — fixture paths and
# `@src/foo.ts` agent mentions — not links a reader is meant to follow.
node scripts/check-markdown-links.js --ignore cli/tests/fixtures --ignore cli/aidd_docs/tasks
summarize-plugin-catalogs:
run: |
[ -d plugins ] || exit 0
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping summarize-plugin-catalogs"
exit 0
fi
for plugin in plugins/*/; do
name=$(basename "$plugin")
node scripts/summarize-markdown.js "$plugin" "${plugin}CATALOG.md" \
--depth=4 \
--fields=description \
--title="$name catalog" \
--tagline="Auto-generated index of skills, agents, references and assets shipped by the \`$name\` plugin." \
>/dev/null
git add "${plugin}CATALOG.md" 2>/dev/null || true
done
run: sh scripts/summarize-plugin-catalogs.sh
summarize-telemetry-prompts-doc:
run: |
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping summarize-telemetry-prompts-doc"
echo 'ℹ️ node not available; skipping summarize-telemetry-prompts-doc'
exit 0
fi
node scripts/summarize-markdown.js plugins/aidd-telemetry docs/prompts-documentation.md \
Expand All @@ -152,7 +137,7 @@ pre-commit:
sync-readme-counts:
run: |
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping sync-readme-counts"
echo 'ℹ️ node not available; skipping sync-readme-counts'
exit 0
fi
node scripts/sync-readme-counts.mjs >/dev/null
Expand All @@ -174,7 +159,7 @@ pre-commit:
glob: "{cli/src/**,cli/tests/**}"
run: |
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping cli-type-honesty"
echo 'ℹ️ node not available; skipping cli-type-honesty'
exit 0
fi
node scripts/check-cli-type-honesty.mjs
Expand All @@ -195,9 +180,8 @@ commit-msg:

# lefthook owns prepare-commit-msg here and regenerates it on install, so `aidd telemetry on`'s
# delegate is called from a job instead. Absent delegate, nothing runs — opt-in per machine.
# `aidd telemetry` finds this job by the delegate name in its run line.
prepare-commit-msg:
commands:
aidd-session-trailer:
run: |
delegate="$(git rev-parse --git-common-dir)/hooks/aidd-session-trailer.sh"
if [ -f "$delegate" ]; then sh "$delegate" {1} {2}; fi
run: sh scripts/session-trailer-delegate.sh aidd-session-trailer.sh {1} {2}
24 changes: 24 additions & 0 deletions scripts/__tests__/lefthook-runs-hold-no-double-quote.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const yaml = require("js-yaml");

const root = path.resolve(__dirname, "../..");

// Lefthook on Windows passes a run to `sh -c` unescaped, so a `"` truncates it (#948).
test("no run in lefthook.yml holds a double quote", () => {
const config = yaml.load(fs.readFileSync(path.join(root, "lefthook.yml"), "utf8"));
const offenders = [];
let scanned = 0;

for (const [hook, def] of Object.entries(config)) {
for (const [name, command] of Object.entries(def?.commands ?? {})) {
scanned++;
if (String(command.run ?? "").includes('"')) offenders.push(`${hook}/${name}`);
}
}

assert.ok(scanned > 0, "no `commands:` run found in lefthook.yml");
assert.deepEqual(offenders, []);
});
9 changes: 9 additions & 0 deletions scripts/__tests__/skill-argument-hints.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,12 @@ test("a skill whose actions take no input needs no argument-hint", () => {

assert.equal(clean.status, 0, clean.output);
});

test("a skill checked out with CRLF line endings still reads its argument-hint", () => {
const clean = runOn({
[SKILL]: frontmatter(["argument-hint: a file path or a ticket"]).replaceAll("\n", "\r\n"),
"plugins/aidd-probe/skills/01-thing/actions/01-do.md": "## Input\r\n\r\nA path.",
});

assert.equal(clean.status, 0, clean.output);
});
12 changes: 12 additions & 0 deletions scripts/__tests__/sync-readme-counts.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const assert = require("node:assert/strict");
const { spawnSync } = require("node:child_process");
const path = require("node:path");
const test = require("node:test");

const SCRIPT = path.resolve(__dirname, "..", "sync-readme-counts.mjs");

test("the README counts check finds the README and agrees with the tree", () => {
const result = spawnSync(process.execPath, [SCRIPT, "--check"], { encoding: "utf8" });

assert.equal(result.status, 0, `${result.stdout}${result.stderr}`);
});
6 changes: 3 additions & 3 deletions scripts/check-skill-argument-hints.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@ function argumentHint(content) {
async function takesInput(actionFiles) {
for (const file of actionFiles) {
const body = await readFile(file, "utf8");
if (/^## Input$/m.test(body)) return true;
if (/^## Input\r?$/m.test(body)) return true;
}
return actionFiles.length === 0;
}

const breaches = [];
for (const dir of await skillDirs()) {
const skillPath = path.join(dir, "SKILL.md");
const content = await readFile(skillPath, "utf8").catch(() => null);
if (content === null) continue;
const content = (await readFile(skillPath, "utf8").catch(() => null))?.replaceAll("\r\n", "\n");
if (content == null) continue;

// Named the way the contract and every other guard name a path, whatever the host separator.
const relative = path.relative(ROOT, skillPath).split(path.sep).join("/");
Expand Down
4 changes: 4 additions & 0 deletions scripts/session-trailer-delegate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
delegate="$(git rev-parse --git-common-dir)/hooks/$1"
shift
if [ -f "$delegate" ]; then sh "$delegate" "$@"; fi
16 changes: 16 additions & 0 deletions scripts/summarize-plugin-catalogs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/sh
[ -d plugins ] || exit 0
if ! command -v node >/dev/null 2>&1; then
echo "ℹ️ node not available; skipping summarize-plugin-catalogs"
exit 0
fi
for plugin in plugins/*/; do
name=$(basename "$plugin")
node scripts/summarize-markdown.js "$plugin" "${plugin}CATALOG.md" \
--depth=4 \
--fields=description \
--title="$name catalog" \
--tagline="Auto-generated index of skills, agents, references and assets shipped by the \`$name\` plugin." \
>/dev/null
git add "${plugin}CATALOG.md" 2>/dev/null || true
done
3 changes: 2 additions & 1 deletion scripts/sync-readme-counts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
// `N skill(s)` code-span after that plugin's heading. `--check` reports instead of writing.

import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs";
import { fileURLToPath } from "node:url";

const ROOT = new URL("..", import.meta.url).pathname;
const ROOT = fileURLToPath(new URL("..", import.meta.url));
const README = ROOT + "README.md";
const PLUGINS = ROOT + "plugins";

Expand Down
Loading