javi-forge 1.26.0 → 1.28.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.
Files changed (46) hide show
  1. package/ci-local/hooks/commit-msg +7 -0
  2. package/ci-local/hooks/pre-commit +8 -0
  3. package/ci-local/hooks/pre-push +8 -0
  4. package/dist/cli/dispatch/ci.js +1 -1
  5. package/dist/cli/dispatch/simple-renderers.js +1 -1
  6. package/dist/cli/dispatch/skills-cmd.js +9 -1
  7. package/dist/cli/help.d.ts +1 -1
  8. package/dist/cli/help.js +12 -0
  9. package/dist/commands/ci.js +5 -1
  10. package/dist/commands/doctor.js +9 -0
  11. package/dist/commands/init/steps/ghagga.d.ts +3 -4
  12. package/dist/commands/init/steps/ghagga.js +5 -15
  13. package/dist/commands/plugin.d.ts +4 -2
  14. package/dist/commands/plugin.js +4 -4
  15. package/dist/commands/skills/analysis.js +31 -2
  16. package/dist/commands/skills/benchmark.js +10 -0
  17. package/dist/commands/skills/constants.d.ts +5 -0
  18. package/dist/commands/skills/constants.js +5 -0
  19. package/dist/commands/skills/parsing.d.ts +18 -3
  20. package/dist/commands/skills/parsing.js +29 -3
  21. package/dist/commands/skills/scoring.d.ts +7 -6
  22. package/dist/commands/skills/scoring.js +31 -1
  23. package/dist/lib/agent-skills.d.ts +1 -0
  24. package/dist/lib/agent-skills.js +155 -1
  25. package/dist/lib/auto-skill-install.d.ts +5 -0
  26. package/dist/lib/auto-skill-install.js +40 -2
  27. package/dist/lib/context.d.ts +22 -0
  28. package/dist/lib/context.js +120 -79
  29. package/dist/lib/plugin.d.ts +1 -0
  30. package/dist/lib/plugin.js +58 -1
  31. package/dist/lib/safe-read.d.ts +62 -0
  32. package/dist/lib/safe-read.js +221 -0
  33. package/dist/lib/security-analysis.d.ts +19 -2
  34. package/dist/lib/security-analysis.js +65 -13
  35. package/dist/lib/skill-install-gate.d.ts +31 -0
  36. package/dist/lib/skill-install-gate.js +30 -0
  37. package/dist/lib/skill-scanner.d.ts +65 -1
  38. package/dist/lib/skill-scanner.js +307 -4
  39. package/dist/types/index.d.ts +18 -0
  40. package/dist/ui/AutoSkills.d.ts +3 -1
  41. package/dist/ui/AutoSkills.js +17 -2
  42. package/dist/ui/Plugin.d.ts +3 -1
  43. package/dist/ui/Plugin.js +4 -4
  44. package/dist/ui/Skills.js +12 -7
  45. package/package.json +9 -5
  46. package/templates/github/ghagga-review.yml +0 -30
@@ -1,6 +1,13 @@
1
1
  #!/bin/bash
2
2
  set -e
3
3
  # =============================================================================
4
+ # LEGACY — retained for reference only (hook-consolidation).
5
+ # The canonical hook mechanism is now `.git/hooks/` written by `installCIHooks`
6
+ # (javi-forge ci init / init). The shipped commit-msg shim lives in
7
+ # assets/hooks/commit-msg; `javi-forge init` no longer copies this directory or
8
+ # sets `core.hooksPath=ci-local/hooks`. This body is kept for historical
9
+ # reference and is NOT the source of truth. See docs/commands.md → hooks.
10
+ # =============================================================================
4
11
  # COMMIT-MSG: Block accidental AI-attribution in commit messages
5
12
  # =============================================================================
6
13
  # Best-effort, NOT an adversarial-grade security control.
@@ -1,5 +1,13 @@
1
1
  #!/bin/bash
2
2
  # =============================================================================
3
+ # LEGACY — retained for reference only (hook-consolidation).
4
+ # The canonical hook mechanism is now `.git/hooks/` written by `installCIHooks`
5
+ # (javi-forge ci init / init), whose static shims exec the dispatcher
6
+ # `javi-forge hooks run <name>`. `javi-forge init` no longer copies this
7
+ # directory or sets `core.hooksPath=ci-local/hooks`; a repo still on that legacy
8
+ # path is auto-migrated on the next install. This body is kept for historical
9
+ # reference and is NOT the source of truth. See docs/commands.md → hooks.
10
+ # =============================================================================
3
11
  # PRE-COMMIT: Quick CI check via javi-forge
