javi-forge 1.26.0 → 1.27.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/skills-cmd.js +8 -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/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/context.d.ts +22 -0
- package/dist/lib/context.js +120 -79
- 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-scanner.d.ts +22 -1
- package/dist/lib/skill-scanner.js +76 -4
- package/dist/types/index.d.ts +18 -0
- package/dist/ui/Skills.js +12 -7
- package/package.json +1 -1
- 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.
|
|
@@ -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/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
|
}
|
|
@@ -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) {
|
package/dist/lib/context.d.ts
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import type { InitOptions, StackContextEntry } from "../types/index.js";
|
|
2
2
|
export declare function buildIndexMd(projectName: string, stackCtx: StackContextEntry, ciProvider: string, memory: string): string;
|
|
3
3
|
export declare function buildSummaryMd(projectName: string, stack: string, ciProvider: string, memory: string, modules: string[], dependencies?: string[]): string;
|
|
4
|
+
/**
|
|
5
|
+
* Dependency detection outcome. `warnings` explains every manifest that could
|
|
6
|
+
* not be read or parsed, so a failure is distinguishable from "no dependencies"
|
|
7
|
+
* instead of being swallowed by a blanket catch.
|
|
8
|
+
*/
|
|
9
|
+
export interface DependencyDetection {
|
|
10
|
+
dependencies: string[];
|
|
11
|
+
warnings: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Detect top-level dependencies from project manifest files, reporting why a
|
|
15
|
+
* manifest was skipped. Returns up to 10 dependency names (key deps only).
|
|
16
|
+
*/
|
|
17
|
+
export declare function detectDependenciesDetailed(projectDir: string, stack: string): Promise<DependencyDetection>;
|
|
4
18
|
/**
|
|
5
19
|
* Detect top-level dependencies from project manifest files.
|
|
6
20
|
* Returns up to 10 dependency names (key deps only, not devDeps).
|
|
21
|
+
*
|
|
22
|
+
* List-returning convenience over `detectDependenciesDetailed`. It DISCARDS the
|
|
23
|
+
* read/parse warnings — an unreadable manifest is indistinguishable from an
|
|
24
|
+
* honest empty list through this function. Callers that must tell those two
|
|
25
|
+
* apart (like `refreshContextDir`) call `detectDependenciesDetailed` directly.
|
|
7
26
|
*/
|
|
8
27
|
export declare function detectDependencies(projectDir: string, stack: string): Promise<string[]>;
|
|
9
28
|
/**
|
|
@@ -23,5 +42,8 @@ export declare function refreshContextDir(projectDir: string): Promise<{
|
|
|
23
42
|
index: string;
|
|
24
43
|
summary: string;
|
|
25
44
|
updated: boolean;
|
|
45
|
+
/** Manifest read/parse warnings — surfaced so an unreadable manifest is not
|
|
46
|
+
* silently reported as a project with no dependencies. */
|
|
47
|
+
warnings: string[];
|
|
26
48
|
} | null>;
|
|
27
49
|
//# sourceMappingURL=context.d.ts.map
|