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.
- package/ci-local/hooks/commit-msg +7 -0
- package/ci-local/hooks/pre-commit +8 -0
- package/ci-local/hooks/pre-push +8 -0
- package/dist/cli/dispatch/ci.js +1 -1
- package/dist/cli/dispatch/simple-renderers.js +1 -1
- package/dist/cli/dispatch/skills-cmd.js +9 -1
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +12 -0
- package/dist/commands/ci.js +5 -1
- package/dist/commands/doctor.js +9 -0
- package/dist/commands/init/steps/ghagga.d.ts +3 -4
- package/dist/commands/init/steps/ghagga.js +5 -15
- package/dist/commands/plugin.d.ts +4 -2
- package/dist/commands/plugin.js +4 -4
- package/dist/commands/skills/analysis.js +31 -2
- package/dist/commands/skills/benchmark.js +10 -0
- package/dist/commands/skills/constants.d.ts +5 -0
- package/dist/commands/skills/constants.js +5 -0
- package/dist/commands/skills/parsing.d.ts +18 -3
- package/dist/commands/skills/parsing.js +29 -3
- package/dist/commands/skills/scoring.d.ts +7 -6
- package/dist/commands/skills/scoring.js +31 -1
- package/dist/lib/agent-skills.d.ts +1 -0
- package/dist/lib/agent-skills.js +155 -1
- package/dist/lib/auto-skill-install.d.ts +5 -0
- package/dist/lib/auto-skill-install.js +40 -2
- package/dist/lib/context.d.ts +22 -0
- package/dist/lib/context.js +120 -79
- package/dist/lib/plugin.d.ts +1 -0
- package/dist/lib/plugin.js +58 -1
- package/dist/lib/safe-read.d.ts +62 -0
- package/dist/lib/safe-read.js +221 -0
- package/dist/lib/security-analysis.d.ts +19 -2
- package/dist/lib/security-analysis.js +65 -13
- package/dist/lib/skill-install-gate.d.ts +31 -0
- package/dist/lib/skill-install-gate.js +30 -0
- package/dist/lib/skill-scanner.d.ts +65 -1
- package/dist/lib/skill-scanner.js +307 -4
- package/dist/types/index.d.ts +18 -0
- package/dist/ui/AutoSkills.d.ts +3 -1
- package/dist/ui/AutoSkills.js +17 -2
- package/dist/ui/Plugin.d.ts +3 -1
- package/dist/ui/Plugin.js +4 -4
- package/dist/ui/Skills.js +12 -7
- package/package.json +9 -5
- 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
|
package/ci-local/hooks/pre-push
CHANGED
|
@@ -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
|
package/dist/cli/dispatch/ci.js
CHANGED
|
@@ -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
|
|
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";
|
package/dist/cli/help.d.ts
CHANGED
|
@@ -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
|
package/dist/commands/ci.js
CHANGED
|
@@ -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
|
-
|
|
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");
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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.
|
package/dist/commands/plugin.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
/**
|
|
4
|
-
export
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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) {
|