4
12
  # =============================================================================
5
13
  # Requires: npm install -g javi-forge
@@ -1,5 +1,13 @@
1
1
  #!/bin/bash
2
2
  # =============================================================================
3
+ # LEGACY — retained for reference only (hook-consolidation).
4
+ # The canonical hook mechanism is now `.git/hooks/` written by `installCIHooks`
5
+ # (javi-forge ci init / init), whose static shims exec the dispatcher
6
+ # `javi-forge hooks run <name>`. `javi-forge init` no longer copies this
7
+ # directory or sets `core.hooksPath=ci-local/hooks`; a repo still on that legacy
8
+ # path is auto-migrated on the next install. This body is kept for historical
9
+ # reference and is NOT the source of truth. See docs/commands.md → hooks.
10
+ # =============================================================================
3
11
  # PRE-PUSH: Full CI simulation via javi-forge
4
12
  # =============================================================================
5
13
  # Requires: npm install -g javi-forge
@@ -83,7 +83,7 @@ export async function handleCi(cli, ctx) {
83
83
  }
84
84
  if (installed.length > 0) {
85
85
  console.log(`✓ Installed git hooks: ${installed.join(", ")}`);
86
- console.log(" Hooks call javi-forge ci (with npx fallback)");
86
+ console.log(" Hooks call javi-forge hooks run (with npx fallback)");
87
87
  }
88
88
  // Upgrades are reported DISTINCTLY from fresh installs: replacing an
89
89
  // older javi-forge hook is not the same event as writing a new one.
@@ -46,7 +46,7 @@ export function handlePlugin(cli, ctx) {
46
46
  : "list";
47
47
  const target = cli.input[2];
48
48
  render(React.createElement(CIContextProvider, { isCI: ctx.isCI },
49
- React.createElement(Plugin, { action: action, target: target, dryRun: cli.flags.dryRun, codex: cli.flags.codex })), { stdin: ctx.inkStdin });
49
+ React.createElement(Plugin, { action: action, target: target, dryRun: cli.flags.dryRun, codex: cli.flags.codex, force: cli.flags.force })), { stdin: ctx.inkStdin });
50
50
  }
51
51
  export function handleInitDefault(cli, ctx) {
52
52
  const presetStack = VALID_STACKS.includes(cli.flags.stack)
@@ -37,7 +37,7 @@ export async function handleSkillsCmd(cli, ctx) {
37
37
  // Auto / auto-install: render interactive Ink UI
38
38
  if (skillsAction === "auto" || skillsAction === "auto-install") {
39
39
  render(React.createElement(CIContextProvider, { isCI: ctx.isCI },
40
- React.createElement(AutoSkills, { projectDir: process.cwd(), skillsDir: cli.flags.skillsDir || undefined, dryRun: cli.flags.dryRun })), { stdin: ctx.inkStdin });
40
+ React.createElement(AutoSkills, { projectDir: process.cwd(), skillsDir: cli.flags.skillsDir || undefined, dryRun: cli.flags.dryRun, force: cli.flags.force })), { stdin: ctx.inkStdin });
41
41
  return;
42
42
  }
43
43
  // Score and benchmark are non-interactive CLI commands
@@ -57,6 +57,10 @@ export async function handleSkillsCmd(cli, ctx) {
57
57
  console.error(`\u2717 Skill not found: ${skillPath}`);
58
58
  process.exit(1);
59
59
  }
60
+ if (result.unread) {
61
+ console.error(`\u2717 Skill could not be scored \u2014 ${result.unread}: ${skillPath}`);
62
+ process.exit(1);
63
+ }
60
64
  console.log(`\nSkill: ${result.skillName}`);
61
65
  console.log(` Completeness: ${result.completeness}/100`);
62
66
  console.log(` Clarity: ${result.clarity}/100`);
@@ -77,6 +81,10 @@ export async function handleSkillsCmd(cli, ctx) {
77
81
  console.error(`\u2717 Skill not found: ${skillPath}`);
78
82
  process.exit(1);
79
83
  }
84
+ if (result.unread) {
85
+ console.error(`\u2717 Skill could not be benchmarked \u2014 ${result.unread}: ${skillPath}`);
86
+ process.exit(1);
87
+ }
80
88
  console.log(`\nBenchmark: ${result.skillName}`);
