Compare commits
6 Commits
82888f3137
...
v0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| bb2e89c83f | |||
| 63c2f73a5e | |||
| 3fd386a637 | |||
| c9d384a4fa | |||
| dedcf994df | |||
| 4e56b46dc9 |
51
.gitea/workflows/publish.yml
Normal file
51
.gitea/workflows/publish.yml
Normal file
@@ -0,0 +1,51 @@
|
||||
name: publish
|
||||
|
||||
# Publish this package and its omp port to the npm registry on version tags.
|
||||
#
|
||||
# Prerequisites on npmjs.com / git.freno.me:
|
||||
# - an npm automation token (publish scope) stored as the repo secret
|
||||
# NPM_TOKEN (consumed via actions/setup-node registry auth)
|
||||
# - the scoped package names claimed on npm (@mikefreno/<name> and
|
||||
# @mikefreno/omp-<name>)
|
||||
#
|
||||
# Both packages ship TypeScript sources — nothing is built — so lifecycle
|
||||
# scripts (prepublishOnly tsc) are skipped: the port workflow already
|
||||
# typechecks the generated port against the pinned @oh-my-pi SDK, and the pi
|
||||
# package typechecks against the global pi SDK that CI does not install.
|
||||
#
|
||||
# Manual publish: Actions tab → Run workflow (workflow_dispatch).
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup node (npm registry auth)
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish pi package
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm publish --access public --ignore-scripts
|
||||
|
||||
- name: Port to omp
|
||||
run: bun port-to-omp.mjs --out "$RUNNER_TEMP/omp-port"
|
||||
|
||||
- name: Publish omp package
|
||||
working-directory: ${{ runner.temp }}/omp-port
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm publish --access public --ignore-scripts
|
||||
37
.githooks/pre-commit
Executable file
37
.githooks/pre-commit
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
# pre-commit — typecheck the generated omp port before committing.
|
||||
#
|
||||
# Mirrors the CI port job (.gitea/workflows/port-to-omp.yml). The source
|
||||
# repo's own tsconfig extends the host harness tsconfig and is not
|
||||
# self-contained, so the reliable typecheck target is the regenerated port:
|
||||
# it ships a self-contained tsconfig and the pinned @oh-my-pi SDK as a real
|
||||
# devDependency. Regenerating into a temp dir and running `tsc --noEmit`
|
||||
# there catches exactly what CI will fail on (e.g. an interface field added
|
||||
# without updating its object literals).
|
||||
#
|
||||
# Enable (per clone): git config core.hooksPath .githooks
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
cd "$ROOT"
|
||||
|
||||
# Fast path: no TypeScript-adjacent change staged -> nothing to typecheck.
|
||||
if git diff --cached --quiet -- src/ agents/ package.json tsconfig.json port-to-omp.mjs; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
LOG="$(mktemp)"
|
||||
trap 'rm -rf "$TMP" "$LOG"' EXIT
|
||||
|
||||
if ! bun port-to-omp.mjs --out "$TMP" >"$LOG" 2>&1; then
|
||||
echo "pre-commit: port regeneration failed (CI would fail too) — output:" >&2
|
||||
tail -20 "$LOG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! (cd "$TMP" && bun run typecheck) >"$LOG" 2>&1; then
|
||||
echo "pre-commit: port typecheck failed (this is what CI runs) — output:" >&2
|
||||
tail -30 "$LOG" >&2
|
||||
exit 1
|
||||
fi
|
||||
15
README.md
15
README.md
@@ -202,6 +202,21 @@ pygienium/
|
||||
└─ agents/ ← scanner.md fixer.md deep-modules.md defensive-guards.md
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
The omp port at `~/.omp/agent/extensions/pygienium` is regenerated **only by CI
|
||||
on push** (`.gitea/workflows/port-to-omp.yml`) — never by hand.
|
||||
|
||||
The source repo's own `tsconfig.json` extends the host harness tsconfig, so the
|
||||
reliable typecheck target is the regenerated port (self-contained tsconfig +
|
||||
pinned `@oh-my-pi` SDK devDependency). A committed pre-commit hook runs exactly
|
||||
what CI runs — regenerate the port into a temp dir and `tsc --noEmit` it — and
|
||||
fails the commit on any error:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath .githooks
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pygienium",
|
||||
"version": "0.1.0",
|
||||
"name": "@mikefreno/pygienium",
|
||||
"version": "0.1.1",
|
||||
"description": "Code hygiene extension for pi — isolated sub-agent checks that scan a target, apply fixes, and emit a findings+changes report.",
|
||||
"keywords": [
|
||||
"pi-package",
|
||||
@@ -51,5 +51,11 @@
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "bun test"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"src/",
|
||||
"agents/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ const OMP = join(HOME, ".omp", "agent", "extensions");
|
||||
|
||||
const SKIP = new Set([
|
||||
"port-to-omp.mjs",
|
||||
"release-tag.sh",
|
||||
".gitea",
|
||||
".github",
|
||||
"node_modules",
|
||||
@@ -217,6 +218,11 @@ omp install @mikefreno/omp-pygienium
|
||||
This is the omp port of [Mike/pygienium](https://git.freno.me/Mike/pygienium), regenerated automatically from the source repo. See the source repo for full documentation.
|
||||
`;
|
||||
|
||||
|
||||
// publish workflow emitted into the port repo so the omp package can be
|
||||
// released independently (tag push or manual dispatch on the omp repo).
|
||||
const PORT_PUBLISH_WORKFLOW = "name: publish\n\n# Publish this omp port package to the npm registry on version tags.\n#\n# Prerequisites:\n# - npm automation token (publish scope) stored as the repo secret NPM_TOKEN\n# - package name claimed on npm (@mikefreno/omp-<name>)\n#\n# Manual publish: Actions tab → Run workflow (workflow_dispatch).\n\non:\n push:\n tags: ['v*']\n workflow_dispatch:\n\njobs:\n publish:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup node (npm registry auth)\n uses: actions/setup-node@v4\n with:\n node-version: '22'\n registry-url: 'https://registry.npmjs.org'\n\n - name: Publish\n env:\n NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n run: npm publish --access public --ignore-scripts\n";
|
||||
|
||||
const FILE_RULES = {
|
||||
"src/index.ts": [
|
||||
{ from: " * Read the pygienium chat style from pi's settings.json.", to: " * Read the pygienium chat style from omp's settings.json." },
|
||||
@@ -321,6 +327,10 @@ function portExtension() {
|
||||
}
|
||||
}
|
||||
writeFileSync(join(dstDir, "tsconfig.json"), PYGENIUM_TSCONFIG);
|
||||
const wfDir = join(dstDir, ".gitea", "workflows");
|
||||
mkdirSync(wfDir, { recursive: true });
|
||||
writeFileSync(join(wfDir, "publish.yml"), PORT_PUBLISH_WORKFLOW);
|
||||
|
||||
console.log("== bun install (regenerates bun.lock + node_modules)");
|
||||
execSync("bun install", { cwd: dstDir, stdio: "inherit" });
|
||||
console.log("== done");
|
||||
|
||||
196
scripts/release-tag.sh
Executable file
196
scripts/release-tag.sh
Executable file
@@ -0,0 +1,196 @@
|
||||
#!/bin/bash
|
||||
|
||||
# release-tag.sh — version bump, commit, tag, and push for this pi extension.
|
||||
#
|
||||
# Mirrors the release flow from PodTui's scripts/release-tag.sh, adapted for
|
||||
# the pi extension repos: the version lives in package.json, and the omp port
|
||||
# derives its version from it during regeneration.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/release-tag.sh interactive release
|
||||
# scripts/release-tag.sh --dry-run plan the bump/tag/pushes without doing
|
||||
#
|
||||
# After this script runs:
|
||||
# - pushing master triggers the port-to-omp workflow, which regenerates and
|
||||
# pushes the omp port (picking up the new version)
|
||||
# - pushing the v* tag triggers the publish workflow, which npm-publishes
|
||||
# this package and its omp port together
|
||||
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
DRY_RUN=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
-h | --help)
|
||||
echo "usage: $0 [--dry-run]"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ! -d .git ] && [ ! -f .git ]; then
|
||||
echo -e "${RED}✗ not a git repository: $PROJECT_ROOT${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
echo -e "${RED}✗ working tree is dirty — commit or stash before releasing${NC}"
|
||||
git status --short
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NAME=$(python3 -c "import json;print(json.load(open('package.json'))['name'])")
|
||||
OMP_NAME=$(python3 -c "import json,re;n=json.load(open('package.json'))['name'];print(re.sub(r'^@mikefreno/(.+)$', r'@mikefreno/omp-\1', n))")
|
||||
CURRENT_VERSION=$(python3 -c "import json;print(json.load(open('package.json'))['version'])")
|
||||
|
||||
echo -e "${CYAN}Package:${NC} ${GREEN}${NAME}${NC}"
|
||||
echo -e "${CYAN}Current version:${NC} ${GREEN}v${CURRENT_VERSION}${NC}"
|
||||
echo ""
|
||||
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<<"$CURRENT_VERSION"
|
||||
MAJOR=$(echo "$MAJOR" | sed 's/[^0-9].*//')
|
||||
MINOR=$(echo "$MINOR" | sed 's/[^0-9].*//')
|
||||
PATCH=$(echo "$PATCH" | sed 's/[^0-9].*//')
|
||||
|
||||
echo -e "${CYAN}Select version bump type:${NC}"
|
||||
echo " 1) Major (breaking changes) ${MAJOR}.${MINOR}.${PATCH} → $((MAJOR + 1)).0.0"
|
||||
echo " 2) Minor (new features) ${MAJOR}.${MINOR}.${PATCH} → ${MAJOR}.$((MINOR + 1)).0"
|
||||
echo " 3) Patch (bug fixes) ${MAJOR}.${MINOR}.${PATCH} → ${MAJOR}.${MINOR}.$((PATCH + 1))"
|
||||
echo " 4) Custom version"
|
||||
echo " 5) Cancel"
|
||||
echo ""
|
||||
read -p "Enter choice (1-5): " -n 1 -r CHOICE
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
case $CHOICE in
|
||||
1) NEW_VERSION="$((MAJOR + 1)).0.0" ;;
|
||||
2) NEW_VERSION="${MAJOR}.$((MINOR + 1)).0" ;;
|
||||
3) NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" ;;
|
||||
4)
|
||||
read -p "Enter new version (e.g. 0.6.1): " -r NEW_VERSION
|
||||
echo ""
|
||||
;;
|
||||
5)
|
||||
echo -e "${YELLOW}Cancelled.${NC}"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}✗ invalid choice${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! echo "$NEW_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo -e "${RED}✗ version must be MAJOR.MINOR.PATCH (got: ${NEW_VERSION})${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git rev-parse -q --verify "refs/tags/v${NEW_VERSION}" >/dev/null; then
|
||||
echo -e "${RED}✗ tag v${NEW_VERSION} already exists${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${CYAN}New version:${NC} ${GREEN}v${NEW_VERSION}${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}This will:${NC}"
|
||||
echo " 1. Set package.json → \"version\": \"${NEW_VERSION}\""
|
||||
echo " 2. Commit the bump"
|
||||
echo " 3. Create annotated tag v${NEW_VERSION}"
|
||||
echo " 4. Push master and the tag to every remote"
|
||||
REMOTES=$(git remote)
|
||||
for r in $REMOTES; do
|
||||
echo " → $r"
|
||||
done
|
||||
echo ""
|
||||
echo -e "${YELLOW}Note: master push triggers the port-to-omp workflow; the v* tag${NC}"
|
||||
echo -e "push triggers the npm publish workflow (this package + omp port).${NC}"
|
||||
echo ""
|
||||
read -p "Proceed? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}Aborted.${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
echo -e "${BLUE}── dry run: no changes made ──${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Apply the bump ───────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${CYAN}[1/4]${NC} Updating package.json..."
|
||||
sed -i.bak "s/\"version\": \"[^\"]*\"/\"version\": \"${NEW_VERSION}\"/" package.json
|
||||
rm -f package.json.bak
|
||||
echo -e "${GREEN}✓ package.json updated${NC}"
|
||||
|
||||
if git diff --quiet -- package.json; then
|
||||
echo -e "${RED}✗ package.json did not change${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${CYAN}[2/4]${NC} Committing..."
|
||||
git add package.json
|
||||
git commit -m "chore: bump version to v${NEW_VERSION}"
|
||||
echo -e "${GREEN}✓ committed${NC}"
|
||||
|
||||
echo -e "${CYAN}[3/4]${NC} Tagging..."
|
||||
git tag -a "v${NEW_VERSION}" -m "${NAME} v${NEW_VERSION}"
|
||||
echo -e "${GREEN}✓ tagged v${NEW_VERSION}${NC}"
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}[4/4]${NC} Pushing..."
|
||||
FAILED=""
|
||||
for r in $REMOTES; do
|
||||
echo -e " → ${BLUE}${r}${NC} master..."
|
||||
if git push "$r" master >/dev/null 2>&1; then
|
||||
echo -e " ${GREEN}✓ master pushed${NC}"
|
||||
else
|
||||
echo -e " ${RED}✗ master push failed${NC}"
|
||||
FAILED="$FAILED $r"
|
||||
continue
|
||||
fi
|
||||
echo -e " → ${BLUE}${r}${NC} v${NEW_VERSION}..."
|
||||
if git push "$r" "v${NEW_VERSION}" >/dev/null 2>&1; then
|
||||
echo -e " ${GREEN}✓ tag pushed${NC}"
|
||||
else
|
||||
echo -e " ${RED}✗ tag push failed${NC}"
|
||||
FAILED="$FAILED $r"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [ -n "$FAILED" ]; then
|
||||
echo -e "${RED}✗ push failed for remote(s):${FAILED}${NC}"
|
||||
echo -e "${YELLOW}Re-run: git push <remote> master && git push <remote> v${NEW_VERSION}${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}═══════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN}✓ ${NAME} v${NEW_VERSION} released${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}Version:${NC} ${CURRENT_VERSION} → ${GREEN}${NEW_VERSION}${NC}"
|
||||
echo -e "${CYAN}Tag:${NC} v${NEW_VERSION}"
|
||||
echo ""
|
||||
echo -e "${BLUE}Next steps (automatic, nothing to do):${NC}"
|
||||
echo " 1. Gitea Actions port-to-omp regenerates and pushes the omp port"
|
||||
echo " (which now carries version ${NEW_VERSION})"
|
||||
echo " 2. Gitea Actions publish npm-publishes ${NAME} and"
|
||||
echo " ${OMP_NAME} to the npm registry"
|
||||
echo ""
|
||||
echo -e "${CYAN}Local port (optional):${NC} run 'bun port-to-omp.mjs' in this repo, or"
|
||||
echo "pull in ~/.omp once CI has pushed."
|
||||
@@ -243,7 +243,7 @@ async function runSessionToCompletion(
|
||||
session: AgentSession,
|
||||
opts: AgentTaskOptions,
|
||||
): Promise<AgentRunResult> {
|
||||
const acc: SessionEventAccumulator = { text: "" };
|
||||
const acc: SessionEventAccumulator = { text: "", sawMessage: false };
|
||||
try {
|
||||
const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
||||
applySessionEvent(acc, event, opts.onEvent);
|
||||
|
||||
@@ -21,14 +21,13 @@
|
||||
* @module pygienium/checks/deep-modules
|
||||
*/
|
||||
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
registerCheck,
|
||||
type CheckDefinition,
|
||||
type CheckScope,
|
||||
} from "./registry.js";
|
||||
import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
||||
import { hasScopeSources, scopeRulesMarkdown } from "./scope.js";
|
||||
|
||||
/** Output directory for this check's persistent reports. */
|
||||
export function deepModulesOutputDir(cwd: string): string {
|
||||
@@ -50,23 +49,13 @@ export function changesPath(cwd: string): string {
|
||||
* with zero source files gives the scanner nothing to classify.
|
||||
*/
|
||||
function deepModulesGate(cwd: string): string | undefined {
|
||||
let found = false;
|
||||
try {
|
||||
const entries = readdirSync(cwd);
|
||||
for (const entry of entries) {
|
||||
if (isScopeSource(entry)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasScopeSources(cwd)) return undefined;
|
||||
} catch {
|
||||
// unreadable cwd → let the agent decide; don't block.
|
||||
return undefined;
|
||||
}
|
||||
if (!found) {
|
||||
return "no source files found to inspect";
|
||||
}
|
||||
return undefined;
|
||||
return "no source files found to inspect";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,14 +41,13 @@
|
||||
* @module pygienium/checks/defensive-guards
|
||||
*/
|
||||
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
registerCheck,
|
||||
type CheckDefinition,
|
||||
type CheckScope,
|
||||
} from "./registry.js";
|
||||
import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
||||
import { hasScopeSources, scopeRulesMarkdown } from "./scope.js";
|
||||
|
||||
/** Output directory for this check's persistent reports. */
|
||||
export function defensiveGuardsOutputDir(cwd: string): string {
|
||||
@@ -70,23 +69,13 @@ export function changesPath(cwd: string): string {
|
||||
* with zero source files gives the scanner nothing to analyse.
|
||||
*/
|
||||
function defensiveGuardsGate(cwd: string): string | undefined {
|
||||
let found = false;
|
||||
try {
|
||||
const entries = readdirSync(cwd);
|
||||
for (const entry of entries) {
|
||||
if (isScopeSource(entry)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasScopeSources(cwd)) return undefined;
|
||||
} catch {
|
||||
// unreadable cwd → let the agent decide; don't block.
|
||||
return undefined;
|
||||
}
|
||||
if (!found) {
|
||||
return "no source files found to inspect";
|
||||
}
|
||||
return undefined;
|
||||
return "no source files found to inspect";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
* @module pygienium/checks/scope
|
||||
*/
|
||||
|
||||
import { readdirSync, statSync } from "node:fs";
|
||||
import type { Dirent } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* Implementation-code file extensions pygienium inspects.
|
||||
*
|
||||
@@ -106,6 +110,38 @@ export function isScopeSource(path: string): boolean {
|
||||
return SCOPE_EXTENSIONS.has(lower.slice(dot));
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the tree rooted at `root` contains at least one in-scope source
|
||||
* file. Walks recursively (honoring {@link SCOPE_EXCLUDE_DIRS}) — a top-level
|
||||
* entry scan alone would skip any repo whose source lives in subdirectories,
|
||||
* e.g. `game/` or `src/`, even though recon's git inventory finds hundreds of
|
||||
* files. A file root is judged by {@link isScopeSource} directly.
|
||||
*/
|
||||
export function hasScopeSources(root: string): boolean {
|
||||
const st = statSync(root, { throwIfNoEntry: false });
|
||||
if (!st) return false;
|
||||
if (st.isFile()) return isScopeSource(root);
|
||||
const stack = [root];
|
||||
while (stack.length > 0) {
|
||||
const dir = stack.pop() as string;
|
||||
let entries: Dirent[];
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (SCOPE_EXCLUDE_DIRS.has(entry.name)) continue;
|
||||
stack.push(join(dir, entry.name));
|
||||
} else if (entry.isFile() && isScopeSource(entry.name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Markdown section injected into every scan task string so the sub-agent knows
|
||||
* exactly what to inspect and what to skip — stated once here, not copy-pasted
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
* @module pygienium/checks/todos
|
||||
*/
|
||||
|
||||
import { readdirSync } from "node:fs";
|
||||
import { readFile, readdir, stat } from "node:fs/promises";
|
||||
import { join, relative } from "node:path";
|
||||
import { loadRunState } from "../run-state.js";
|
||||
@@ -46,6 +45,7 @@ import {
|
||||
type CheckScope,
|
||||
} from "./registry.js";
|
||||
import {
|
||||
hasScopeSources,
|
||||
isScopeSource,
|
||||
SCOPE_EXCLUDE_DIRS,
|
||||
scopeRulesMarkdown,
|
||||
@@ -405,23 +405,13 @@ function renderFindings(
|
||||
* with zero source files gives the scanner nothing to analyse.
|
||||
*/
|
||||
function todosGate(cwd: string): string | undefined {
|
||||
let found = false;
|
||||
try {
|
||||
const entries = readdirSync(cwd);
|
||||
for (const entry of entries) {
|
||||
if (isScopeSource(entry)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasScopeSources(cwd)) return undefined;
|
||||
} catch {
|
||||
// unreadable cwd → let the agent decide; don't block.
|
||||
return undefined;
|
||||
}
|
||||
if (!found) {
|
||||
return "no source files found to inspect";
|
||||
}
|
||||
return undefined;
|
||||
return "no source files found to inspect";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -168,4 +168,17 @@ describe("deep-modules check", () => {
|
||||
await rm(empty, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
it("does not skip when sources live in subdirectories (gate scans recursively)", async () => {
|
||||
// Top level holds only a directory — the old shallow gate skipped any
|
||||
// repo whose source lives under `game/`/`src/`-style subdirs.
|
||||
await mkdir(join(cwd, "game"), { recursive: true });
|
||||
await writeFile(join(cwd, "game", "main.lua"), "return {}\n", "utf8");
|
||||
const check = getCheck("deep-modules")!;
|
||||
|
||||
await handleCheckCommand(check, "", stubCtx(cwd));
|
||||
|
||||
const state = await loadRunState(cwd);
|
||||
expect(state?.checks["deep-modules"]?.status).toBe("complete");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -229,4 +229,17 @@ describe("defensive-guards check", () => {
|
||||
const state = await loadRunState(cwd);
|
||||
expect(state?.checks["defensive-guards"]?.status).toBe("skipped");
|
||||
});
|
||||
|
||||
it("does not skip when sources live in subdirectories (gate scans recursively)", async () => {
|
||||
// Top level holds only a directory — the old shallow gate skipped any
|
||||
// repo whose source lives under `game/`/`src/`-style subdirs.
|
||||
await mkdir(join(cwd, "game"), { recursive: true });
|
||||
await writeFile(join(cwd, "game", "main.lua"), "return {}\n", "utf8");
|
||||
const check = getCheck("defensive-guards")!;
|
||||
|
||||
await handleCheckCommand(check, "", stubCtx(cwd));
|
||||
|
||||
const state = await loadRunState(cwd);
|
||||
expect(state?.checks["defensive-guards"]?.status).toBe("complete");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -335,6 +335,19 @@ describe("todos check", () => {
|
||||
expect(state?.checks["todos"]?.status).toBe("skipped");
|
||||
});
|
||||
|
||||
it("does not skip when sources live in subdirectories (gate scans recursively)", async () => {
|
||||
// Top level holds only a directory — the old shallow gate skipped any
|
||||
// repo whose source lives under `game/`/`src/`-style subdirs.
|
||||
await mkdir(join(cwd, "game"), { recursive: true });
|
||||
await writeFile(join(cwd, "game", "main.lua"), "return {}\n", "utf8");
|
||||
const check = getCheck("todos")!;
|
||||
|
||||
await handleCheckCommand(check, "", stubCtx(cwd));
|
||||
|
||||
const state = await loadRunState(cwd);
|
||||
expect(state?.checks["todos"]?.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("reports a new/resolved delta against the previous run's counts", async () => {
|
||||
await seedStubs(cwd);
|
||||
const check = getCheck("todos")!;
|
||||
|
||||
Reference in New Issue
Block a user