fix: resolve scoped guidance review findings · airscripts/agentskill@cfb0ed1 · GitHub
Skip to content

Commit cfb0ed1

Browse files
committed
fix: resolve scoped guidance review findings
1 parent 7f193d5 commit cfb0ed1

7 files changed

Lines changed: 259 additions & 8 deletions

File tree

‎ROADMAP.md‎

Lines changed: 5 additions & 4 deletions

‎agentskill-analyzers/tests/contracts.rs‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,31 @@ fn scope_manifest_and_budgeted_evidence_are_deterministic() {
188188
assert_eq!(paths, vec![".", "packages/api"]);
189189

190190
let selected = ["packages/api".to_string()];
191+
192+
for (mode, input_tokens, output_tokens, follow_up_rounds) in [
193+
("compact", 4_000, 512, 1),
194+
("standard", 8_000, 1_000, 2),
195+
("deep", 16_000, 2_000, 4),
196+
] {
197+
let profile =
198+
agentskill_analyzers::run_evidence_scoped(&repo, None, Some(&selected), Some(mode))
199+
.unwrap();
200+
201+
assert_eq!(profile["budget"]["mode"], mode);
202+
assert_eq!(profile["budget"]["input_tokens"], input_tokens);
203+
assert_eq!(profile["budget"]["output_tokens"], output_tokens);
204+
assert_eq!(profile["budget"]["follow_up_rounds"], follow_up_rounds);
205+
let tree_len = profile["analyzers"]["scan"]["tree"]
206+
.as_array()
207+
.unwrap()
208+
.len();
209+
if mode == "compact" {
210+
assert!(tree_len <= 32);
211+
} else {
212+
assert!(tree_len > 32);
213+
}
214+
}
215+
191216
let evidence =
192217
agentskill_analyzers::run_evidence_scoped(&repo, None, Some(&selected), Some("compact"))
193218
.unwrap();
@@ -235,6 +260,18 @@ fn scope_manifest_and_budgeted_evidence_are_deterministic() {
235260
.len()
236261
<= 32
237262
);
263+
assert!(
264+
evidence["analyzers"]["scan"]["read_order"]
265+
.as_array()
266+
.unwrap()
267+
.len()
268+
<= 16
269+
);
270+
for result in evidence["analyzers"]["graph"].as_object().unwrap().values() {
271+
if let Some(edges) = result["edges"].as_array() {
272+
assert!(edges.len() <= 32);
273+
}
274+
}
238275

239276
let standard =
240277
agentskill_analyzers::run_evidence_scoped(&repo, None, Some(&selected), Some("standard"))
@@ -248,6 +285,22 @@ fn scope_manifest_and_budgeted_evidence_are_deterministic() {
248285
> 32
249286
);
250287

288+
let deep =
289+
agentskill_analyzers::run_evidence_scoped(&repo, None, Some(&selected), Some("deep"))
290+
.unwrap();
291+
assert!(deep["analyzers"]["scan"]["tree"].as_array().unwrap().len() > 32);
292+
assert!(
293+
deep["analyzers"]["scan"]["tree"].as_array().unwrap().len()
294+
>= standard["analyzers"]["scan"]["tree"]
295+
.as_array()
296+
.unwrap()
297+
.len()
298+
);
299+
assert!(
300+
agentskill_analyzers::run_evidence_scoped(&repo, None, Some(&selected), Some("unknown"))
301+
.is_err()
302+
);
303+
251304
let explicit = ["src".to_string()];
252305
let explicit_manifest = agentskill_analyzers::run_scopes(&repo, Some(&explicit)).unwrap();
253306
assert_eq!(explicit_manifest["scopes"][0]["path"], "src");

‎agentskill-docs/architecture.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ Validation and drift report unsupported or low-confidence evidence, duplicate
103103
inherited rules, and contradictory inherited rules; semantic rule authorship
104104
remains with the LLM.
105105

106+
The Rust runtime bounds evidence detail for the selected profile. The invoking
107+
LLM harness owns actual input-context, output-token, and follow-up-round
108+
ceilings using the profile metadata; the runtime does not perform token
109+
counting.
110+
106111
## Crate Responsibilities
107112

108113
### Core

‎agentskill-generation/src/lib.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,6 +1026,21 @@ fn validate_markdown(
10261026
));
10271027
}
10281028