81
89
  for (const check of result.checks) {
82
90
  const icon = check.passed ? "\u2713" : "\u2717";
@@ -8,7 +8,7 @@
8
8
  * Help banner shown by meow when `--help` is passed or invalid args are supplied.
9
9
  * Multi-line template literal — preserve exact formatting (whitespace is significant).
10
10
  */
11
- export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Enable the TDD pre-commit section + install managed hooks\n tdd pipeline Enable the TDD pre-push section (--mode strict|warn)\n hooks run Run a git hook's composed sections (pre-commit | pre-push)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
11
+ export declare const HELP_TEXT = "\n Usage\n $ javi-forge [command] [options]\n\n Commands\n init Bootstrap a new project (default)\n ci Run CI simulation (lint + compile + test + security + ghagga)\n ci validate Validate .javi-forge/ci.yaml without running anything\n ci init Install git hooks that call javi-forge ci\n tdd init Enable the TDD pre-commit section + install managed hooks\n tdd pipeline Enable the TDD pre-push section (--mode strict|warn)\n hooks run Run a git hook's composed sections (pre-commit | pre-push)\n analyze Run repoforge skills analysis\n doctor Show health report\n workflow show Render a workflow graph as ASCII (--template <name> or file path)\n workflow validate Validate project state against a workflow graph\n workflow list List available workflows and built-in templates\n plugin add Install a plugin from GitHub (org/repo)\n plugin remove Remove an installed plugin\n plugin list List installed plugins\n plugin search Search the plugin registry\n plugin validate Validate a local plugin directory\n plugin sync Auto-detect and wire installed plugins\n plugin export Export plugin to Agent Skills spec format (skills.json)\n plugin export --codex: Export plugin to Codex-compatible TOML subagent files\n plugin export-skills Generate aggregated skills.json from all installed plugins\n plugin export-skills global Generate global skills.json from all globally installed plugins\n plugin import Import an Agent Skills spec package as a javi-forge plugin\n skills doctor Show skills health report (add --deep for conflict detection)\n skills budget Show token cost of loaded skills (add -b N for custom budget)\n skills score Score a skill on quality dimensions (completeness, clarity, testability, token-efficiency)\n skills benchmark Benchmark a skill with structural quality checks\n skills auto Auto-detect project stack and suggest/install matching AI skills\n skills auto-install Alias for skills auto\n skill publish Package a skill directory for marketplace distribution (generates plugin.json)\n security baseline Create security baseline from current audit findings\n security check Check for regressions against baseline (exits non-zero if found)\n security update Re-snapshot baseline (acknowledge current vulns)\n security allowlist Add all current findings to the allowlist (suppress in future checks)\n llms-txt Generate AI-friendly llms.txt for current project\n\n Options\n --dry-run Preview changes without writing files\n --stack Project stack (node, python, go, rust, java-gradle, java-maven, elixir)\n --ci CI provider (github, gitlab, woodpecker)\n --memory Memory module (engram, obsidian-brain, memory-simple, none)\n --project-name Project name (skips name prompt)\n --ghagga Enable GHAGGA review system\n --mock Enable mock-first mode (no real API keys needed)\n --local-ai Include local AI dev stack (Ollama + Docker Compose)\n --batch Non-interactive mode (auto-proceed, no keyboard input)\n --deep Enable deep analysis (conflict + duplicate detection)\n --budget, -b Token budget limit for skills (default: 8000)\n --skills-dir Custom skills directory path\n --author Author name for skill publish\n --repo Repository URL for skill publish\n --version Show version\n --help Show this help\n\n CI options (javi-forge ci)\n --quick Lint + compile only (fast, for pre-commit)\n --shell Open interactive shell in CI container\n --detect Show detected stack and exit\n --config PATH Load ordered CI runners from a versioned config file\n (default discovery: .javi-forge/ci.yaml)\n --stack STACK Force a single explicit stack (single-stack repos only \u2014\n insufficient for hybrid repos; use --config instead)\n --no-docker Run commands natively (no Docker)\n --no-ci-ghagga Skip GHAGGA review\n --no-security Skip Semgrep security scan\n --timeout N Per-step timeout in seconds (default: 600)\n\n CI hooks (javi-forge ci init)\n Install git hooks that call javi-forge ci.\n No files copied \u2014 hooks reference the global CLI.\n Existing hooks javi-forge did not write are refused, never clobbered.\n --force Overwrite a foreign or locally modified hook. The previous\n content is copied to a .bak sibling first; if that backup\n cannot be written, the hook is left untouched. Symlinked\n hook paths are refused even with --force.\n\n SkillGuard install gate (plugin add / plugin import / skills auto)\n Every install is scanned before anything is written. Refusals are\n fail-closed and name the offending files:\n - SKILL.md files that block (critical threats) are refused \u2014 always.\n - Unscannable files (binary, oversized, unreadable) are refused unless\n --force is given.\n - Symlinks anywhere in the tree and SKILL.md files outside the declared\n set are manifest-integrity refusals \u2014 they are refused even with --force.\n - Empty or missing skills.json `skills` array on import is refused.\n\n Examples\n $ javi-forge\n $ javi-forge init --dry-run\n $ javi-forge init --stack node --ci github\n $ javi-forge ci\n $ javi-forge ci init\n $ javi-forge ci init --force\n $ javi-forge plugin add org/repo\n $ javi-forge plugin add org/repo --force\n $ javi-forge tdd init\n $ javi-forge ci --quick\n $ javi-forge ci --no-ci-ghagga --no-security\n $ javi-forge ci --no-docker\n $ javi-forge ci --shell\n $ javi-forge ci --config .javi-forge/ci.yaml\n $ javi-forge ci validate\n $ javi-forge ci --help\n $ javi-forge analyze\n $ javi-forge doctor\n $ javi-forge plugin add mapbox/agent-skills\n $ javi-forge plugin list\n";
12
12
  /**
13
13
  * Per-command help for `ci`, shown by `javi-forge ci --help` (or when `ci` is
14
14
  * given an unknown subcommand). Kept consistent with the global HELP_TEXT
package/dist/cli/help.js CHANGED
@@ -89,6 +89,16 @@ export const HELP_TEXT = `
89
89
  cannot be written, the hook is left untouched. Symlinked
90
90
  hook paths are refused even with --force.
91
91
 
92
+ SkillGuard install gate (plugin add / plugin import / skills auto)
93
+ Every install is scanned before anything is written. Refusals are
94
+ fail-closed and name the offending files:
95
+ - SKILL.md files that block (critical threats) are refused — always.
96
+ - Unscannable files (binary, oversized, unreadable) are refused unless
97
+ --force is given.
98
+ - Symlinks anywhere in the tree and SKILL.md files outside the declared
99
+ set are manifest-integrity refusals — they are refused even with --force.
100
+ - Empty or missing skills.json \`skills\` array on import is refused.
101
+
92
102
  Examples
93
103
  $ javi-forge
94
104
  $ javi-forge init --dry-run
@@ -96,6 +106,8 @@ export const HELP_TEXT = `
96
106
  $ javi-forge ci
97
107
  $ javi-forge ci init
98
108
  $ javi-forge ci init --force
109
+ $ javi-forge plugin add org/repo
110
+ $ javi-forge plugin add org/repo --force
99
111
  $ javi-forge tdd init
100
112
  $ javi-forge ci --quick
101
113
  $ javi-forge ci --no-ci-ghagga --no-security
@@ -487,7 +487,11 @@ export async function runCI(options, onStep, onGateOutcome) {
487
487
  try {
488
488
  const ctxResult = await refreshContextDir(projectDir);
489
489
  if (ctxResult) {
490
- report(onStep, stepContext, "Refresh .context/ directory", "done", "INDEX.md + summary.md updated");
490
+ // An unreadable manifest is reported, not silently treated as no deps.
491
+ const detail = ctxResult.warnings.length > 0
492
+ ? `INDEX.md + summary.md updated (manifest warnings: ${ctxResult.warnings.join("; ")})`
493
+ : "INDEX.md + summary.md updated";
494
+ report(onStep, stepContext, "Refresh .context/ directory", "done", detail);
491
495
  }
492
496
  else {
493
497
  report(onStep, stepContext, "Refresh .context/ directory", "skipped", "no .context/ or no manifest");
@@ -319,6 +319,15 @@ export async function runDoctor(projectDir) {
319
319
  status: "ok",
320
320
  detail: "INDEX.md + summary.md updated",
321
321
  });
322
+ // A manifest that could not be read is not "no dependencies" — surface
323
+ // each warning so an unreadable/oversized/invalid manifest is visible.
324
+ for (const warning of result.warnings) {
325
+ contextChecks.push({
326
+ label: "dependency manifest",
327
+ status: "fail",
328
+ detail: warning,
329
+ });
330
+ }
322
331
  }
323
332
  else {
324
333
  contextChecks.push({
@@ -1,15 +1,14 @@
1
1
  import type { StepFn } from "../types.js";
2
2
  /**
3
- * Step 9: Install GHAGGA review system.
3
+ * Step 9: Install the local GHAGGA review module.
4
4
  *
5
5
  * - When ghagga is false, reports "skipped".
6
6
  * - Copies <MODULES_DIR>/ghagga → <project>/.javi-forge/modules/ghagga (no overwrite).
7
- * - For GitHub provider, also copies the ghagga-review.yml caller workflow into
8
- * <project>/.github/workflows/.
9
7
  * - If module source dir is missing, reports "error" with "module not found".
10
8
  * - Errors are swallowed and reported as status:"error" — never thrown.
11
9
  *
12
- * Extracted VERBATIM from src/commands/init.ts (PR 3 of 6).
10
+ * The GitHub Action review workflow is intentionally NOT scaffolded — ghagga
11
+ * runs locally/self-hosted, not as a GitHub Action.
13
12
  */
14
13
  export declare const stepGhagga: StepFn;
15
14
  //# sourceMappingURL=ghagga.d.ts.map
@@ -1,23 +1,22 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
- import { FORGE_ROOT, MODULES_DIR } from "../../../constants.js";
3
+ import { MODULES_DIR } from "../../../constants.js";
4
4
  import { ensureDirExists } from "../../../lib/common.js";
5
5
  import { report } from "../report.js";
6
6
  /**
7
- * Step 9: Install GHAGGA review system.
7
+ * Step 9: Install the local GHAGGA review module.
8
8
  *
9
9
  * - When ghagga is false, reports "skipped".
10
10
  * - Copies <MODULES_DIR>/ghagga → <project>/.javi-forge/modules/ghagga (no overwrite).
11
- * - For GitHub provider, also copies the ghagga-review.yml caller workflow into
12
- * <project>/.github/workflows/.
13
11
  * - If module source dir is missing, reports "error" with "module not found".
14
12
  * - Errors are swallowed and reported as status:"error" — never thrown.
15
13
  *
16
- * Extracted VERBATIM from src/commands/init.ts (PR 3 of 6).
14
+ * The GitHub Action review workflow is intentionally NOT scaffolded — ghagga
15
+ * runs locally/self-hosted, not as a GitHub Action.
17
16
  */
18
17
  export const stepGhagga = async (ctx) => {
19
18
  const { projectDir, dryRun, onStep, options } = ctx;
20
- const { ghagga, ciProvider } = options;
19
+ const { ghagga } = options;
21
20
  const stepId = "ghagga";
22
21
  report(onStep, stepId, "Install GHAGGA review system", "running");
23
22
  try {
@@ -31,15 +30,6 @@ export const stepGhagga = async (ctx) => {
31
30
  overwrite: false,
32
31
  errorOnExist: false,
33
32
  });
34
- // Copy ghagga caller workflow to CI provider location
35
- if (ciProvider === "github") {
36
- const workflowSrc = path.join(FORGE_ROOT, "templates", "github", "ghagga-review.yml");
37
- if (await fs.pathExists(workflowSrc)) {
38
- const workflowDest = path.join(projectDir, ".github", "workflows", "ghagga-review.yml");
39
- await ensureDirExists(path.dirname(workflowDest));
40
- await fs.copy(workflowSrc, workflowDest, { overwrite: false });
41
- }
42
- }
43
33
  }
44
34
  report(onStep, stepId, "Install GHAGGA review system", "done");
45
35
  }
