renma 0.17.0 → 0.18.1
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/CHANGELOG.md +152 -1
- package/README.md +11 -1
- package/architecture.md +21 -10
- package/design.md +52 -34
- package/dist/agent-skills.d.ts +1 -1
- package/dist/agent-skills.d.ts.map +1 -1
- package/dist/agent-skills.js +28 -8
- package/dist/agent-skills.js.map +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +120 -12
- package/dist/catalog.js.map +1 -1
- package/dist/cli-help.d.ts +12 -8
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +23 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +23 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/bom.d.ts +6 -3
- package/dist/commands/bom.d.ts.map +1 -1
- package/dist/commands/bom.js +27 -7
- package/dist/commands/bom.js.map +1 -1
- package/dist/commands/catalog.d.ts +1 -1
- package/dist/commands/catalog.d.ts.map +1 -1
- package/dist/commands/catalog.js +7 -1
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/ci-report.d.ts.map +1 -1
- package/dist/commands/ci-report.js +18 -5
- package/dist/commands/ci-report.js.map +1 -1
- package/dist/commands/diff.js +8 -2
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/graph.d.ts +6 -2
- package/dist/commands/graph.d.ts.map +1 -1
- package/dist/commands/graph.js +23 -2
- package/dist/commands/graph.js.map +1 -1
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +2 -0
- package/dist/commands/inspect.js.map +1 -1
- package/dist/commands/ownership.d.ts +4 -3
- package/dist/commands/ownership.d.ts.map +1 -1
- package/dist/commands/ownership.js +27 -23
- package/dist/commands/ownership.js.map +1 -1
- package/dist/commands/readiness.d.ts +2 -1
- package/dist/commands/readiness.d.ts.map +1 -1
- package/dist/commands/readiness.js +98 -30
- package/dist/commands/readiness.js.map +1 -1
- package/dist/commands/scaffold.d.ts +3 -0
- package/dist/commands/scaffold.d.ts.map +1 -1
- package/dist/commands/scaffold.js +44 -9
- package/dist/commands/scaffold.js.map +1 -1
- package/dist/commands/suggest-metadata.d.ts.map +1 -1
- package/dist/commands/suggest-metadata.js +2 -0
- package/dist/commands/suggest-metadata.js.map +1 -1
- package/dist/commands/trust-graph.d.ts.map +1 -1
- package/dist/commands/trust-graph.js +12 -0
- package/dist/commands/trust-graph.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -3
- package/dist/config.js.map +1 -1
- package/dist/context-lens.d.ts +1 -0
- package/dist/context-lens.d.ts.map +1 -1
- package/dist/context-lens.js +19 -1
- package/dist/context-lens.js.map +1 -1
- package/dist/diagnostic-ids.d.ts +9 -0
- package/dist/diagnostic-ids.d.ts.map +1 -1
- package/dist/diagnostic-ids.js +9 -0
- package/dist/diagnostic-ids.js.map +1 -1
- package/dist/discovery.d.ts +5 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +108 -65
- package/dist/discovery.js.map +1 -1
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +15 -0
- package/dist/markdown.js.map +1 -1
- package/dist/metadata.d.ts +15 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +235 -0
- package/dist/metadata.js.map +1 -1
- package/dist/model.d.ts +20 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +4 -1
- package/dist/model.js.map +1 -1
- package/dist/quality-profile.d.ts +91 -0
- package/dist/quality-profile.d.ts.map +1 -0
- package/dist/quality-profile.js +91 -0
- package/dist/quality-profile.js.map +1 -0
- package/dist/repeated-context.d.ts.map +1 -1
- package/dist/repeated-context.js +38 -83
- package/dist/repeated-context.js.map +1 -1
- package/dist/repository-boundary.d.ts +30 -0
- package/dist/repository-boundary.d.ts.map +1 -0
- package/dist/repository-boundary.js +116 -0
- package/dist/repository-boundary.js.map +1 -0
- package/dist/repository-evidence.d.ts +2 -0
- package/dist/repository-evidence.d.ts.map +1 -1
- package/dist/repository-evidence.js +6 -4
- package/dist/repository-evidence.js.map +1 -1
- package/dist/repository-paths.d.ts +6 -2
- package/dist/repository-paths.d.ts.map +1 -1
- package/dist/repository-paths.js +75 -24
- package/dist/repository-paths.js.map +1 -1
- package/dist/rules.d.ts +2 -0
- package/dist/rules.d.ts.map +1 -1
- package/dist/rules.js +322 -189
- package/dist/rules.js.map +1 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +5 -1
- package/dist/scanner.js.map +1 -1
- package/dist/security-diagnostics.d.ts.map +1 -1
- package/dist/security-diagnostics.js +95 -30
- package/dist/security-diagnostics.js.map +1 -1
- package/dist/security-diff.d.ts +5 -2
- package/dist/security-diff.d.ts.map +1 -1
- package/dist/security-diff.js +20 -4
- package/dist/security-diff.js.map +1 -1
- package/dist/security-policy-inventory.d.ts +15 -3
- package/dist/security-policy-inventory.d.ts.map +1 -1
- package/dist/security-policy-inventory.js +123 -26
- package/dist/security-policy-inventory.js.map +1 -1
- package/dist/security-policy.d.ts +7 -0
- package/dist/security-policy.d.ts.map +1 -1
- package/dist/security-policy.js +70 -7
- package/dist/security-policy.js.map +1 -1
- package/dist/security-posture.d.ts.map +1 -1
- package/dist/security-posture.js +2 -1
- package/dist/security-posture.js.map +1 -1
- package/dist/skill-migration.js +2 -0
- package/dist/skill-migration.js.map +1 -1
- package/dist/static-support.d.ts +13 -0
- package/dist/static-support.d.ts.map +1 -0
- package/dist/static-support.js +284 -0
- package/dist/static-support.js.map +1 -0
- package/dist/token-estimator.d.ts +21 -0
- package/dist/token-estimator.d.ts.map +1 -0
- package/dist/token-estimator.js +84 -0
- package/dist/token-estimator.js.map +1 -0
- package/dist/trust-graph.d.ts +3 -3
- package/dist/trust-graph.d.ts.map +1 -1
- package/dist/trust-graph.js +69 -10
- package/dist/trust-graph.js.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +15 -8
- package/docs/advanced-skill-authoring.md +16 -9
- package/docs/agent-skills-compatibility.md +10 -2
- package/docs/authoring-guide.md +89 -6
- package/docs/context-lens.md +319 -153
- package/docs/diagnostics.md +46 -11
- package/docs/metadata-budget.md +32 -0
- package/docs/quality-profile.md +135 -0
- package/docs/repository-context-bom.md +67 -8
- package/docs/schemas/repository-context-bom-v2.schema.json +648 -0
- package/docs/schemas/trust-graph-v2.schema.json +301 -0
- package/docs/security-policy.md +21 -5
- package/docs/trust-graph.md +47 -0
- package/docs/user-manual.md +48 -7
- package/examples/context-lens/README.md +8 -3
- package/examples/context-lens/contexts/testing/boundary-value-analysis.md +6 -2
- package/examples/context-lens/lenses/testing/spec-review-boundary-values.md +27 -5
- package/examples/context-lens/lenses/testing/test-design-boundary-values.md +27 -5
- package/examples/context-lens/skills/testing/spec-review/SKILL.md +33 -4
- package/examples/context-repo/README.md +6 -7
- package/examples/context-repo/contexts/domain/payment/idempotency.md +7 -0
- package/examples/context-repo/contexts/testing/boundary-value-analysis.md +7 -0
- package/examples/context-repo/contexts/testing/negative-testing.md +7 -0
- package/examples/context-repo/skills/testing/spec-review/SKILL.md +5 -0
- package/package.json +3 -1
- package/plan-discovery.md +24 -15
- package/plan.md +59 -119
- package/scripts/verify-package.mjs +8 -1
package/dist/rules.js
CHANGED
|
@@ -2,96 +2,52 @@ import path from "node:path";
|
|
|
2
2
|
import { resolvedAgentSkillDescription } from "./agent-skills.js";
|
|
3
3
|
import { addDaysIsoDate, isIsoDate, parseDayDuration, todayIsoDate, } from "./freshness.js";
|
|
4
4
|
import { DIAGNOSTIC_IDS } from "./diagnostic-ids.js";
|
|
5
|
-
import { classifyRepositorySkillEntrypointPath, classifyRepositorySkillPath, normalizeRepositoryRelativePath, } from "./discovery.js";
|
|
5
|
+
import { classifyRepositorySkillEntrypointPath, classifyRepositorySkillPath, logicalSkillDirectory, normalizeRepositoryRelativePath, } from "./discovery.js";
|
|
6
6
|
import { helperScriptPath, resolveHelperScriptPath, } from "./repository-paths.js";
|
|
7
7
|
import { parseAssetMetadata } from "./metadata.js";
|
|
8
8
|
import { runRuleRegistry } from "./rule-engine.js";
|
|
9
|
+
import { DEFAULT_QUALITY_PROFILE } from "./quality-profile.js";
|
|
10
|
+
import { estimateTokens, markdownBody } from "./token-estimator.js";
|
|
11
|
+
import { staticSupportReferences } from "./static-support.js";
|
|
9
12
|
const SECRET_PATTERN = /\b(?:password|passwd|token|api[_-]?key|secret|credential|private[_-]?key)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{8,})/i;
|
|
10
13
|
const PRIVATE_KEY_PATTERN = /-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/;
|
|
11
14
|
const REMOTE_PATTERN = /\b(?:curl|wget)\b.*(?:\|\s*(?:sh|bash)|\b(?:example\.com|prod|production|--insecure|-k)\b)|\b(?:ssh|scp)\b.*\b(?:example\.com|prod|production|root@|--insecure|-k|StrictHostKeyChecking=no|UserKnownHostsFile=\/dev\/null)\b/i;
|
|
12
15
|
const ENV_COPY_PATTERN = /\b(?:process\.env|env)\b.*\b(?:spawn|exec|execFile|system|subprocess|child_process)\b|\b(?:spawn|exec|execFile|system|subprocess|child_process)\b.*\b(?:process\.env|env)\b/i;
|
|
13
16
|
const USER_LOCAL_PATH_PATTERN = /(?:^|[^a-z0-9_])(?:\/Users\/[^\s/\\]+|\/home\/[^\s/\\]+|[A-Za-z]:\\Users\\[^\s\\]+)(?:\/|$)/iu;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const REUSABLE_CONTEXT_MIN_LINES = 24;
|
|
17
|
-
const REUSABLE_CONTEXT_MIN_TOKENS = 180;
|
|
17
|
+
const QUALITY = DEFAULT_QUALITY_PROFILE;
|
|
18
|
+
const MAX_LOCAL_SUPPORT_REFERENCE_HOPS = QUALITY.agentSkills.recommendedReferenceDepth + 1;
|
|
18
19
|
const REQUIRED_INPUTS_PATTERN = /\b(?:required inputs?|inputs|input requirements?|required information|prerequisites?|required context|required files|required permissions?|permission requirements?|environment requirements?|before running,\s*provide|before you begin,\s*provide|the user must provide|needs the following|target files|permissions required|environment required)\b|\brequires:/;
|
|
19
20
|
const COMPLETION_CRITERIA_PATTERN = /\b(?:completion criteria|completion checklist|success criteria|success requirements|done criteria|done when|definition of done|acceptance criteria|deliverables?|final response|final answer|expected outcomes?|expected results?|expected output|required output|output requirements?|report should include|patch should include|when complete|workflow is complete|the workflow is complete after|task is complete|counts as complete|completion requirements?|stop when|do not finish until)\b/;
|
|
20
|
-
const REUSABLE_CONTEXT_MIN_SIGNALS = 3;
|
|
21
|
-
const SUPPORT_SHARED_CONTEXT_MIN_LINES = 18;
|
|
22
|
-
const SUPPORT_SHARED_CONTEXT_MIN_TOKENS = 140;
|
|
23
|
-
const SUPPORT_SHARED_CONTEXT_MIN_HEADINGS = 2;
|
|
24
|
-
const SUPPORT_SHARED_CONTEXT_MIN_PHRASES = 2;
|
|
25
21
|
const REUSABLE_CONTEXT_HEADING_PATTERNS = [
|
|
26
|
-
[/\
|
|
27
|
-
[/\
|
|
28
|
-
[/\bconfiguration\b/i, "Configuration"],
|
|
29
|
-
[/\benvironment\b/i, "Environment"],
|
|
30
|
-
[/\bprerequisites?\b/i, "Prerequisites"],
|
|
31
|
-
[/\bplatform\b/i, "Platform"],
|
|
32
|
-
[/\btroubleshooting\b/i, "Troubleshooting"],
|
|
22
|
+
[/\bplatform facts?\b/i, "Platform Facts"],
|
|
23
|
+
[/\breusable troubleshooting\b/i, "Reusable Troubleshooting"],
|
|
33
24
|
[/\bknown issues?\b/i, "Known Issues"],
|
|
34
|
-
[/\blimitations?\b/i, "Limitations"],
|
|
35
|
-
[/\bbest practices?\b/i, "Best Practices"],
|
|
36
|
-
[/\btesting heuristics?\b/i, "Testing Heuristics"],
|
|
37
|
-
[/\btest strategy\b/i, "Test Strategy"],
|
|
38
|
-
[/\bverification\b/i, "Verification"],
|
|
39
|
-
[/\bexamples?\b/i, "Examples"],
|
|
40
|
-
[/\bedge cases?\b/i, "Edge Cases"],
|
|
41
|
-
[/\brisks?\b/i, "Risks"],
|
|
42
25
|
[/\bdomain rules?\b/i, "Domain Rules"],
|
|
43
|
-
[/\
|
|
44
|
-
[/\
|
|
26
|
+
[/\bcompatibility (?:matrix|matrices)\b/i, "Compatibility Matrix"],
|
|
27
|
+
[/\bproduct polic(?:y|ies)\b/i, "Product Policy"],
|
|
28
|
+
[/\bshared testing heuristics?\b/i, "Shared Testing Heuristics"],
|
|
45
29
|
];
|
|
46
30
|
const REUSABLE_CONTEXT_PHRASE_PATTERNS = [
|
|
47
|
-
[/\buse this when\b/i, "use this when"],
|
|
48
31
|
[/\bknown issue\b/i, "known issue"],
|
|
49
|
-
[/\
|
|
50
|
-
[/\btroubleshooting\b/i, "troubleshooting"],
|
|
51
|
-
[/\bflaky\b/i, "flaky"],
|
|
52
|
-
[/\bretry\b/i, "retry"],
|
|
32
|
+
[/\breusable troubleshooting\b/i, "reusable troubleshooting"],
|
|
53
33
|
[/\bplatform-specific\b/i, "platform-specific"],
|
|
54
|
-
[/\
|
|
55
|
-
[/\
|
|
56
|
-
[/\
|
|
57
|
-
[/\bbest practice\b/i, "best practice"],
|
|
58
|
-
[/\bdo not\b/i, "do not"],
|
|
59
|
-
[/\bavoid\b/i, "avoid"],
|
|
60
|
-
[/\balways\b/i, "always"],
|
|
61
|
-
[/\bnever\b/i, "never"],
|
|
34
|
+
[/\bsource of truth\b/i, "source of truth"],
|
|
35
|
+
[/\bused by (?:multiple|several) skills\b/i, "cross-Skill use"],
|
|
36
|
+
[/\bshared (?:rule|policy|heuristic|knowledge)\b/i, "shared knowledge"],
|
|
62
37
|
];
|
|
63
38
|
const SUPPORT_SHARED_CONTEXT_HEADING_PATTERNS = [
|
|
64
39
|
...REUSABLE_CONTEXT_HEADING_PATTERNS,
|
|
65
|
-
[/\bdecision logic\b/i, "Decision Logic"],
|
|
66
|
-
[/\bsafety notes?\b/i, "Safety Notes"],
|
|
67
|
-
[/\bvalidation\b/i, "Validation"],
|
|
68
|
-
[/\boperating model\b/i, "Operating Model"],
|
|
69
40
|
[/\bpolicy\b/i, "Policy"],
|
|
70
|
-
[/\bguidelines?\b/i, "Guidelines"],
|
|
71
|
-
[/\bprocedures?\b/i, "Procedure"],
|
|
72
|
-
[/\bchecklist\b/i, "Checklist"],
|
|
73
41
|
[/\bcompatibility\b/i, "Compatibility"],
|
|
74
|
-
[/\
|
|
42
|
+
[/\bsource of truth\b/i, "Source of Truth"],
|
|
75
43
|
];
|
|
76
44
|
const SUPPORT_SHARED_CONTEXT_PHRASE_PATTERNS = [
|
|
77
|
-
[/\bmust\b/i, "must"],
|
|
78
|
-
[/\bshould\b/i, "should"],
|
|
79
|
-
[/\balways\b/i, "always"],
|
|
80
|
-
[/\bnever\b/i, "never"],
|
|
81
|
-
[/\bavoid\b/i, "avoid"],
|
|
82
|
-
[/\bprefer\b/i, "prefer"],
|
|
83
|
-
[/\bdo not\b/i, "do not"],
|
|
84
|
-
[/\brequired\b/i, "required"],
|
|
85
|
-
[/\brecommended\b/i, "recommended"],
|
|
86
45
|
[/\bknown issue\b/i, "known issue"],
|
|
87
|
-
[/\
|
|
88
|
-
[/\
|
|
89
|
-
[/\
|
|
90
|
-
[/\
|
|
91
|
-
[/\
|
|
92
|
-
[/\bbest practice\b/i, "best practice"],
|
|
93
|
-
[/\bvalidate\b/i, "validate"],
|
|
94
|
-
[/\bverify\b/i, "verify"],
|
|
46
|
+
[/\breusable troubleshooting\b/i, "reusable troubleshooting"],
|
|
47
|
+
[/\bsource of truth\b/i, "source of truth"],
|
|
48
|
+
[/\bused by (?:multiple|several) skills\b/i, "cross-Skill use"],
|
|
49
|
+
[/\bindependent (?:owner|ownership|lifecycle)\b/i, "independent ownership"],
|
|
50
|
+
[/\bshared (?:rule|policy|heuristic|knowledge)\b/i, "shared knowledge"],
|
|
95
51
|
];
|
|
96
52
|
const NON_SEMANTIC_CONTEXT_PATH_SEGMENTS = new Set([
|
|
97
53
|
"promoted",
|
|
@@ -114,15 +70,10 @@ const NON_SEMANTIC_CONTEXT_PATH_SEGMENTS = new Set([
|
|
|
114
70
|
"candidate",
|
|
115
71
|
"candidates",
|
|
116
72
|
]);
|
|
117
|
-
const CONTEXT_TOKEN_LIMITS =
|
|
118
|
-
context: 1200,
|
|
119
|
-
profile: 500,
|
|
120
|
-
reference: 800,
|
|
121
|
-
example: 800,
|
|
122
|
-
};
|
|
73
|
+
const CONTEXT_TOKEN_LIMITS = QUALITY.contentTokenWarn;
|
|
123
74
|
/** Run all deterministic rules and return findings in stable source order. */
|
|
124
75
|
export function runRules(documents, config, catalog, options = {}) {
|
|
125
|
-
const findings = runRuleRegistry(documents, rulesForEvaluationDate(evaluationDay(options.evaluationDate), options.repositoryPaths), catalog, config);
|
|
76
|
+
const findings = runRuleRegistry(documents, rulesForEvaluationDate(evaluationDay(options.evaluationDate), options.repositoryPaths, options.repositoryPathStates), catalog, config);
|
|
126
77
|
return findings.sort((a, b) => {
|
|
127
78
|
const byPath = a.evidence.path.localeCompare(b.evidence.path);
|
|
128
79
|
if (byPath !== 0)
|
|
@@ -130,7 +81,7 @@ export function runRules(documents, config, catalog, options = {}) {
|
|
|
130
81
|
return a.evidence.startLine - b.evidence.startLine;
|
|
131
82
|
});
|
|
132
83
|
}
|
|
133
|
-
function rulesForEvaluationDate(evaluationDate, repositoryPaths) {
|
|
84
|
+
function rulesForEvaluationDate(evaluationDate, repositoryPaths, repositoryPathStates) {
|
|
134
85
|
return [
|
|
135
86
|
{
|
|
136
87
|
id: "strict-layout-policy",
|
|
@@ -153,7 +104,7 @@ function rulesForEvaluationDate(evaluationDate, repositoryPaths) {
|
|
|
153
104
|
},
|
|
154
105
|
{
|
|
155
106
|
id: "skill-local-support-reachability",
|
|
156
|
-
run: ({ documents }) => skillLocalSupportReachabilityFindings(documents),
|
|
107
|
+
run: ({ documents }) => skillLocalSupportReachabilityFindings(documents, repositoryPaths, repositoryPathStates),
|
|
157
108
|
},
|
|
158
109
|
{
|
|
159
110
|
id: "support-asset-shared-context-candidate",
|
|
@@ -646,33 +597,51 @@ function shapeFindings(document) {
|
|
|
646
597
|
const description = document.artifact.kind === "skill"
|
|
647
598
|
? (resolvedAgentSkillDescription(document) ?? "")
|
|
648
599
|
: (document.metadata.description ?? "");
|
|
649
|
-
|
|
600
|
+
// Skill budgets measure only Markdown after frontmatter. Agent Skills loads
|
|
601
|
+
// metadata separately, and Renma applies independent metadata budgets.
|
|
602
|
+
const bodyTokenCount = estimateTokens(markdownBody(document.artifact.content));
|
|
650
603
|
if (!description) {
|
|
651
604
|
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_DESCRIPTION, "Skill is missing an explicit description", "quality", "medium", "Add frontmatter description so agents can route to the skill intentionally."));
|
|
652
605
|
}
|
|
653
606
|
else if (document.artifact.kind === "skill" &&
|
|
654
|
-
|
|
655
|
-
|
|
607
|
+
QUALITY.descriptionMinChars > 0 &&
|
|
608
|
+
description.length < QUALITY.descriptionMinChars) {
|
|
609
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_SHORT_DESCRIPTION, "Skill description is too short for routing clarity", "quality", "low", `Expand frontmatter description to at least ${QUALITY.descriptionMinChars} characters with usage routing guidance.`));
|
|
656
610
|
}
|
|
657
|
-
const reusableContextFinding = reusableContextCandidateFinding(document,
|
|
611
|
+
const reusableContextFinding = reusableContextCandidateFinding(document, bodyTokenCount);
|
|
658
612
|
if (reusableContextFinding)
|
|
659
613
|
findings.push(reusableContextFinding);
|
|
660
|
-
if (document.artifact.kind === "skill" &&
|
|
661
|
-
|
|
662
|
-
|
|
614
|
+
if (document.artifact.kind === "skill" &&
|
|
615
|
+
bodyTokenCount > QUALITY.skillTokenWarn) {
|
|
616
|
+
const limit = bodyTokenCount > QUALITY.skillTokenStrongWarn
|
|
617
|
+
? QUALITY.skillTokenStrongWarn
|
|
618
|
+
: QUALITY.skillTokenWarn;
|
|
619
|
+
const severity = bodyTokenCount > QUALITY.skillTokenStrongWarn ? "medium" : "low";
|
|
620
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_SKILL_TOKEN_BUDGET, "Skill body exceeds advisory token budget", "quality", severity, "Review progressive disclosure without deleting workflow steps. Keep selection boundaries, read conditions, ordered workflow, constraints, and completion criteria in SKILL.md. Move Skill-specific conditional detail to references/, deterministic repeated implementation to scripts/, output material to assets/, and independently owned cross-Skill knowledge to contexts/.", {
|
|
621
|
+
whyItMatters: "Long Skill bodies can make activated workflows harder to navigate. Size is advisory evidence only; Agent Skills recommends staying under 5,000 tokens and Renma adds an earlier low review point at 2,000 estimated tokens.",
|
|
663
622
|
constraints: [
|
|
664
623
|
"Do not introduce runtime context resolution.",
|
|
665
624
|
"Do not create prompt packages.",
|
|
666
625
|
"Do not make Renma responsible for selecting context.",
|
|
667
|
-
"Preserve
|
|
668
|
-
"
|
|
626
|
+
"Preserve ordered workflow steps, constraints, and completion criteria.",
|
|
627
|
+
"Choose a destination by semantic ownership, not by size alone.",
|
|
669
628
|
],
|
|
670
629
|
verificationSteps: [
|
|
671
630
|
"Run renma scan.",
|
|
672
631
|
"Run any project-specific validation checks that apply to this repository.",
|
|
673
|
-
"Confirm
|
|
632
|
+
"Confirm any moved material remains statically reachable and semantically owned by its destination.",
|
|
674
633
|
],
|
|
675
|
-
llmHint: "
|
|
634
|
+
llmHint: "Review the Skill for progressive disclosure. Keep core workflow in SKILL.md; use references/ for local detail, scripts/ for deterministic implementation, assets/ for output resources, and contexts/ only for independently owned shared knowledge.",
|
|
635
|
+
details: {
|
|
636
|
+
measured: bodyTokenCount,
|
|
637
|
+
limit,
|
|
638
|
+
unit: "estimated_tokens",
|
|
639
|
+
profile: QUALITY.profile,
|
|
640
|
+
measurement: "markdown_body_after_frontmatter",
|
|
641
|
+
source: limit === QUALITY.skillTokenStrongWarn
|
|
642
|
+
? "agent_skills_recommendation_and_renma_severity"
|
|
643
|
+
: "renma_quality_policy",
|
|
644
|
+
},
|
|
676
645
|
}));
|
|
677
646
|
}
|
|
678
647
|
if (document.artifact.kind === "skill" &&
|
|
@@ -732,17 +701,25 @@ function shapeFindings(document) {
|
|
|
732
701
|
if (!/verify|validation|test|confirm result|expected output/.test(text)) {
|
|
733
702
|
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_MISSING_VERIFICATION, "Skill lacks verification guidance", "quality", "medium", "State how to verify success with a command, check, or observable result."));
|
|
734
703
|
}
|
|
735
|
-
if (document.headings.length <
|
|
736
|
-
|
|
737
|
-
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_LOW_HEADING_DENSITY, "Long instruction file has few headings", "structure", "low", "
|
|
704
|
+
if (document.headings.length < QUALITY.lowHeadingDensityMinHeadings &&
|
|
705
|
+
bodyTokenCount >= QUALITY.lowHeadingDensityMinTokens) {
|
|
706
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_LOW_HEADING_DENSITY, "Long instruction file has few headings", "structure", "low", "Add task-oriented headings so agents can navigate the body reliably.", {
|
|
707
|
+
details: {
|
|
708
|
+
measured: bodyTokenCount,
|
|
709
|
+
limit: QUALITY.lowHeadingDensityMinTokens,
|
|
710
|
+
unit: "estimated_tokens",
|
|
711
|
+
profile: QUALITY.profile,
|
|
712
|
+
measurement: "markdown_body_after_frontmatter",
|
|
713
|
+
},
|
|
714
|
+
}));
|
|
738
715
|
}
|
|
739
716
|
return findings;
|
|
740
717
|
}
|
|
741
|
-
function reusableContextCandidateFinding(document,
|
|
718
|
+
function reusableContextCandidateFinding(document, estimatedTokens) {
|
|
742
719
|
if (document.artifact.kind !== "skill")
|
|
743
720
|
return undefined;
|
|
744
|
-
if (document.lines.length <
|
|
745
|
-
|
|
721
|
+
if (document.lines.length < QUALITY.reusableContextCandidate.minLines &&
|
|
722
|
+
estimatedTokens < QUALITY.reusableContextCandidate.minTokens)
|
|
746
723
|
return undefined;
|
|
747
724
|
const headingMatches = document.headings.flatMap((heading) => REUSABLE_CONTEXT_HEADING_PATTERNS.filter(([pattern]) => pattern.test(heading.text)).map(([, label]) => ({
|
|
748
725
|
label,
|
|
@@ -766,7 +743,10 @@ function reusableContextCandidateFinding(document, tokenCount) {
|
|
|
766
743
|
];
|
|
767
744
|
const phraseLabels = [...new Set(phraseMatches.map((match) => match.label))];
|
|
768
745
|
const signalCount = new Set([...headingLabels, ...phraseLabels]).size;
|
|
769
|
-
|
|
746
|
+
// All patterns above are reusable-knowledge evidence. Ordinary workflow
|
|
747
|
+
// headings such as Verification, Examples, Steps, Risks, and Constraints are
|
|
748
|
+
// intentionally excluded and cannot qualify a Skill by themselves.
|
|
749
|
+
if (signalCount < QUALITY.reusableContextCandidate.minSignals)
|
|
770
750
|
return undefined;
|
|
771
751
|
const evidenceLine = headingMatches[0]?.line ??
|
|
772
752
|
phraseMatches[0]?.line ??
|
|
@@ -784,26 +764,37 @@ function reusableContextCandidateFinding(document, tokenCount) {
|
|
|
784
764
|
: undefined,
|
|
785
765
|
].filter((part) => Boolean(part));
|
|
786
766
|
return {
|
|
787
|
-
id: DIAGNOSTIC_IDS.
|
|
788
|
-
title: "Skill may
|
|
767
|
+
id: DIAGNOSTIC_IDS.QUAL_SKILL_MIXED_RESPONSIBILITY,
|
|
768
|
+
title: "Skill may mix its workflow with reusable knowledge",
|
|
789
769
|
category: "maintenance",
|
|
790
770
|
severity: "low",
|
|
791
771
|
confidence: "medium",
|
|
792
772
|
evidence: evidence(document, evidenceLine, evidenceParts.join("; ")),
|
|
793
773
|
whyItMatters: "Reusable setup notes, troubleshooting, platform guidance, testing heuristics, or domain rules are easier to own, review, and reuse when they live in shared context assets instead of only inside one skill.",
|
|
794
|
-
remediation: "Review the matched headings and phrases.
|
|
774
|
+
remediation: "Review the matched headings and phrases. Promote content to contexts/ only when it needs cross-Skill reuse, independent ownership, lifecycle, or source-of-truth status; otherwise keep Skill-specific detail in SKILL.md or references/.",
|
|
795
775
|
constraints: [
|
|
796
776
|
"Do not make Renma select runtime context.",
|
|
797
777
|
"Do not assemble prompt packages.",
|
|
798
778
|
"Do not automatically rewrite or split skills.",
|
|
799
|
-
"Preserve SKILL.md as
|
|
779
|
+
"Preserve SKILL.md as a focused workflow entrypoint.",
|
|
800
780
|
"Give extracted context assets stable metadata such as id, owner, and status.",
|
|
801
781
|
],
|
|
802
782
|
verificationSteps: [
|
|
803
783
|
"Run renma scan.",
|
|
804
784
|
"Confirm the advisory is resolved or intentionally accepted after reusable knowledge is represented as shared context assets.",
|
|
805
785
|
],
|
|
806
|
-
llmHint: "
|
|
786
|
+
llmHint: "Check whether the matched knowledge is used across Skills or needs independent ownership. Use contexts/ only for shared knowledge; keep Skill-local procedures and edge cases in SKILL.md or references/.",
|
|
787
|
+
details: {
|
|
788
|
+
measured: estimatedTokens,
|
|
789
|
+
limit: QUALITY.reusableContextCandidate.minTokens,
|
|
790
|
+
unit: "estimated_tokens",
|
|
791
|
+
lines: document.lines.length,
|
|
792
|
+
minLines: QUALITY.reusableContextCandidate.minLines,
|
|
793
|
+
distinctSignals: signalCount,
|
|
794
|
+
minSignals: QUALITY.reusableContextCandidate.minSignals,
|
|
795
|
+
profile: QUALITY.profile,
|
|
796
|
+
measurement: "markdown_body_after_frontmatter",
|
|
797
|
+
},
|
|
807
798
|
};
|
|
808
799
|
}
|
|
809
800
|
function supportSharedContextCandidateFindings(document) {
|
|
@@ -815,9 +806,9 @@ function supportSharedContextCandidateFindings(document) {
|
|
|
815
806
|
!document.artifact.path.endsWith(".md")) {
|
|
816
807
|
return [];
|
|
817
808
|
}
|
|
818
|
-
const
|
|
819
|
-
if (document.lines.length <
|
|
820
|
-
|
|
809
|
+
const estimatedTokens = estimateTokens(markdownBody(document.artifact.content));
|
|
810
|
+
if (document.lines.length < QUALITY.sharedSupportCandidate.minLines &&
|
|
811
|
+
estimatedTokens < QUALITY.sharedSupportCandidate.minTokens) {
|
|
821
812
|
return [];
|
|
822
813
|
}
|
|
823
814
|
const contentLineIndexes = markdownBodyLineIndexes(document);
|
|
@@ -852,8 +843,10 @@ function supportSharedContextCandidateFindings(document) {
|
|
|
852
843
|
];
|
|
853
844
|
});
|
|
854
845
|
const sourceSignals = [...headingMatches, ...phraseMatches];
|
|
855
|
-
if (headingMatches.
|
|
856
|
-
|
|
846
|
+
if (new Set(headingMatches.map((match) => match.label)).size <
|
|
847
|
+
QUALITY.sharedSupportCandidate.minHeadings ||
|
|
848
|
+
new Set(phraseMatches.map((match) => match.label)).size <
|
|
849
|
+
QUALITY.sharedSupportCandidate.minPhrases) {
|
|
857
850
|
return [];
|
|
858
851
|
}
|
|
859
852
|
const evidenceMatches = sourceSignals
|
|
@@ -897,6 +890,19 @@ function supportSharedContextCandidateFindings(document) {
|
|
|
897
890
|
"Confirm reusable source-of-truth knowledge lives in contexts/ and skill-local references only contain local support guidance.",
|
|
898
891
|
],
|
|
899
892
|
llmHint: "Search the repository for similar headings, filenames, repeated procedures, commands, constraints, or overlapping guidance. If this support file appears reusable, propose a first-class context asset under contexts/, move the reusable details without losing information, keep truly local notes in the skill directory, and update declared context references.",
|
|
893
|
+
details: {
|
|
894
|
+
measured: estimatedTokens,
|
|
895
|
+
limit: QUALITY.sharedSupportCandidate.minTokens,
|
|
896
|
+
unit: "estimated_tokens",
|
|
897
|
+
lines: document.lines.length,
|
|
898
|
+
minLines: QUALITY.sharedSupportCandidate.minLines,
|
|
899
|
+
headings: new Set(headingMatches.map((match) => match.label)).size,
|
|
900
|
+
minHeadings: QUALITY.sharedSupportCandidate.minHeadings,
|
|
901
|
+
phrases: new Set(phraseMatches.map((match) => match.label)).size,
|
|
902
|
+
minPhrases: QUALITY.sharedSupportCandidate.minPhrases,
|
|
903
|
+
profile: QUALITY.profile,
|
|
904
|
+
measurement: "full_file",
|
|
905
|
+
},
|
|
900
906
|
},
|
|
901
907
|
];
|
|
902
908
|
}
|
|
@@ -1085,7 +1091,9 @@ function parentSkillPath(referencePath) {
|
|
|
1085
1091
|
: undefined;
|
|
1086
1092
|
}
|
|
1087
1093
|
function skillReferenceLine(skill, referencePath) {
|
|
1088
|
-
const skillDir =
|
|
1094
|
+
const skillDir = logicalSkillDirectory(skill.artifact.path);
|
|
1095
|
+
if (!skillDir)
|
|
1096
|
+
return undefined;
|
|
1089
1097
|
const relativePath = path.posix.relative(skillDir, referencePath);
|
|
1090
1098
|
const referencedTokens = [referencePath, relativePath];
|
|
1091
1099
|
for (const index of markdownBodyLineIndexes(skill)) {
|
|
@@ -1207,27 +1215,81 @@ function contextBudgetFindings(document) {
|
|
|
1207
1215
|
document.artifact.kind !== "example") {
|
|
1208
1216
|
return [];
|
|
1209
1217
|
}
|
|
1210
|
-
const
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1218
|
+
const tokenBudget = parseAssetMetadata(document).tokenBudgetDecision;
|
|
1219
|
+
const defaultLimit = tokenBudget.defaultLimit ?? CONTEXT_TOKEN_LIMITS[document.artifact.kind];
|
|
1220
|
+
const estimatedTokens = tokenBudget.estimatedTokens ?? estimateTokens(document.artifact.content);
|
|
1221
|
+
const effectiveLimit = tokenBudget.effectiveLimit ?? defaultLimit;
|
|
1222
|
+
const overrideActive = tokenBudget.status === "active";
|
|
1223
|
+
const findings = [];
|
|
1224
|
+
if (tokenBudget.status === "invalid") {
|
|
1225
|
+
const invalidFinding = documentFinding(document, DIAGNOSTIC_IDS.QUAL_INVALID_TOKEN_BUDGET_OVERRIDE, "Support asset has invalid token-budget override metadata", "quality", "low", "Correct or remove the malformed token-budget decision metadata. An override is valid only after a human decides the asset should remain intentionally coherent or ordered; do not add or increase an override merely to make diagnostics pass.", {
|
|
1226
|
+
whyItMatters: "Token-budget overrides record a declared human decision. Malformed, ambiguous, incomplete, orphaned, or unnecessary metadata cannot safely replace the default advisory limit.",
|
|
1217
1227
|
constraints: [
|
|
1218
|
-
"Do not
|
|
1219
|
-
"Do not
|
|
1220
|
-
"
|
|
1221
|
-
"Keep static references from the parent file or SKILL.md to every split part.",
|
|
1222
|
-
],
|
|
1223
|
-
verificationSteps: [
|
|
1224
|
-
"Run renma scan.",
|
|
1225
|
-
"Run the repository-specific validation or test command, if one exists.",
|
|
1226
|
-
"Confirm the finding is resolved or reduced and every split part remains reachable.",
|
|
1228
|
+
"Do not automatically insert token-budget override metadata.",
|
|
1229
|
+
"Do not treat the override as a general ignore mechanism.",
|
|
1230
|
+
"Ask whether a meaningful split preserves coherence and execution order before proposing an override.",
|
|
1227
1231
|
],
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1232
|
+
verificationSteps: ["Run renma scan."],
|
|
1233
|
+
llmHint: "Report the invalid fields to the user. First ask whether the asset can be split along meaningful boundaries without harming coherence or execution order. Only if the user confirms it should remain long should you repair explicit override metadata with their rationale.",
|
|
1234
|
+
details: {
|
|
1235
|
+
estimatedTokens,
|
|
1236
|
+
defaultLimit,
|
|
1237
|
+
...(tokenBudget.overrideLimit !== undefined
|
|
1238
|
+
? { overrideLimit: tokenBudget.overrideLimit }
|
|
1239
|
+
: {}),
|
|
1240
|
+
effectiveLimit,
|
|
1241
|
+
overrideActive,
|
|
1242
|
+
...(tokenBudget.tokenBudgetRationale
|
|
1243
|
+
? { tokenBudgetRationale: tokenBudget.tokenBudgetRationale }
|
|
1244
|
+
: {}),
|
|
1245
|
+
invalidReasons: tokenBudget.invalidReasons,
|
|
1246
|
+
profile: QUALITY.profile,
|
|
1247
|
+
measurement: "full_file",
|
|
1248
|
+
},
|
|
1249
|
+
});
|
|
1250
|
+
findings.push({
|
|
1251
|
+
...invalidFinding,
|
|
1252
|
+
...(tokenBudget.evidence ? { evidence: tokenBudget.evidence } : {}),
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
if (estimatedTokens <= effectiveLimit)
|
|
1256
|
+
return findings;
|
|
1257
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.QUAL_SUPPORT_ASSET_TOKEN_BUDGET, "Support asset exceeds token guidance", "quality", "low", `Ask the user whether this ${document.artifact.kind} asset can be split along meaningful boundaries without harming coherence or execution order. Split it only after the user agrees. If the user confirms it should remain intentionally long, they may record an explicit token-budget override and rationale; do not recommend an override merely to make this diagnostic pass.`, {
|
|
1258
|
+
whyItMatters: "Large content assets deserve a low-advisory coherence review. A meaningful split is preferred when it preserves semantic boundaries, while an explicit override can record the user's declared decision for an intentionally coherent or ordered long-form asset.",
|
|
1259
|
+
constraints: [
|
|
1260
|
+
"Do not introduce runtime context resolution.",
|
|
1261
|
+
"Do not create prompt packages.",
|
|
1262
|
+
"Do not split based on size alone.",
|
|
1263
|
+
"Require the user's decision before splitting or treating the asset as intentionally long.",
|
|
1264
|
+
"Do not automatically add or increase token-budget override metadata.",
|
|
1265
|
+
"Preserve concrete procedural steps losslessly if a semantic split is chosen.",
|
|
1266
|
+
"Keep static references from the parent file or SKILL.md to every split part.",
|
|
1267
|
+
],
|
|
1268
|
+
verificationSteps: [
|
|
1269
|
+
"Run renma scan.",
|
|
1270
|
+
"Run the repository-specific validation or test command, if one exists.",
|
|
1271
|
+
"Confirm every agreed split part remains reachable, or that an intentional-long decision uses valid declared metadata.",
|
|
1272
|
+
],
|
|
1273
|
+
llmHint: "Ask the user whether the asset can be split along meaningful boundaries without harming coherence or execution order. Split only with agreement. If the user confirms it must remain intentionally long, use their explicit rationale for a valid override; never add one only to silence the finding.",
|
|
1274
|
+
details: {
|
|
1275
|
+
estimatedTokens,
|
|
1276
|
+
defaultLimit,
|
|
1277
|
+
...(tokenBudget.overrideLimit !== undefined
|
|
1278
|
+
? { overrideLimit: tokenBudget.overrideLimit }
|
|
1279
|
+
: {}),
|
|
1280
|
+
effectiveLimit,
|
|
1281
|
+
overrideActive,
|
|
1282
|
+
...(tokenBudget.tokenBudgetRationale
|
|
1283
|
+
? { tokenBudgetRationale: tokenBudget.tokenBudgetRationale }
|
|
1284
|
+
: {}),
|
|
1285
|
+
measured: estimatedTokens,
|
|
1286
|
+
limit: effectiveLimit,
|
|
1287
|
+
unit: "estimated_tokens",
|
|
1288
|
+
profile: QUALITY.profile,
|
|
1289
|
+
measurement: "full_file",
|
|
1290
|
+
},
|
|
1291
|
+
}));
|
|
1292
|
+
return findings;
|
|
1231
1293
|
}
|
|
1232
1294
|
function profileFindings(document) {
|
|
1233
1295
|
if (document.artifact.kind !== "profile")
|
|
@@ -1239,19 +1301,31 @@ function profileFindings(document) {
|
|
|
1239
1301
|
documentFinding(document, DIAGNOSTIC_IDS.PROF_MISSING_BASE, "Profile overlay does not declare its base skill", "structure", "medium", "Declare the base skill or compatibility target so routing conflicts are auditable."),
|
|
1240
1302
|
];
|
|
1241
1303
|
}
|
|
1242
|
-
function skillLocalSupportReachabilityFindings(documents) {
|
|
1304
|
+
function skillLocalSupportReachabilityFindings(documents, repositoryPaths, repositoryPathStates) {
|
|
1243
1305
|
const skills = documents.filter((document) => document.artifact.kind === "skill");
|
|
1244
1306
|
return skills.flatMap((skill) => {
|
|
1245
|
-
const skillDir =
|
|
1246
|
-
|
|
1247
|
-
document.artifact.path.startsWith(`${skillDir}/`));
|
|
1248
|
-
if (localSupportDocs.length === 0)
|
|
1307
|
+
const skillDir = logicalSkillDirectory(skill.artifact.path);
|
|
1308
|
+
if (!skillDir)
|
|
1249
1309
|
return [];
|
|
1310
|
+
const localSupportDocs = documents.filter((document) => {
|
|
1311
|
+
if (!["profile", "reference", "example", "script", "asset"].includes(document.artifact.kind)) {
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
const classified = classifyRepositorySkillPath(document.artifact.path);
|
|
1315
|
+
return (classified?.kind === "support" && classified.skillDirectory === skillDir);
|
|
1316
|
+
});
|
|
1317
|
+
const localSupportPaths = repositoryPaths
|
|
1318
|
+
? [...repositoryPaths].filter((candidate) => {
|
|
1319
|
+
const classified = classifyRepositorySkillPath(candidate);
|
|
1320
|
+
return (classified?.kind === "support" &&
|
|
1321
|
+
classified.skillDirectory === skillDir);
|
|
1322
|
+
})
|
|
1323
|
+
: localSupportDocs.map((document) => document.artifact.path);
|
|
1250
1324
|
const findings = [];
|
|
1251
1325
|
const text = skill.artifact.content.toLowerCase();
|
|
1252
|
-
const hasLocalSupportGuidance = /support file|local support|context route|context map|mixin|profiles?\/|references?\/|examples?\/|load .*?(?:profile|reference|example)|reference .*?(?:profile|reference|example)/.test(text);
|
|
1253
|
-
if (!hasLocalSupportGuidance) {
|
|
1254
|
-
findings.push(documentFinding(skill, DIAGNOSTIC_IDS.SUPPORT_MISSING_REACHABILITY_GUIDANCE, "Skill has local support files but no reachability guidance", "structure", "medium", "Add local support file reachability guidance so the top-level skill declares when profiles, references, examples, or
|
|
1326
|
+
const hasLocalSupportGuidance = /support file|local support|context route|context map|mixin|profiles?\/|references?\/|examples?\/|scripts?\/|assets?\/|load .*?(?:profile|reference|example|script|asset)|(?:read|run|reference) .*?(?:profile|reference|example|script|asset)/.test(text);
|
|
1327
|
+
if (localSupportDocs.length > 0 && !hasLocalSupportGuidance) {
|
|
1328
|
+
findings.push(documentFinding(skill, DIAGNOSTIC_IDS.SUPPORT_MISSING_REACHABILITY_GUIDANCE, "Skill has local support files but no reachability guidance", "structure", "medium", "Add local support file reachability guidance so the top-level skill declares when profiles, references, examples, scripts, or assets are reachable. If support content was split into ordered parts, reference the index or all parts in order. Preserve original concrete steps. Do not delete, summarize, or merge away procedural steps.", {
|
|
1255
1329
|
whyItMatters: "Local support files should be statically discoverable from the skill so humans and LLM coding agents can tell which repository evidence belongs to the skill without relying on runtime context selection.",
|
|
1256
1330
|
constraints: [
|
|
1257
1331
|
"Do not introduce runtime context resolution.",
|
|
@@ -1262,14 +1336,15 @@ function skillLocalSupportReachabilityFindings(documents) {
|
|
|
1262
1336
|
verificationSteps: [
|
|
1263
1337
|
"Run renma scan.",
|
|
1264
1338
|
"Run any project-specific validation checks that apply to this repository.",
|
|
1265
|
-
"Confirm each local profile, reference, or
|
|
1339
|
+
"Confirm each local profile, reference, example, script, or asset is reachable from SKILL.md or from a referenced parent support file.",
|
|
1266
1340
|
],
|
|
1267
|
-
llmHint: "Add concise reachability guidance in SKILL.md that references local profiles, references, examples, or ordered support indexes without adding runtime routing behavior.",
|
|
1341
|
+
llmHint: "Add concise reachability guidance in SKILL.md that references local profiles, references, examples, scripts, assets, or ordered support indexes without adding runtime routing behavior.",
|
|
1268
1342
|
}));
|
|
1269
1343
|
}
|
|
1270
|
-
const
|
|
1344
|
+
const reachabilityDepth = localSupportReachabilityDepth(skill, skillDir, localSupportDocs, localSupportPaths);
|
|
1271
1345
|
for (const document of localSupportDocs) {
|
|
1272
|
-
|
|
1346
|
+
const depth = reachabilityDepth.get(document.artifact.path);
|
|
1347
|
+
if (depth === undefined) {
|
|
1273
1348
|
findings.push(documentFinding(document, localSupportUnreachableRuleId(document.artifact.kind), "Local support file is not reachable from the skill", "structure", "low", "Reference this local support file from SKILL.md or from a referenced parent support file with clear reachability guidance. If this file is a split part, ensure the parent skill references the index or all ordered parts so preserved details remain reachable. Do not delete, summarize, or merge away procedural steps just to satisfy the check.", {
|
|
1274
1349
|
whyItMatters: "Unreachable local support files can drift outside review and be missed by humans or LLM coding agents. Reachability should be static repository evidence, not runtime context assembly.",
|
|
1275
1350
|
constraints: [
|
|
@@ -1286,36 +1361,126 @@ function skillLocalSupportReachabilityFindings(documents) {
|
|
|
1286
1361
|
llmHint: "Update SKILL.md or a referenced support index to mention this file by path, basename, or clear title so the static reachability graph can find it.",
|
|
1287
1362
|
}));
|
|
1288
1363
|
}
|
|
1364
|
+
else if (depth > MAX_LOCAL_SUPPORT_REFERENCE_HOPS) {
|
|
1365
|
+
findings.push(documentFinding(document, DIAGNOSTIC_IDS.SUPPORT_DEEP_REFERENCE_CHAIN, "Skill-local resource is behind a deep reference chain", "structure", "low", "Reference this resource directly from SKILL.md or through one directly referenced index/reference. Agent Skills recommends shallow file-reference chains.", {
|
|
1366
|
+
details: {
|
|
1367
|
+
measured: depth,
|
|
1368
|
+
limit: MAX_LOCAL_SUPPORT_REFERENCE_HOPS,
|
|
1369
|
+
unit: "static_reference_hops",
|
|
1370
|
+
profile: QUALITY.profile,
|
|
1371
|
+
},
|
|
1372
|
+
}));
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
const missingPathSources = [
|
|
1376
|
+
skill,
|
|
1377
|
+
...localSupportDocs.filter((document) => (reachabilityDepth.get(document.artifact.path) ?? 99) <=
|
|
1378
|
+
MAX_LOCAL_SUPPORT_REFERENCE_HOPS),
|
|
1379
|
+
];
|
|
1380
|
+
const reportedMissingPaths = new Set();
|
|
1381
|
+
const reportedSymlinkBoundaries = new Set();
|
|
1382
|
+
for (const source of missingPathSources) {
|
|
1383
|
+
for (const reference of localSupportPathReferences(source, skillDir, localSupportPaths)) {
|
|
1384
|
+
if (reference.relative.startsWith("examples/") &&
|
|
1385
|
+
!path.posix.basename(reference.relative).includes(".")) {
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
const symlinkBoundary = referencedSymlinkBoundary(reference.target, repositoryPathStates);
|
|
1389
|
+
if (symlinkBoundary) {
|
|
1390
|
+
const key = `${source.artifact.path}:${symlinkBoundary}`;
|
|
1391
|
+
if (reportedSymlinkBoundaries.has(key))
|
|
1392
|
+
continue;
|
|
1393
|
+
reportedSymlinkBoundaries.add(key);
|
|
1394
|
+
findings.push(findingAt(source, DIAGNOSTIC_IDS.SUPPORT_SYMLINK_PATH, "Skill-local resource path is an unusable symbolic link", "structure", "medium", reference.line, reference.raw, `Replace ${reference.relative} with a regular repository file or directory; Renma never follows symbolic links.`, {
|
|
1395
|
+
details: {
|
|
1396
|
+
sourcePath: source.artifact.path,
|
|
1397
|
+
target: reference.target,
|
|
1398
|
+
state: "symlink",
|
|
1399
|
+
symlinkBoundary,
|
|
1400
|
+
profile: QUALITY.profile,
|
|
1401
|
+
},
|
|
1402
|
+
}));
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
if (repositoryPathExists(repositoryPaths, reference.target, repositoryPathStates) ||
|
|
1406
|
+
repositoryPathExists(repositoryPaths, reference.relative, repositoryPathStates) ||
|
|
1407
|
+
reportedMissingPaths.has(`${source.artifact.path}:${reference.target}`))
|
|
1408
|
+
continue;
|
|
1409
|
+
reportedMissingPaths.add(`${source.artifact.path}:${reference.target}`);
|
|
1410
|
+
findings.push(findingAt(source, DIAGNOSTIC_IDS.SUPPORT_MISSING_PATH, "Skill-local resource path does not exist", "structure", "medium", reference.line, reference.raw, `Create ${reference.relative} under the Skill root or correct the relative path.`, {
|
|
1411
|
+
details: {
|
|
1412
|
+
target: reference.target,
|
|
1413
|
+
profile: QUALITY.profile,
|
|
1414
|
+
},
|
|
1415
|
+
}));
|
|
1416
|
+
}
|
|
1289
1417
|
}
|
|
1290
1418
|
return findings;
|
|
1291
1419
|
});
|
|
1292
1420
|
}
|
|
1293
|
-
function
|
|
1294
|
-
const
|
|
1421
|
+
function repositoryPathExists(repositoryPaths, candidate, repositoryPathStates) {
|
|
1422
|
+
const state = repositoryPathStates?.get(candidate);
|
|
1423
|
+
if (state !== undefined)
|
|
1424
|
+
return state !== "absent";
|
|
1425
|
+
if (!repositoryPaths)
|
|
1426
|
+
return false;
|
|
1427
|
+
if (repositoryPaths.has(candidate))
|
|
1428
|
+
return true;
|
|
1429
|
+
const directoryPrefix = `${candidate.replace(/\/$/, "")}/`;
|
|
1430
|
+
for (const filePath of repositoryPaths) {
|
|
1431
|
+
if (filePath.startsWith(directoryPrefix))
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
function referencedSymlinkBoundary(candidate, repositoryPathStates) {
|
|
1437
|
+
if (!repositoryPathStates)
|
|
1438
|
+
return undefined;
|
|
1439
|
+
const segments = candidate.split("/");
|
|
1440
|
+
for (let length = 1; length <= segments.length; length += 1) {
|
|
1441
|
+
const prefix = segments.slice(0, length).join("/");
|
|
1442
|
+
if (repositoryPathStates.get(prefix) === "symlink")
|
|
1443
|
+
return prefix;
|
|
1444
|
+
}
|
|
1445
|
+
return undefined;
|
|
1446
|
+
}
|
|
1447
|
+
function localSupportReachabilityDepth(skill, skillDirectory, localSupportDocs, candidatePaths) {
|
|
1448
|
+
const reachable = new Map();
|
|
1449
|
+
const references = new Map([skill, ...localSupportDocs].map((document) => [
|
|
1450
|
+
document.artifact.path,
|
|
1451
|
+
new Set(staticSupportReferences(document, skillDirectory, candidatePaths).map((reference) => reference.targetPath)),
|
|
1452
|
+
]));
|
|
1295
1453
|
let changed = true;
|
|
1296
1454
|
while (changed) {
|
|
1297
1455
|
changed = false;
|
|
1298
1456
|
for (const document of localSupportDocs) {
|
|
1299
1457
|
if (reachable.has(document.artifact.path))
|
|
1300
1458
|
continue;
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1459
|
+
if (references.get(skill.artifact.path)?.has(document.artifact.path)) {
|
|
1460
|
+
reachable.set(document.artifact.path, 1);
|
|
1461
|
+
changed = true;
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
const parent = localSupportDocs
|
|
1465
|
+
.filter((candidate) => reachable.has(candidate.artifact.path))
|
|
1466
|
+
.sort((left, right) => (reachable.get(left.artifact.path) ?? 0) -
|
|
1467
|
+
(reachable.get(right.artifact.path) ?? 0))
|
|
1468
|
+
.find((candidate) => references.get(candidate.artifact.path)?.has(document.artifact.path));
|
|
1469
|
+
if (parent) {
|
|
1470
|
+
reachable.set(document.artifact.path, (reachable.get(parent.artifact.path) ?? 0) + 1);
|
|
1307
1471
|
changed = true;
|
|
1308
1472
|
}
|
|
1309
1473
|
}
|
|
1310
1474
|
}
|
|
1311
1475
|
return reachable;
|
|
1312
1476
|
}
|
|
1313
|
-
function
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1477
|
+
function localSupportPathReferences(document, skillDir, candidatePaths) {
|
|
1478
|
+
return staticSupportReferences(document, skillDir, candidatePaths).map((reference) => ({
|
|
1479
|
+
raw: reference.raw,
|
|
1480
|
+
relative: reference.relativePath,
|
|
1481
|
+
target: reference.targetPath,
|
|
1482
|
+
line: reference.line,
|
|
1483
|
+
}));
|
|
1319
1484
|
}
|
|
1320
1485
|
function markdownBodyLineIndexes(document) {
|
|
1321
1486
|
if (document.lines[0]?.trim() !== "---") {
|
|
@@ -1374,34 +1539,11 @@ function thinSkillLayoutFindings(document) {
|
|
|
1374
1539
|
if (classifyRepositorySkillEntrypointPath(document.artifact.path)?.kind !==
|
|
1375
1540
|
"canonical")
|
|
1376
1541
|
return [];
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
];
|
|
1383
|
-
if (wordCount > 450 ||
|
|
1384
|
-
procedureSignals.some((pattern) => pattern.test(document.artifact.content))) {
|
|
1385
|
-
findings.push(documentFinding(document, DIAGNOSTIC_IDS.LAYOUT_SKILL_NOT_THIN, "Skill entrypoint contains procedure content", "structure", wordCount > 700 ? "medium" : "low", "Move reusable procedure content into contexts/** and keep SKILL.md as a concise router that points to the required context assets.", {
|
|
1386
|
-
whyItMatters: "Canonical Skill entrypoints should route agents instead of owning reusable knowledge, helper implementations, or detailed procedures.",
|
|
1387
|
-
verificationSteps: [
|
|
1388
|
-
"Confirm SKILL.md contains routing guidance and required context references only.",
|
|
1389
|
-
"Run renma readiness and check layout.skills_thin.",
|
|
1390
|
-
],
|
|
1391
|
-
llmHint: "Extract long procedure sections into contexts/**, add metadata.renma.requires-context or metadata.renma.optional-context JSON-array strings, and keep SKILL.md focused on when to use or not use the Skill. Pre-0.16 top-level references are migration input only.",
|
|
1392
|
-
}));
|
|
1393
|
-
}
|
|
1394
|
-
const command = firstExecutableCommand(document);
|
|
1395
|
-
if (command) {
|
|
1396
|
-
findings.push(findingAt(document, DIAGNOSTIC_IDS.LAYOUT_SKILL_EXECUTABLE_COMMAND, "Skill entrypoint contains executable command", "structure", "low", command.line, command.command, "Move executable command detail into Skill-local references or governed contexts/** procedures, and keep SKILL.md as a router.", {
|
|
1397
|
-
whyItMatters: "Executable setup commands in SKILL.md make the entrypoint procedural instead of a concise routing layer.",
|
|
1398
|
-
verificationSteps: [
|
|
1399
|
-
"Move the command guidance to a context/reference/procedure.",
|
|
1400
|
-
"If the command invokes a helper script, ensure it resolves to local scripts/** in the owning Skill or a shared tools/** helper.",
|
|
1401
|
-
],
|
|
1402
|
-
}));
|
|
1403
|
-
}
|
|
1404
|
-
return findings;
|
|
1542
|
+
// Agent Skills explicitly permits step-by-step instructions and command
|
|
1543
|
+
// examples in SKILL.md. Focused-workflow quality is evaluated by dedicated
|
|
1544
|
+
// routing, input, completion, progressive-disclosure, and mixed-knowledge
|
|
1545
|
+
// rules instead of the removed 0.17 thin-router heuristic.
|
|
1546
|
+
return [];
|
|
1405
1547
|
}
|
|
1406
1548
|
function helperCommandFindings(document, paths) {
|
|
1407
1549
|
const findings = [];
|
|
@@ -1546,9 +1688,6 @@ function executableCommands(document) {
|
|
|
1546
1688
|
}))
|
|
1547
1689
|
.filter(({ command }) => /^(node|bash|sh|python|python3)\s+/.test(command)));
|
|
1548
1690
|
}
|
|
1549
|
-
function firstExecutableCommand(document) {
|
|
1550
|
-
return executableCommands(document)[0];
|
|
1551
|
-
}
|
|
1552
1691
|
function unresolvedHelperCommandFinding(document, command, scriptPath, details = {}) {
|
|
1553
1692
|
const unsafePath = details.details?.unsafePath === true;
|
|
1554
1693
|
return findingAt(document, DIAGNOSTIC_IDS.PATH_HELPER_COMMAND_UNRESOLVED, "Helper command target does not resolve", "structure", "medium", command.line, command.command, unsafePath
|
|
@@ -1578,9 +1717,6 @@ function lineText(document, line) {
|
|
|
1578
1717
|
function isRepoPathLike(value) {
|
|
1579
1718
|
return /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/.test(value);
|
|
1580
1719
|
}
|
|
1581
|
-
function words(text) {
|
|
1582
|
-
return text.match(/[A-Za-z0-9_./+=-]+|[^\sA-Za-z0-9_./+=-]/g) ?? [];
|
|
1583
|
-
}
|
|
1584
1720
|
function finding(id, title, category, severity, document, remediation, details = {}) {
|
|
1585
1721
|
return {
|
|
1586
1722
|
id,
|
|
@@ -1635,15 +1771,12 @@ function localSupportUnreachableRuleId(kind) {
|
|
|
1635
1771
|
return DIAGNOSTIC_IDS.SUPPORT_UNREACHABLE_PROFILE;
|
|
1636
1772
|
if (kind === "example")
|
|
1637
1773
|
return DIAGNOSTIC_IDS.SUPPORT_UNREACHABLE_EXAMPLE;
|
|
1774
|
+
if (kind === "script")
|
|
1775
|
+
return DIAGNOSTIC_IDS.SUPPORT_UNREACHABLE_SCRIPT;
|
|
1776
|
+
if (kind === "asset")
|
|
1777
|
+
return DIAGNOSTIC_IDS.SUPPORT_UNREACHABLE_ASSET;
|
|
1638
1778
|
return DIAGNOSTIC_IDS.SUPPORT_UNREACHABLE_REFERENCE;
|
|
1639
1779
|
}
|
|
1640
|
-
function escapeRegExp(value) {
|
|
1641
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1642
|
-
}
|
|
1643
|
-
function approximateTokenCount(text) {
|
|
1644
|
-
const matches = text.match(/[A-Za-z0-9_./+=-]+|[^\sA-Za-z0-9_./+=-]/g);
|
|
1645
|
-
return matches?.length ?? 0;
|
|
1646
|
-
}
|
|
1647
1780
|
function isPlaceholder(line) {
|
|
1648
1781
|
return /(?:example|placeholder|your_|<[^>]+>|\$\{[^}]+})/i.test(line);
|
|
1649
1782
|
}
|