1029+
let is_operational = path.file_name().and_then(|name| name.to_str()) == Some("AGENTS.md");
1030+
let reference = path.with_file_name("AGENTS.reference.md");
1031+
if is_operational && reference.is_file() && !content.contains("AGENTS.reference.md") {
1032+
let message = "AGENTS.md must reference local AGENTS.reference.md when it exists";
1033+
errors.push(format!("{}: {message}", display_path(root, path).display()));
1034+
1035+
findings.push(finding(
1036+
"unreferenced_reference_document",
1037+
"error",
1038+
&display_path(root, path).to_string_lossy(),
1039+
message,
1040+
Some("AGENTS.reference.md"),
1041+
));
1042+
}
1043+
10291044
for referenced in referenced_paths(&content) {
10301045
if referenced != "."
10311046
&& !is_optional_reference(root, &referenced)

‎agentskill-generation/tests/workflows.rs‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,24 @@ fn validates_operational_and_reference_documents() {
3333
let result = validate(directory.path().to_string_lossy().as_ref()).unwrap();
3434

3535
assert_eq!(result["valid"], true);
36+
37+
let without_reference_link = fs::read_to_string(directory.path().join("AGENTS.md"))
38+
.unwrap()
39+
.replace("\nRead `AGENTS.reference.md`.", "");
40+
fs::write(directory.path().join("AGENTS.md"), without_reference_link).unwrap();
41+
42+
let unreferenced = validate(directory.path().to_string_lossy().as_ref()).unwrap();
43+
assert_eq!(unreferenced["valid"], false);
44+
assert!(
45+
unreferenced["findings"]
46+
.as_array()
47+
.unwrap()
48+
.iter()
49+
.any(|finding| {
50+
finding["kind"] == "unreferenced_reference_document"
51+
&& finding["document"] == "AGENTS.md"
52+
})
53+
);
3654
}
3755

3856
#[test]
@@ -489,6 +507,153 @@ fn validates_nested_scopes_and_reports_missing_candidates_advisory() {
489507
);
490508
}
491509