@@ -3,7 +3,9 @@ type StepCallback = (step: InitStep) => void;
3
3
  /**
4
4
  * Add (install) a plugin from a GitHub source.
5
5
  */
6
- export declare function runPluginAdd(source: string, dryRun: boolean, onStep: StepCallback): Promise<void>;
6
+ export declare function runPluginAdd(source: string, dryRun: boolean, onStep: StepCallback, options?: {
7
+ force?: boolean;
8
+ }): Promise<void>;
7
9
  /**
8
10
  * Remove an installed plugin by name.
9
11
  */
@@ -35,7 +37,7 @@ export declare function runPluginExportCodex(name: string, onStep: StepCallback)
35
37
  /**
36
38
  * Import an Agent Skills spec package and convert to javi-forge plugin format.
37
39
  */
38
- export declare function runPluginImport(sourceDir: string, dryRun: boolean, onStep: StepCallback): Promise<void>;
40
+ export declare function runPluginImport(sourceDir: string, dryRun: boolean, onStep: StepCallback, force?: boolean): Promise<void>;
39
41
  /**
40
42
  * Generate a project-level skills.json from all installed plugins.
41
43
  * Makes the project discoverable by `npx skills add` and 40+ AI agents.
@@ -7,10 +7,10 @@ function report(onStep, id, label, status, detail) {
7
7
  /**
8
8
  * Add (install) a plugin from a GitHub source.
9
9
  */