510+
#[test]
511+
fn validates_nested_references_with_inherited_signature_configuration() {
512+
let directory = tempdir().unwrap();
513+
fs::write(
514+
directory.path().join("agentskill.toml"),
515+
"signature = false\n",
516+
)
517+
.unwrap();
518+
fs::write(
519+
directory.path().join("AGENTS.md"),
520+
"# Root Guidance\n\n## Free Region\n\nRoot custom instructions.\n",
521+
)
522+
.unwrap();
523+
fs::create_dir_all(directory.path().join("packages/api/src")).unwrap();
524+
fs::write(
525+
directory.path().join("packages/api/src/lib.rs"),
526+
"pub fn api() {}\n",
527+
)
528+
.unwrap();
529+
fs::write(
530+
directory.path().join("packages/api/AGENTS.md"),
531+
"# API Guidance\n\n## Scope\n\n- Path: packages/api\n- Parent: .\n- Inheritance: additive; nearest scope wins.\n\n## Mission\n\nRead `src/lib.rs`.\n\nRead `AGENTS.reference.md` for local API context.\n\n## Free Region\n\nAPI custom instructions.\n",
532+
)
533+
.unwrap();
534+
fs::write(
535+
directory.path().join("packages/api/AGENTS.reference.md"),
536+
"# API Reference\n\n## Provenance And Decisions\n\n- Agentskill Version: `2.0.0`.\n- Evidence Schema Version: `4`.\n- Repository Revision: `fixture-revision`.\n- Configuration: inherited signature disabled.\n- Maintainer-Confirmed Decisions: Scope uses local API instructions.\n- Unresolved Uncertainty: None recorded.\n\nLocal API reference context.\n",
537+
)
538+
.unwrap();
539+
540+
let root = fs::read_to_string(directory.path().join("AGENTS.md")).unwrap();
541+
let scoped = fs::read_to_string(directory.path().join("packages/api/AGENTS.md")).unwrap();
542+
let repo = directory.path().to_string_lossy().into_owned();
543+
544+
let report = validate_with_mode_and_scopes(&repo, SignatureMode::Auto, None).unwrap();
545+
assert_eq!(report["valid"], true);
546+
assert_eq!(report["configuration"]["signature"], false);
547+
assert!(
548+
report["validated_scopes"]
549+
.as_array()
550+
.unwrap()
551+
.iter()
552+
.any(|path| path == "packages/api")
553+
);
554+
555+
let root_free = parse(&root)
556+
.sections
557+
.into_iter()
558+
.find(|section| section.heading == "Free Region")
559+
.unwrap()
560+
.body;
561+
let scoped_free = parse(&scoped)
562+
.sections
563+
.into_iter()
564+
.find(|section| section.heading == "Free Region")
565+
.unwrap()
566+
.body;
567+
assert_ne!(root_free, scoped_free);
568+
569+
let without_reference_link =
570+
scoped.replace("\nRead `AGENTS.reference.md` for local API context.", "");
571+
fs::write(
572+
directory.path().join("packages/api/AGENTS.md"),
573+
without_reference_link,
574+
)
575+
.unwrap();
576+
577+
let unreferenced = validate_with_mode_and_scopes(&repo, SignatureMode::Auto, None).unwrap();
578+
assert_eq!(unreferenced["valid"], false);
579+
assert!(
580+
unreferenced["findings"]
581+
.as_array()
582+
.unwrap()
583+
.iter()
584+
.any(|finding| {
585+
finding["kind"] == "unreferenced_reference_document"
586+
&& finding["document"] == "AGENTS.md"
587+
})
588+
);
589+
590+
fs::write(directory.path().join("packages/api/AGENTS.md"), &scoped).unwrap();
591+
592+
let overridden = validate_with_mode_and_scopes(&repo, SignatureMode::On, None).unwrap();
593+
assert_eq!(overridden["valid"], false);
594+
assert!(
595+
overridden["findings"]
596+
.as_array()
597+
.unwrap()
598+
.iter()
599+
.any(|finding| finding["kind"] == "missing_signature")
600+
);
601+
602+
let disabled = validate_with_mode_and_scopes(&repo, SignatureMode::Off, None).unwrap();
603+
assert_eq!(disabled["valid"], true);
604+
}
605+
606+
#[test]
607+
fn legacy_scoped_documents_are_advisory_until_explicitly_adopted() {
608+
let directory = tempdir().unwrap();
609+
fs::write(
610+
directory.path().join("AGENTS.md"),
611+
"# Root Guidance\n\n## Free Region\n\nRoot custom instructions.\n",
612+
)
613+
.unwrap();
614+
fs::create_dir_all(directory.path().join("packages/api")).unwrap();
615+
fs::write(
616+
directory.path().join("packages/api/AGENTS.md"),
617+
"# Legacy API Guidance\n\n## Free Region\n\nLegacy custom instructions.\n",
618+
)
619+
.unwrap();
620+
621+
let path = directory.path().join("packages/api/AGENTS.md");
622+
let before = fs::read_to_string(&path).unwrap();
623+
let repo = directory.path().to_string_lossy().into_owned();
624+
let scopes = agentskill_analyzers::run_scopes(&repo, None).unwrap();
625+
let legacy = scopes["scopes"]
626+
.as_array()
627+
.unwrap()
628+
.iter()
629+
.find(|scope| scope["path"] == "packages/api")
630+
.unwrap();
631+
632+
assert_eq!(legacy["status"], "legacy");
633+
634+
let report = validate_with_mode_and_scopes(&repo, SignatureMode::Off, None).unwrap();
635+
assert_eq!(report["valid"], true);
636+
assert!(
637+
report["findings"]
638+
.as_array()
639+
.unwrap()
640+
.iter()
641+
.any(|finding| {
642+
finding["kind"] == "legacy_scoped_document"
643+
&& finding["severity"] == "warning"
644+
&& finding["document"] == "packages/api"
645+
})
646+
);
647+
assert!(
648+
!report["validated_scopes"]
649+
.as_array()
650+
.unwrap()
651+
.iter()
652+
.any(|path| path == "packages/api")
653+
);
654+
assert_eq!(fs::read_to_string(&path).unwrap(), before);
655+
}
656+
492657
#[test]
493658
fn scoped_validation_rejects_incorrect_parent_metadata() {
494659
let directory = tempdir().unwrap();

‎agentskill-skill/SKILL.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ finished document.
2424
7. Write compact managed `AGENTS.md` guidance and reference context when
2525
provenance or decisions require it. Scoped documents include local `## Scope`
2626
metadata, an independent `## Free Region`, and an explicit parent link.
27-
8. Present the complete semantic plan and diff before writing. Never create
27+
8. Treat legacy scoped documents as read-only advisory candidates. Adopt or
28+
migrate one only after explicit, reviewed LLM workflow selection; never
29+
rewrite it automatically.
30+
9. Present the complete semantic plan and diff before writing. Never create
2831
documents for merely suggested candidates.
29-
9. Run `agentskill validate <repo> --signature <mode>` and show the semantic
32+
10. Run `agentskill validate <repo> --signature <mode>` and show the semantic
3033
diff. Use `auto` unless the workflow request explicitly selects `on` or
3134
`off`.
3235

‎agentskill-skill/SYSTEM.md‎

Lines changed: 11 additions & 2 deletions

0 commit comments

Comments
 (0)