10
- export async function runPluginAdd(source, dryRun, onStep) {
10
+ export async function runPluginAdd(source, dryRun, onStep, options = {}) {
11
11
  const stepId = "plugin-add";
12
12
  report(onStep, stepId, `Install plugin: ${source}`, "running");
13
- const result = await installPlugin(source, { dryRun });
13
+ const result = await installPlugin(source, { dryRun, force: options.force });
14
14
  if (result.success) {
15
15
  report(onStep, stepId, `Install plugin: ${source}`, "done", dryRun
16
16
  ? `dry-run: would install ${result.name}`
@@ -145,10 +145,10 @@ export async function runPluginExportCodex(name, onStep) {
145
145
  /**
146
146
  * Import an Agent Skills spec package and convert to javi-forge plugin format.
147
147
  */
148
- export async function runPluginImport(sourceDir, dryRun, onStep) {
148
+ export async function runPluginImport(sourceDir, dryRun, onStep, force = false) {
149
149
  const stepId = "plugin-import";
150
150
  report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "running");
151
- const result = await importAgentSkillsPackage(sourceDir, { dryRun });
151
+ const result = await importAgentSkillsPackage(sourceDir, { dryRun, force });
152
152
  if (result.success) {
153
153
  report(onStep, stepId, `Import agent-skills package: ${sourceDir}`, "done", dryRun
154
154
  ? `dry-run: would import ${result.name}`
@@ -1,4 +1,5 @@
1
- import { CONTRADICTION_PAIRS, DEFAULT_BUDGET } from "./constants.js";
1
+ import fs from "fs-extra";
2
+ import { CHARS_PER_TOKEN, CONTRADICTION_PAIRS, DEFAULT_BUDGET, } from "./constants.js";
2
3
  import { detectDirectiveClash } from "./directives.js";
3
4
  import { discoverSkills, estimateTokens, parseSkillFile } from "./parsing.js";
4
5
  // ── Budget Optimization ─────────────────────────────────────────────────────
@@ -98,6 +99,12 @@ export async function findConflicts(skillsDir) {
98
99
  const parsed = await parseSkillFile(sp);
99
100
  if (!parsed)
100
101
  continue;
102
+ // A skipped file has no readable rules to compare. Don't treat its empty
103
+ // rule list as "no conflicts" — it is surfaced as a skip in the budget
104
+ // section (calculateBudget always runs alongside conflict detection), so
105
+ // skip it here explicitly rather than letting it pass silently.
106
+ if (parsed.skip)
107
+ continue;
101
108
  for (const rule of parsed.rules) {
102
109
  allRules.push({
103
110
  skillName: parsed.name,
@@ -138,10 +145,32 @@ export async function calculateBudget(skillsDir, budget = DEFAULT_BUDGET) {
138
145
  const parsed = await parseSkillFile(sp);
139
146
  if (!parsed)
140
147
  continue;
148
+ // Token count reflects the bytes actually kept by the guarded read, so a
149
+ // skipped or truncated skill is reported rather than counted as normal.
150
+ const note = parsed.skip
151
+ ? `skipped: ${parsed.skip.message}`
152
+ : parsed.truncated
153
+ ? "truncated: read budget reached"
154
+ : undefined;
155
+ // A skipped skill has empty `rawContent`, so token estimation from it would
156
+ // be 0 and the file would sort last — the budget tool would hide the very
157
+ // oversized file it exists to flag. Fall back to the real on-disk byte size
158
+ // so an unread skill still counts toward the total and over-budget check.
159
+ let tokens = estimateTokens(parsed.rawContent);
160
+ if (parsed.skip) {
161
+ try {
162
+ const { size } = await fs.stat(sp);
163
+ tokens = Math.ceil(size / CHARS_PER_TOKEN);
164
+ }
165
+ catch {
166
+ // Vanished between discovery and stat — leave 0; the note still flags it.
167
+ }
168
+ }
141
169
  entries.push({
142
170
  skillName: parsed.name,
143
171
  skillPath: sp,
144
- tokens: estimateTokens(parsed.rawContent),
172
+ tokens,
173
+ ...(note ? { note } : {}),
145
174
  });
146
175
  }
147
176
  // Sort by token count descending (biggest consumers first)
@@ -9,6 +9,16 @@ export async function benchmarkSkill(skillPath) {
9
9
  const parsed = await parseSkillFile(skillPath);
10
10
  if (!parsed)
11
11
  return null;
12
+ // A file that could not be read has no structure to benchmark. Reporting it
13
+ // as a skill that failed every check is misleading — mark it unread instead.
14
+ if (parsed.skip) {
15
+ return {
16
+ skillName: parsed.name,
17
+ checks: [],
18
+ passRate: 0,
19
+ unread: parsed.skip.message,
20
+ };
21
+ }
12
22
  const checks = [];
13
23
  // Check 1: Has YAML frontmatter with name
14
24
  checks.push({
@@ -6,5 +6,10 @@ export declare const CHARS_PER_TOKEN = 4;
6
6
  export declare const DEFAULT_THRESHOLD = 50;
7
7
  /** Default registry quality threshold */
8
8
  export declare const DEFAULT_REGISTRY_THRESHOLD = 60;
9
+ /**
10
+ * Hard ceiling for a SKILL.md. A skill file past this is not a skill — it is a
11
+ * dumped log or a vendored bundle — so it is skipped instead of truncated.
12
+ */
13
+ export declare const MAX_SKILL_BYTES: number;
9
14
  export declare const CONTRADICTION_PAIRS: [RegExp, RegExp][];
10
15
  //# sourceMappingURL=constants.d.ts.map
@@ -8,6 +8,11 @@ export const CHARS_PER_TOKEN = 4;
8
8
  export const DEFAULT_THRESHOLD = 50;
9
9
  /** Default registry quality threshold */
10
10
  export const DEFAULT_REGISTRY_THRESHOLD = 60;
11
+ /**
12
+ * Hard ceiling for a SKILL.md. A skill file past this is not a skill — it is a
13
+ * dumped log or a vendored bundle — so it is skipped instead of truncated.
14
+ */
15
+ export const MAX_SKILL_BYTES = 1024 * 1024;
11
16
  // ── Contradiction keywords (pairs that signal opposite intent) ───────────────
12
17
  export const CONTRADICTION_PAIRS = [
13
18
  [/\buse semicolons\b/i, /\bno semicolons\b/i],
@@ -1,12 +1,27 @@
1
+ import { type SafeReadFailureReason } from "../../lib/safe-read.js";
1
2
  /** Estimate token count from a string */
2
3
  export declare function estimateTokens(text: string): number;
3
- /** Read a SKILL.md and extract its name + critical rules section */
4
- export declare function parseSkillFile(skillPath: string): Promise<{
4
+ /** Why a SKILL.md could not be analysed reportable, never thrown */
5
+ export interface SkillReadSkip {
6
+ reason: SafeReadFailureReason;
7
+ message: string;
8
+ }
9
+ export interface ParsedSkillFile {
5
10
  name: string;
6
11
  rules: string[];
7
12
  rawContent: string;
8
13
  triggers: string[];
9
- } | null>;
14
+ /**
15
+ * Non-null when the file could not be read as analysable text (binary,
16
+ * oversized, permission denied). `rawContent` is empty in that case, so
17
+ * callers can report the skip instead of scoring an empty skill.
18
+ */
19
+ skip: SkillReadSkip | null;
20
+ /** True when the file was longer than the read budget and got cut short. */
21
+ truncated: boolean;
22
+ }
23
+ /** Read a SKILL.md and extract its name + critical rules section */
24
+ export declare function parseSkillFile(skillPath: string): Promise<ParsedSkillFile | null>;
10
25
  /** Extract critical rules from markdown content */
11
26
  export declare function extractCriticalRules(content: string): string[];
12
27
  /** Extract trigger keywords from a skill description */
@@ -1,7 +1,8 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import { parseFrontmatter } from "../../lib/frontmatter.js";
4
- import { CHARS_PER_TOKEN } from "./constants.js";
4
+ import { describeSafeReadFailure, safeReadFile, } from "../../lib/safe-read.js";
5
+ import { CHARS_PER_TOKEN, MAX_SKILL_BYTES } from "./constants.js";
5
6
  // ── Helpers ──────────────────────────────────────────────────────────────────
6
7
  /** Estimate token count from a string */
7
8
  export function estimateTokens(text) {
@@ -11,7 +12,24 @@ export function estimateTokens(text) {
11
12
  export async function parseSkillFile(skillPath) {
12
13
  if (!(await fs.pathExists(skillPath)))
13
14
  return null;
14
- const raw = await fs.readFile(skillPath, "utf-8");
15
+ const read = await safeReadFile(skillPath, {
16
+ hardRejectOverBytes: MAX_SKILL_BYTES,
17
+ });
18
+ if (!read.ok) {
19
+ // A path that vanished between the existence check and the read is the
20
+ // same "no such skill" case the caller already handles with null.
21
+ if (read.reason === "not-found" || read.reason === "not-a-file")
22
+ return null;
23
+ return {
24
+ name: path.basename(path.dirname(skillPath)),
25
+ rules: [],
26
+ rawContent: "",
27
+ triggers: [],
28
+ skip: { reason: read.reason, message: describeSafeReadFailure(read) },
29
+ truncated: false,
30
+ };
31
+ }
32
+ const raw = read.content;
15
33
  const fm = parseFrontmatter(raw);
16
34
  const rawName = fm?.data?.name;
17
35
  const name = typeof rawName === "string"
@@ -23,7 +41,15 @@ export async function parseSkillFile(skillPath) {
23
41
  const rawDesc = fm?.data?.description;
24
42
  const description = typeof rawDesc === "string" ? rawDesc : "";
25
43
  const triggers = extractTriggers(description);
26
- return { name, rules, rawContent: raw, triggers };
44
+ // rawContent is the kept text, so token estimates reflect what was analysed.
45
+ return {
46
+ name,
47
+ rules,
48
+ rawContent: raw,
49
+ triggers,
50
+ skip: null,
51
+ truncated: read.truncated,
52
+ };
27
53
  }
28
54
  /** Extract critical rules from markdown content */
29
55
  export function extractCriticalRules(content) {
@@ -1,10 +1,11 @@
1
1
  import type { SkillGrade, SkillRegistryGateResult, SkillScore } from "../../types/index.js";
2
- type ParsedSkill = {
3
- name: string;
4
- rules: string[];
5
- rawContent: string;
6
- triggers: string[];
7
- };
2
+ import { type ParsedSkillFile } from "./parsing.js";
3
+ /**
4
+ * The fields the dimension scorers read. Derived from `ParsedSkillFile` with
5
+ * `Pick` (not a hand-copied shape) so the `skip` field can never be silently
6
+ * dropped: a skipped parse is caught in `scoreSkill` before it reaches a scorer.
7
+ */
8
+ type ParsedSkill = Pick<ParsedSkillFile, "name" | "rules" | "rawContent" | "triggers">;
8
9
  /**
9
10
  * Score completeness (0-100): frontmatter fields, critical rules, structure.
10
11
  */
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_REGISTRY_THRESHOLD, DEFAULT_THRESHOLD } from "./constants.js";
2
- import { estimateTokens, parseSkillFile } from "./parsing.js";
2
+ import { estimateTokens, parseSkillFile, } from "./parsing.js";
3
3
  import { ACTION_VERBS, VAGUE_TERMS } from "./rules.js";
4
4
  // ── Dangerous content patterns (private to scoreSafety) ─────────────────────
5
5
  /** Dangerous patterns in skill content that indicate safety risks */
@@ -243,6 +243,26 @@ export async function scoreSkill(skillPath, threshold = DEFAULT_THRESHOLD) {
243
243
  const parsed = await parseSkillFile(skillPath);
244
244
  if (!parsed)
245
245
  return null;
246
+ // A file that could not be read is NOT a zero-content skill. Scoring it would
247
+ // invent a grade over an empty string — worse, `scoreSafety` starts at 100
248
+ // and finds no dangerous patterns in nothing, so an unreadable file would
249
+ // report perfect safety. Represent it as unread and fail closed instead.
250
+ if (parsed.skip) {
251
+ return {
252
+ skillName: parsed.name,
253
+ completeness: 0,
254
+ clarity: 0,
255
+ testability: 0,
256
+ tokenEfficiency: 0,
257
+ safety: 0,
258
+ agentReadiness: 0,
259
+ overall: 0,
260
+ grade: "F",
261
+ threshold,
262
+ passing: false,
263
+ unread: parsed.skip.message,
264
+ };
265
+ }
246
266
  const completeness = scoreCompleteness(parsed);
247
267
  const clarity = scoreClarity(parsed);
248
268
  const testability = scoreTestability(parsed);
@@ -279,6 +299,16 @@ export async function registryGate(skillPath, threshold = DEFAULT_REGISTRY_THRES
279
299
  const score = await scoreSkill(skillPath, threshold);
280
300
  if (!score)
281
301
  return null;
302
+ // An unread skill can never be accepted into the registry: it was not scored,
303
+ // so it cannot clear any threshold. Reject it with an explicit reason.
304
+ if (score.unread) {
305
+ return {
306
+ skillName: score.skillName,
307
+ score,
308
+ accepted: false,
309
+ reason: `Rejected: could not read skill (${score.unread}) — not scored`,
310
+ };
311
+ }
282
312
  const accepted = score.passing;
283
313
  let reason;
284
314
  if (!accepted) {
@@ -30,6 +30,7 @@ export declare function exportPluginAsAgentSkills(name: string): Promise<{
30
30
  */
31
31
  export declare function importAgentSkillsPackage(sourceDir: string, options?: {
32
32
  dryRun?: boolean;
33
+ force?: boolean;
33
34
  }): Promise<{
34
35
  success: boolean;
35
36
  name?: string;