@event4u/agent-config 9.12.0 → 9.14.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +110 -238
- package/README.md +1 -1
- package/dist/agent-src/commands/optimize/deep.md +147 -0
- package/dist/agent-src/commands/optimize.md +12 -6
- package/dist/agent-src/rules/active-remediation.md +0 -2
- package/dist/agent-src/rules/architecture.md +0 -1
- package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -2
- package/dist/agent-src/rules/broken-access-control.md +0 -2
- package/dist/agent-src/rules/code-comment-discipline.md +0 -3
- package/dist/agent-src/rules/code-provenance.md +0 -3
- package/dist/agent-src/rules/communication-through-line.md +2 -3
- package/dist/agent-src/rules/content-quoting-floor.md +0 -2
- package/dist/agent-src/rules/context-hygiene.md +1 -3
- package/dist/agent-src/rules/decision-revisit-gate.md +0 -2
- package/dist/agent-src/rules/design-fidelity.md +29 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +0 -3
- package/dist/agent-src/rules/downstream-changes.md +10 -1
- package/dist/agent-src/rules/external-reference-deep-dive.md +0 -7
- package/dist/agent-src/rules/fast-path-marker-visibility.md +0 -1
- package/dist/agent-src/rules/git-history-discipline.md +0 -7
- package/dist/agent-src/rules/guidelines.md +0 -2
- package/dist/agent-src/rules/improve-before-implement.md +4 -2
- package/dist/agent-src/rules/invite-challenge.md +0 -4
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -2
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -2
- package/dist/agent-src/rules/missing-tool-handling.md +0 -1
- package/dist/agent-src/rules/no-attribution-footers.md +0 -3
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +8 -5
- package/dist/agent-src/rules/no-pr-progress-comments.md +1 -3
- package/dist/agent-src/rules/no-roadmap-references.md +0 -2
- package/dist/agent-src/rules/output-discipline.md +0 -1
- package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -2
- package/dist/agent-src/rules/preservation-guard.md +0 -3
- package/dist/agent-src/rules/role-mode-adherence.md +0 -1
- package/dist/agent-src/rules/secret-vcs-guard.md +0 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +0 -2
- package/dist/agent-src/rules/session-canary.md +0 -2
- package/dist/agent-src/rules/size-enforcement.md +0 -5
- package/dist/agent-src/rules/source-confidentiality.md +0 -3
- package/dist/agent-src/rules/source-discovery-gate.md +0 -2
- package/dist/agent-src/rules/think-before-action.md +3 -3
- package/dist/agent-src/rules/token-budget-discipline.md +0 -1
- package/dist/agent-src/rules/token-efficiency.md +1 -2
- package/dist/agent-src/rules/tool-safety.md +0 -1
- package/dist/agent-src/rules/user-interaction.md +0 -3
- package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +8 -0
- package/dist/agent-src/skills/analysis-skill-router/SKILL.md +55 -86
- package/dist/agent-src/skills/character-consistency/SKILL.md +7 -4
- package/dist/agent-src/skills/decision-record/SKILL.md +8 -0
- package/dist/agent-src/skills/design-review/SKILL.md +11 -0
- package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +8 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +34 -3
- package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
- package/dist/agent-src/skills/motion-choreographer/SKILL.md +7 -4
- package/dist/agent-src/skills/overbuild-review-lens/SKILL.md +201 -0
- package/dist/agent-src/skills/performance/SKILL.md +14 -0
- package/dist/agent-src/skills/pixar-storyteller/SKILL.md +8 -5
- package/dist/agent-src/skills/scene-expander/SKILL.md +9 -6
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +10 -0
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +14 -37
- package/dist/agent-src/skills/video-director/SKILL.md +9 -6
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +154 -13
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +40 -0
- package/dist/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-GCVFLBP2.js} +399 -365
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +123 -73
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +174 -13
- package/dist/install/install.mjs +648 -614
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +783 -507
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +2 -2
- package/docs/MIGRATION.md +1 -1
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +71 -0
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +24 -1
- package/docs/guidelines/code-clarity.md +9 -2
- package/docs/guidelines/design-fidelity-mechanics.md +44 -0
- package/llms.txt +63 -27
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +154 -13
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +40 -0
- package/src/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/src/config/agent-settings.template.yml +8 -0
- package/src/scripts/_lib/counted_probe.ts +142 -0
- package/src/scripts/_lib/exec_evidence.ts +5 -0
- package/src/scripts/_lib/overbuild_lens_contract.ts +196 -0
- package/src/scripts/_lib/release_scope.ts +207 -0
- package/src/scripts/_lib/scoped_projection.ts +212 -0
- package/src/scripts/ai_council/stance_tally.ts +37 -2
- package/src/scripts/audit_auto_rules.ts +9 -9
- package/src/scripts/audit_likelihood.ts +3 -3
- package/src/scripts/audit_overlap.ts +2 -2
- package/src/scripts/audit_skill_overlap.ts +233 -95
- package/src/scripts/audit_skill_overlap_allowlist.json +4 -0
- package/src/scripts/build_proof.ts +15 -1
- package/src/scripts/check_artefact_count_messaging.ts +128 -15
- package/src/scripts/check_dependency_floors.ts +16 -6
- package/src/scripts/check_gate_coverage.ts +6 -6
- package/src/scripts/check_kernel_rule_bundle.ts +5 -4
- package/src/scripts/check_token_quality_golden.ts +0 -18
- package/src/scripts/code_graph/loader.ts +35 -6
- package/src/scripts/compile_router.ts +3 -8
- package/src/scripts/condense.ts +2 -2
- package/src/scripts/count_scoped_projection.ts +104 -0
- package/src/scripts/explain_run.ts +0 -1
- package/src/scripts/generate_pack_manifests.ts +3 -20
- package/src/scripts/hooks/block_kernel_rule_writes.ts +145 -10
- package/src/scripts/hooks/block_no_verify.ts +101 -0
- package/src/scripts/install.ts +13 -90
- package/src/scripts/lint_agent_security.ts +5 -4
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_regression.ts +92 -1
- package/src/scripts/lint_skill_descriptions.ts +93 -5
- package/src/scripts/project_thin_rules.ts +2 -2
- package/src/scripts/release.ts +107 -3
- package/src/scripts/resolve_lint_scope.ts +53 -0
- package/src/scripts/router_telemetry.ts +0 -5
- package/src/scripts/runtime_dispatcher.ts +40 -0
- package/src/scripts/schemas/pack.schema.json +1 -5
- package/src/scripts/schemas/rule.schema.json +1 -2
- package/src/scripts/self_review_gate.ts +15 -73
- package/src/scripts/skill_linter.ts +160 -11
- package/src/scripts/sweep_dead_scan_roots.ts +550 -0
- package/src/scripts/trigger_coverage.ts +11 -37
- package/src/scripts/update_counts.ts +90 -11
package/src/scripts/release.ts
CHANGED
|
@@ -357,6 +357,109 @@ const SECTIONS: ReadonlyArray<readonly [string, string | null, readonly string[]
|
|
|
357
357
|
['Chores', null, ['chore']],
|
|
358
358
|
];
|
|
359
359
|
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Curated head (road-to-release-shape-honesty Phase 2)
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The five head lines, in the order an operator reads them.
|
|
366
|
+
*
|
|
367
|
+
* Reviewers of 9.9.0 and 9.10.0 repeatedly could not tell, from a generated
|
|
368
|
+
* commit log, which entries change consumer behaviour, which need migration,
|
|
369
|
+
* which are internal gate repairs, and which ended as nulls. The log is a
|
|
370
|
+
* faithful record of *what was committed*; it is not a statement of *what
|
|
371
|
+
* changed for you*. The head is that statement, and it sits above the log
|
|
372
|
+
* rather than replacing it.
|
|
373
|
+
*
|
|
374
|
+
* Wording is taken from `docs/RELEASE_STORY_TEMPLATE.md` rather than invented,
|
|
375
|
+
* so the package does not end up with two competing definitions of a curated
|
|
376
|
+
* head.
|
|
377
|
+
*/
|
|
378
|
+
const RELEASE_HEAD_SECTIONS: ReadonlyArray<string> = [
|
|
379
|
+
'Behaviour changes',
|
|
380
|
+
'Default changes + migration',
|
|
381
|
+
'Security and correctness',
|
|
382
|
+
'Honest nulls',
|
|
383
|
+
'Known limitations',
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Cap on the head, in rendered lines.
|
|
388
|
+
*
|
|
389
|
+
* Roughly ten operator-relevant lines: past that it stops being a summary and
|
|
390
|
+
* becomes a second changelog. The era budget is also finite
|
|
391
|
+
* (`_lib/changelog_eras.ts`, 250 lines per era), so an unbounded head would
|
|
392
|
+
* quietly consume it.
|
|
393
|
+
*/
|
|
394
|
+
export const RELEASE_HEAD_CAP_LINES = 10;
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The default value of every head line.
|
|
398
|
+
*
|
|
399
|
+
* `_none_` and not a placeholder token: for most releases it is the **true**
|
|
400
|
+
* answer, and a release that genuinely changed no defaults should say so
|
|
401
|
+
* rather than carry an unfilled marker. That also keeps the skeleton free of
|
|
402
|
+
* the placeholder prose `output-discipline` bans — there is nothing here that
|
|
403
|
+
* is wrong-if-shipped, only something that is uninformative-if-unedited.
|
|
404
|
+
*/
|
|
405
|
+
const HEAD_DEFAULT = '_none_';
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Render the curated head. Emitted by the generator on every release so it
|
|
409
|
+
* cannot be forgotten; edited by the maintainer before merge.
|
|
410
|
+
*/
|
|
411
|
+
export function render_release_head(
|
|
412
|
+
filled: Readonly<Record<string, string>> = {},
|
|
413
|
+
): string[] {
|
|
414
|
+
const lines: string[] = [
|
|
415
|
+
'### Release highlights',
|
|
416
|
+
'',
|
|
417
|
+
'<!-- Curated head: fill before merge, keep it under ' +
|
|
418
|
+
`${RELEASE_HEAD_CAP_LINES} lines, and leave \`${HEAD_DEFAULT}\` where it is ` +
|
|
419
|
+
'genuinely the answer. The generated log below is unchanged. -->',
|
|
420
|
+
];
|
|
421
|
+
for (const label of RELEASE_HEAD_SECTIONS) {
|
|
422
|
+
const value = (filled[label] ?? '').trim();
|
|
423
|
+
lines.push(`- **${label}:** ${value === '' ? HEAD_DEFAULT : value}`);
|
|
424
|
+
}
|
|
425
|
+
return lines;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Drop commits whose rendered line would be identical.
|
|
430
|
+
*
|
|
431
|
+
* The generated log is keyed by SHA, but a reader is not: a cherry-pick, a
|
|
432
|
+
* re-land, or one change split across branches produces the same
|
|
433
|
+
* `scope: subject` twice with different SHAs, and the notes then say the same
|
|
434
|
+
* thing twice with two links. Keyed on `type + scope + subject` — the parts
|
|
435
|
+
* that reach the rendered line — keeping the first occurrence, so the earliest
|
|
436
|
+
* SHA stays the citation.
|
|
437
|
+
*
|
|
438
|
+
* Breaking commits are never folded into a non-breaking twin: `!` changes what
|
|
439
|
+
* the line *means*, and collapsing them would hide a breaking change behind a
|
|
440
|
+
* routine one.
|
|
441
|
+
*/
|
|
442
|
+
export function dedupe_commit_lines(commits: readonly Commit[]): Commit[] {
|
|
443
|
+
const seen = new Set<string>();
|
|
444
|
+
const out: Commit[] = [];
|
|
445
|
+
for (const c of commits) {
|
|
446
|
+
const key = [c.breaking ? '!' : '', c.type, c.scope ?? '', c.subject].join('\x1f');
|
|
447
|
+
if (seen.has(key)) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
seen.add(key);
|
|
451
|
+
out.push(c);
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Rendered length of the head, for the cap assertion. */
|
|
457
|
+
export function release_head_line_count(head: readonly string[]): number {
|
|
458
|
+
// The HTML comment is invisible to a reader, so it does not count against
|
|
459
|
+
// an operator-facing cap.
|
|
460
|
+
return head.filter((l) => !l.trimStart().startsWith('<!--')).length;
|
|
461
|
+
}
|
|
462
|
+
|
|
360
463
|
const BREAKING_RE = /^([a-z]+)(\([^)]+\))?!:/;
|
|
361
464
|
const CONVENTIONAL_RE = /^(?<type>[a-z]+)(?:\((?<scope>[^)]+)\))?(?<bang>!)?: (?<subject>.+)$/;
|
|
362
465
|
|
|
@@ -687,7 +790,7 @@ function render_changelog_entry(
|
|
|
687
790
|
prev: string | null,
|
|
688
791
|
commits: readonly Commit[],
|
|
689
792
|
today: string,
|
|
690
|
-
opts: { test_trend_line?: string | null } = {},
|
|
793
|
+
opts: { test_trend_line?: string | null; head?: Readonly<Record<string, string>> } = {},
|
|
691
794
|
): [string, string] {
|
|
692
795
|
const test_trend_line = opts.test_trend_line ?? null;
|
|
693
796
|
let heading: string;
|
|
@@ -706,7 +809,7 @@ function render_changelog_entry(
|
|
|
706
809
|
}
|
|
707
810
|
grouped['BREAKING CHANGES'] = [];
|
|
708
811
|
const other: Commit[] = [];
|
|
709
|
-
for (const c of commits) {
|
|
812
|
+
for (const c of dedupe_commit_lines(commits)) {
|
|
710
813
|
if (c.breaking) {
|
|
711
814
|
grouped['BREAKING CHANGES']!.push(c);
|
|
712
815
|
continue;
|
|
@@ -724,7 +827,8 @@ function render_changelog_entry(
|
|
|
724
827
|
}
|
|
725
828
|
}
|
|
726
829
|
|
|
727
|
-
|
|
830
|
+
// The curated head sits above the generated log, which stays unchanged.
|
|
831
|
+
const body_lines: string[] = [...render_release_head(opts.head ?? {})];
|
|
728
832
|
const ordered_labels = ['BREAKING CHANGES', ...SECTIONS.map(([label]) => label)];
|
|
729
833
|
for (const label of ordered_labels) {
|
|
730
834
|
const bucket = grouped[label] ?? [];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Print the diff base a content check should compare HEAD against.
|
|
4
|
+
*
|
|
5
|
+
* A resolver, not a gate — it decides nothing and blocks nothing. It exists so
|
|
6
|
+
* the shell-side checks in `.github/workflows/skill-lint.yml` resolve their
|
|
7
|
+
* scope from the same source as `skill_linter --changed`, instead of hardcoding
|
|
8
|
+
* `origin/${base_ref}` and going blind on a release PR (the "0 changed corpus
|
|
9
|
+
* files, INCONCLUSIVE" outcome).
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* resolve_lint_scope [--base-ref <ref>] [--json]
|
|
13
|
+
*
|
|
14
|
+
* Prints the base ref on stdout and the reason on stderr, so a caller can do
|
|
15
|
+
* BASE="$(./scripts-run src/scripts/resolve_lint_scope --base-ref "origin/$X")"
|
|
16
|
+
* and still see the explanation in the log.
|
|
17
|
+
*
|
|
18
|
+
* Exit codes: 0 always — a resolver that cannot widen reports the ordinary
|
|
19
|
+
* base and says why. Failing here would turn an unresolvable tag into a red
|
|
20
|
+
* release for the wrong reason; the emptiness decision belongs to the gate.
|
|
21
|
+
*/
|
|
22
|
+
import { resolveContentLintScope } from './_lib/release_scope.js';
|
|
23
|
+
|
|
24
|
+
export function main(argv: string[]): number {
|
|
25
|
+
let baseRef: string | undefined;
|
|
26
|
+
let asJson = false;
|
|
27
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
28
|
+
const a = argv[i] as string;
|
|
29
|
+
if (a === '--base-ref') {
|
|
30
|
+
baseRef = argv[i + 1];
|
|
31
|
+
i += 1;
|
|
32
|
+
} else if (a.startsWith('--base-ref=')) {
|
|
33
|
+
baseRef = a.slice('--base-ref='.length);
|
|
34
|
+
} else if (a === '--json') {
|
|
35
|
+
asJson = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const scope = resolveContentLintScope(baseRef === undefined ? {} : { baseRef });
|
|
40
|
+
if (asJson) {
|
|
41
|
+
process.stdout.write(`${JSON.stringify(scope)}\n`);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
process.stderr.write(`resolve_lint_scope: ${scope.reason}\n`);
|
|
45
|
+
process.stdout.write(`${scope.base}\n`);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const invokedDirectly =
|
|
50
|
+
process.argv[1] !== undefined && /resolve_lint_scope\.ts$/.test(process.argv[1]);
|
|
51
|
+
if (invokedDirectly) {
|
|
52
|
+
process.exit(main(process.argv.slice(2)));
|
|
53
|
+
}
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
* | `keyword` | case-insensitive substring inside the prompt text |
|
|
25
25
|
* | `phrase` | case-insensitive substring (multi-word) inside the prompt text |
|
|
26
26
|
* | `command` | case-sensitive prefix on `command:` field (optional context) |
|
|
27
|
-
* | `intent` | informational only — never auto-matches; counted separately |
|
|
28
27
|
* | `path_prefix` | prefix match against any path in `open_files` (optional context) |
|
|
29
28
|
* | `file_pattern` | fnmatch against any path in `open_files` (optional context) |
|
|
30
29
|
*
|
|
@@ -227,10 +226,6 @@ export function trigger_matches(
|
|
|
227
226
|
}
|
|
228
227
|
return false;
|
|
229
228
|
}
|
|
230
|
-
if ("intent" in trigger) {
|
|
231
|
-
// Intent triggers are informational and never auto-match.
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
229
|
return false;
|
|
235
230
|
}
|
|
236
231
|
|
|
@@ -34,6 +34,7 @@ import { build_registry } from './runtime_registry.js';
|
|
|
34
34
|
import type { ExecutionResult} from './runtime_handler.js';
|
|
35
35
|
import { HandlerError, execute_shell } from './runtime_handler.js';
|
|
36
36
|
import { resolve_level } from './_lib/script_output.js';
|
|
37
|
+
import { validate_tool_declarations } from './tool_registry.js';
|
|
37
38
|
|
|
38
39
|
const _HERE = fileURLToPath(import.meta.url);
|
|
39
40
|
|
|
@@ -187,6 +188,45 @@ export function dispatch(skillName: string, registry: SkillRuntime[]): DispatchR
|
|
|
187
188
|
warnings: [],
|
|
188
189
|
});
|
|
189
190
|
}
|
|
191
|
+
// Gate integrity — the grant resolves against TRUSTED config, never
|
|
192
|
+
// against the skill's own frontmatter.
|
|
193
|
+
//
|
|
194
|
+
// road-to-governance-invariants Phase 4, property (b). Every field the
|
|
195
|
+
// three checks above read (`execution_type`, `handler`, `safety_mode`)
|
|
196
|
+
// comes out of the skill file's own YAML, so a skill that writes
|
|
197
|
+
// `safety_mode: strict` self-certifies past all of them. `tool-safety`
|
|
198
|
+
// states "Allowlist only — tool names must match the tool registry",
|
|
199
|
+
// and `validate_tool_declarations` implemented exactly that — but
|
|
200
|
+
// nothing called it. Measured before this branch existed: a skill
|
|
201
|
+
// declaring `allowed_tools: ["NotInRegistry", "Bash(*)"]` dispatched
|
|
202
|
+
// `ready`.
|
|
203
|
+
//
|
|
204
|
+
// Scoped to the automated path deliberately. That is where
|
|
205
|
+
// `runtime-safety` puts the hard requirement, and it is the path that
|
|
206
|
+
// would execute with no human in the loop. Assisted skills get a
|
|
207
|
+
// warning instead (below): blocking them would reject the scoped-grant
|
|
208
|
+
// syntax (`Bash(scripts-run:*)`) that `tool-safety` itself prefers and
|
|
209
|
+
// that this two-entry registry does not model.
|
|
210
|
+
const toolCheck = validate_tool_declarations(skill.allowed_tools);
|
|
211
|
+
if (!toolCheck.valid) {
|
|
212
|
+
return new DispatchResult({
|
|
213
|
+
request: new ExecutionRequest({
|
|
214
|
+
skill_name: skill.name,
|
|
215
|
+
execution_type: skill.execution_type,
|
|
216
|
+
handler: skill.handler,
|
|
217
|
+
timeout_seconds: skill.timeout_seconds,
|
|
218
|
+
safety_mode: skill.safety_mode,
|
|
219
|
+
allowed_tools: skill.allowed_tools,
|
|
220
|
+
status: 'blocked',
|
|
221
|
+
reason: `Automated skill declares an untrusted tool grant — ${toolCheck.errors.join('; ')}`,
|
|
222
|
+
}),
|
|
223
|
+
warnings: [],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
} else if (skill.allowed_tools.length > 0) {
|
|
227
|
+
// Not silently ignored: an unregistered grant on a human-confirmed
|
|
228
|
+
// path is surfaced as a warning rather than a block.
|
|
229
|
+
warnings.push(...validate_tool_declarations(skill.allowed_tools).errors);
|
|
190
230
|
}
|
|
191
231
|
|
|
192
232
|
// Assisted/automated skill is ready
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"$comment": "Generated by scripts/generate_pack_manifests.py and validated on load by scripts/validate_pack_yaml.py. Schema introduced in road-to-6.0.0-D Phase 0 Step 2. `dependencies` is the artefact-level include graph (skills/rules the pack's commands reference); `requires` is the pack-level hard-dependency graph (must be acyclic — enforced by scripts/lint_pack_dependencies.py).",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
9
|
-
"required": ["id", "
|
|
9
|
+
"required": ["id", "artefact_count"],
|
|
10
10
|
"properties": {
|
|
11
11
|
"id": {
|
|
12
12
|
"type": "string",
|
|
@@ -58,10 +58,6 @@
|
|
|
58
58
|
"trust_level_default": {
|
|
59
59
|
"type": "string"
|
|
60
60
|
},
|
|
61
|
-
"version": {
|
|
62
|
-
"type": "string",
|
|
63
|
-
"minLength": 1
|
|
64
|
-
},
|
|
65
61
|
"artefact_count": {
|
|
66
62
|
"type": "integer",
|
|
67
63
|
"minimum": 0
|
|
@@ -63,14 +63,13 @@
|
|
|
63
63
|
"properties": {
|
|
64
64
|
"keyword": {"type": "string"},
|
|
65
65
|
"phrase": {"type": "string"},
|
|
66
|
-
"intent": {"type": "string"},
|
|
67
66
|
"file_pattern": {"type": "string"},
|
|
68
67
|
"path_prefix": {"type": "string", "description": "Literal path-prefix match pattern the host evaluates against the file the agent is editing — NOT a file reference. The rewriter leaves it verbatim. Source-of-truth rules that must fire on edits under `.agent-src.uncondensed/` keep that prefix here (the prefix ban applies only to `load_context:` and body links — see road-to-path-fixes.md P2.2 / AI-Council 2026-05-06)."},
|
|
69
68
|
"command": {"type": "string"},
|
|
70
69
|
"reason": {"type": "string"}
|
|
71
70
|
}
|
|
72
71
|
},
|
|
73
|
-
"description": "Router activation triggers (Phase 3 of road-to-kernel-and-router.md). Forbidden on kernel rules, required on non-kernel rules. Schema: docs/contracts/rule-router.md."
|
|
72
|
+
"description": "Router activation triggers (Phase 3 of road-to-kernel-and-router.md). Forbidden on kernel rules, required on non-kernel rules. Five match keys: keyword, phrase, file_pattern, path_prefix, command. `intent` was removed — it never auto-matched at runtime and only gave rule authors false confidence that an activation path existed; additionalProperties:false now rejects it. Schema: docs/contracts/rule-router.md."
|
|
74
73
|
},
|
|
75
74
|
"routes_to": {
|
|
76
75
|
"type": "array",
|
|
@@ -125,79 +125,21 @@ function changedLineCount(baseRef: string, files: string[], cwd: string = REPO_R
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// ── Release-PR detection (road-to-feedback-9.2.0-followups Phase 3) ────
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
for (const line of addedLines) {
|
|
144
|
-
const changelogMatch = /^##\s*\[(\d+\.\d+\.\d+)\]/.exec(line);
|
|
145
|
-
if (changelogMatch) changelogVersion = changelogMatch[1] ?? null;
|
|
146
|
-
const packageMatch = /^\s*"version"\s*:\s*"(\d+\.\d+\.\d+)"/.exec(line);
|
|
147
|
-
if (packageMatch) packageVersion = packageMatch[1] ?? null;
|
|
148
|
-
}
|
|
149
|
-
if (changelogVersion && packageVersion && changelogVersion === packageVersion) {
|
|
150
|
-
return changelogVersion;
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function releaseDetectionPatch(baseRef: string, cwd: string): string {
|
|
156
|
-
const r = spawnSync('git', ['diff', `${baseRef}...HEAD`, '--', 'CHANGELOG.md', 'package.json'], {
|
|
157
|
-
cwd,
|
|
158
|
-
encoding: 'utf8',
|
|
159
|
-
maxBuffer: 8 * 1024 * 1024,
|
|
160
|
-
});
|
|
161
|
-
return (r.stdout ?? '').toString();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Impure wrapper: collects the packaging patch via git, then detects. */
|
|
165
|
-
export function detectReleaseVersionFromGit(baseRef: string, cwd: string = REPO_ROOT): string | null {
|
|
166
|
-
return detectReleaseVersion(releaseDetectionPatch(baseRef, cwd));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** `major.minor.patch`, tolerating an optional `v` prefix; null if not semver-shaped. */
|
|
170
|
-
function parseSemver(raw: string): [number, number, number] | null {
|
|
171
|
-
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(raw.trim());
|
|
172
|
-
if (!m) return null;
|
|
173
|
-
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function semverLessThan(a: [number, number, number], b: [number, number, number]): boolean {
|
|
177
|
-
const [aMajor, aMinor, aPatch] = a;
|
|
178
|
-
const [bMajor, bMinor, bPatch] = b;
|
|
179
|
-
if (aMajor !== bMajor) return aMajor < bMajor;
|
|
180
|
-
if (aMinor !== bMinor) return aMinor < bMinor;
|
|
181
|
-
return aPatch < bPatch;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Highest semver-shaped tag strictly below `version`, or null if none. Pure
|
|
186
|
-
* over the supplied tag list — no git call. Non-semver tags (branch-backup
|
|
187
|
-
* names etc.) are ignored rather than throwing.
|
|
188
|
-
*/
|
|
189
|
-
export function pickPreviousTag(version: string, tags: readonly string[]): string | null {
|
|
190
|
-
const target = parseSemver(version);
|
|
191
|
-
if (!target) return null;
|
|
192
|
-
let best: { raw: string; parsed: [number, number, number] } | null = null;
|
|
193
|
-
for (const raw of tags) {
|
|
194
|
-
const parsed = parseSemver(raw);
|
|
195
|
-
if (!parsed) continue;
|
|
196
|
-
if (!semverLessThan(parsed, target)) continue;
|
|
197
|
-
if (!best || semverLessThan(best.parsed, parsed)) best = { raw, parsed };
|
|
198
|
-
}
|
|
199
|
-
return best ? best.raw : null;
|
|
200
|
-
}
|
|
128
|
+
//
|
|
129
|
+
// Moved to `_lib/release_scope.ts` so the release-aware content checks reuse
|
|
130
|
+
// this detector instead of re-deriving it (road-to-release-shape-honesty
|
|
131
|
+
// Phase 1). Imported for local use AND re-exported — a bare `export … from`
|
|
132
|
+
// would not bind the names in this module's scope. Public surface unchanged.
|
|
133
|
+
import {
|
|
134
|
+
detectReleaseVersionFromGit,
|
|
135
|
+
pickPreviousTag,
|
|
136
|
+
} from './_lib/release_scope.js';
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
detectReleaseVersion,
|
|
140
|
+
detectReleaseVersionFromGit,
|
|
141
|
+
pickPreviousTag,
|
|
142
|
+
} from './_lib/release_scope.js';
|
|
201
143
|
|
|
202
144
|
function listGitTags(cwd: string): string[] {
|
|
203
145
|
const r = spawnSync('git', ['tag'], { cwd, encoding: 'utf8' });
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
* 2 = errors
|
|
31
31
|
* 3 = internal error / structural malice
|
|
32
32
|
*/
|
|
33
|
-
import { spawnSync } from 'node:child_process';
|
|
34
33
|
import * as fs from 'node:fs';
|
|
35
34
|
import * as path from 'node:path';
|
|
36
35
|
import process from 'node:process';
|
|
@@ -44,6 +43,9 @@ import {
|
|
|
44
43
|
type YamlValue,
|
|
45
44
|
} from './validate_frontmatter.js';
|
|
46
45
|
import { artefact_roots, resolve_logical } from './_lib/agent_src.js';
|
|
46
|
+
import { ProbeOverflowError, runCountedProbe } from './_lib/counted_probe.js';
|
|
47
|
+
import { resolveContentLintScope } from './_lib/release_scope.js';
|
|
48
|
+
import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
|
|
47
49
|
|
|
48
50
|
// __file__ equivalent for repo-root derivation (mirrors Python
|
|
49
51
|
// `Path(__file__).resolve().parent.parent.parent` = <repo>/src/scripts → repo).
|
|
@@ -166,10 +168,10 @@ const DOUBLE_BLANK_PATTERN = /\n{3,}/;
|
|
|
166
168
|
const VALID_RULE_TYPES = new Set(['always', 'auto', 'manual']);
|
|
167
169
|
const VALID_RULE_SOURCES = new Set(['package', 'project']);
|
|
168
170
|
|
|
171
|
+
// `intent` was removed — see compile_router.ts's copy of this allowlist.
|
|
169
172
|
const ROUTER_ALLOWED_TRIGGER_KEYS = new Set([
|
|
170
173
|
'keyword',
|
|
171
174
|
'phrase',
|
|
172
|
-
'intent',
|
|
173
175
|
'file_pattern',
|
|
174
176
|
'path_prefix',
|
|
175
177
|
'command',
|
|
@@ -2428,20 +2430,58 @@ export function gather_candidate_files_under(srcRoot: string): string[] {
|
|
|
2428
2430
|
return sortedUnique(out);
|
|
2429
2431
|
}
|
|
2430
2432
|
|
|
2431
|
-
|
|
2433
|
+
/**
|
|
2434
|
+
* The scope `--changed` resolved, for the caller to report.
|
|
2435
|
+
*
|
|
2436
|
+
* Exported alongside the file list because a release PR silently widening its
|
|
2437
|
+
* own scope would be as opaque as the blindness it fixes — the caller prints
|
|
2438
|
+
* this line.
|
|
2439
|
+
*/
|
|
2440
|
+
export let last_changed_scope_reason = '';
|
|
2441
|
+
|
|
2442
|
+
/**
|
|
2443
|
+
* Whether the base-span diff (`base...HEAD`) itself ran cleanly.
|
|
2444
|
+
*
|
|
2445
|
+
* The verified-empty release outcome (see `main`) may only rest on an empty
|
|
2446
|
+
* change set that was actually COMPUTED — a base diff that errored (missing
|
|
2447
|
+
* tag object, corrupt ref) degrades to the worktree fallbacks and would
|
|
2448
|
+
* otherwise be indistinguishable from a genuinely empty span.
|
|
2449
|
+
*/
|
|
2450
|
+
export let last_changed_base_diff_ok = false;
|
|
2451
|
+
|
|
2452
|
+
/**
|
|
2453
|
+
* Candidate files changed against the resolved base.
|
|
2454
|
+
*
|
|
2455
|
+
* The base is release-aware (`_lib/release_scope.ts`): on a release PR it
|
|
2456
|
+
* widens to `previous_release_tag...HEAD`, because a release PR's own diff is
|
|
2457
|
+
* the version bump and the changelog — everything substantive landed in the
|
|
2458
|
+
* commits before the cut. Scoped to the PR's own file list this returned
|
|
2459
|
+
* nothing on a 179-commit release, and the gate reported
|
|
2460
|
+
* "0 skills checked, INCONCLUSIVE" and passed.
|
|
2461
|
+
*
|
|
2462
|
+
* `since` pins the base explicitly and wins over detection.
|
|
2463
|
+
*/
|
|
2464
|
+
export function gather_changed_candidate_files(root: string, since?: string | null): string[] {
|
|
2465
|
+
const scope = resolveContentLintScope({ cwd: root, since: since ?? null });
|
|
2466
|
+
last_changed_scope_reason = scope.reason;
|
|
2467
|
+
last_changed_base_diff_ok = false;
|
|
2432
2468
|
const diffCommands = [
|
|
2433
|
-
['git', 'diff', '--name-only',
|
|
2469
|
+
['git', 'diff', '--name-only', `${scope.base}...HEAD`],
|
|
2434
2470
|
['git', 'diff', '--name-only', '--cached', 'HEAD'],
|
|
2435
2471
|
['git', 'diff', '--name-only', 'HEAD'],
|
|
2436
2472
|
];
|
|
2437
2473
|
try {
|
|
2438
2474
|
let rawLines: string[] = [];
|
|
2475
|
+
let first = true;
|
|
2439
2476
|
for (const cmd of diffCommands) {
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2477
|
+
// A truncated diff would silently shrink the change set — the same
|
|
2478
|
+
// degrade-to-zero shape this roadmap closes elsewhere.
|
|
2479
|
+
const result = runCountedProbe(cmd[0] as string, cmd.slice(1), { cwd: root });
|
|
2480
|
+
if (first) {
|
|
2481
|
+
last_changed_base_diff_ok = result.ok;
|
|
2482
|
+
first = false;
|
|
2483
|
+
}
|
|
2484
|
+
if (result.ok && result.stdout.trim()) {
|
|
2445
2485
|
rawLines = splitlines(result.stdout);
|
|
2446
2486
|
break;
|
|
2447
2487
|
}
|
|
@@ -2479,7 +2519,15 @@ export function gather_changed_candidate_files(root: string): string[] {
|
|
|
2479
2519
|
}
|
|
2480
2520
|
}
|
|
2481
2521
|
return sortedUnique(files);
|
|
2482
|
-
} catch {
|
|
2522
|
+
} catch (exc) {
|
|
2523
|
+
// An overflowed diff is not an empty diff. Everything else (a shallow
|
|
2524
|
+
// clone with no `origin/main`, a detached worktree) legitimately has no
|
|
2525
|
+
// resolvable change set and keeps the historical empty-list degrade.
|
|
2526
|
+
if (exc instanceof ProbeOverflowError) {
|
|
2527
|
+
throw exc;
|
|
2528
|
+
}
|
|
2529
|
+
last_changed_scope_reason =
|
|
2530
|
+
`change set unresolvable (${exc instanceof Error ? exc.message : String(exc)})`;
|
|
2483
2531
|
return [];
|
|
2484
2532
|
}
|
|
2485
2533
|
}
|
|
@@ -4082,6 +4130,8 @@ interface Args {
|
|
|
4082
4130
|
report: boolean;
|
|
4083
4131
|
repoRoot: string;
|
|
4084
4132
|
quiet: boolean;
|
|
4133
|
+
/** Explicit diff base for `--changed`; wins over release detection. */
|
|
4134
|
+
since: string | null;
|
|
4085
4135
|
}
|
|
4086
4136
|
|
|
4087
4137
|
export function parse_args(argv: string[]): Args {
|
|
@@ -4095,6 +4145,7 @@ export function parse_args(argv: string[]): Args {
|
|
|
4095
4145
|
condensationQuality: false,
|
|
4096
4146
|
strictWarnings: false,
|
|
4097
4147
|
report: false,
|
|
4148
|
+
since: null,
|
|
4098
4149
|
repoRoot: '.',
|
|
4099
4150
|
quiet: false,
|
|
4100
4151
|
};
|
|
@@ -4104,6 +4155,15 @@ export function parse_args(argv: string[]): Args {
|
|
|
4104
4155
|
args.all = true;
|
|
4105
4156
|
} else if (a === '--changed') {
|
|
4106
4157
|
args.changed = true;
|
|
4158
|
+
} else if (a === '--since') {
|
|
4159
|
+
const v = argv[i + 1];
|
|
4160
|
+
i += 1;
|
|
4161
|
+
if (v === undefined || v.startsWith('--')) {
|
|
4162
|
+
throw new ArgError('argument --since: expected a git ref');
|
|
4163
|
+
}
|
|
4164
|
+
args.since = v;
|
|
4165
|
+
} else if (a.startsWith('--since=')) {
|
|
4166
|
+
args.since = a.slice('--since='.length);
|
|
4107
4167
|
} else if (a === '--format') {
|
|
4108
4168
|
const v = argv[i + 1];
|
|
4109
4169
|
i += 1;
|
|
@@ -4143,6 +4203,58 @@ export function parse_args(argv: string[]): Args {
|
|
|
4143
4203
|
|
|
4144
4204
|
class ArgError extends Error {}
|
|
4145
4205
|
|
|
4206
|
+
/**
|
|
4207
|
+
* True when an empty change set is a defect rather than a legitimate scope.
|
|
4208
|
+
*
|
|
4209
|
+
* Only on a release PR whose scope actually widened to a tag span: a release
|
|
4210
|
+
* that changed no skill and no rule is possible in principle, but a *resolved*
|
|
4211
|
+
* span that matched nothing is the "0 skills checked, INCONCLUSIVE" shape this
|
|
4212
|
+
* gate exists to stop. A release whose previous tag could not be resolved is
|
|
4213
|
+
* excluded — that scope never widened, so failing it would punish a shallow
|
|
4214
|
+
* clone for the wrong reason.
|
|
4215
|
+
*/
|
|
4216
|
+
function _release_scope_requires_findings(root: string, since?: string | null): boolean {
|
|
4217
|
+
const scope = resolveContentLintScope({ cwd: root, since: since ?? null });
|
|
4218
|
+
return scope.isRelease && scope.previousTag !== null;
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
/**
|
|
4222
|
+
* True when the `--changed` corpus filter still matches tracked files at HEAD.
|
|
4223
|
+
*
|
|
4224
|
+
* The discriminator between a DEAD scope and a VERIFIED-EMPTY release span.
|
|
4225
|
+
* A release that touches no skill/rule file is a legitimate outcome — but only
|
|
4226
|
+
* provably so when the exact predicate `gather_changed_candidate_files`
|
|
4227
|
+
* applies to diff lines demonstrably still sees the corpus. Probing
|
|
4228
|
+
* `git ls-files` with that same predicate means a moved projection root
|
|
4229
|
+
* (the ADR-051 blindness class) fails this probe exactly as it would blind
|
|
4230
|
+
* the diff — so "empty because the root moved" cannot masquerade as
|
|
4231
|
+
* "empty because nothing changed".
|
|
4232
|
+
*/
|
|
4233
|
+
function _changed_filter_matches_head(root: string): boolean {
|
|
4234
|
+
// Probes the canonical projection root only (ADR-051): a corpus that
|
|
4235
|
+
// exists solely under a legacy path counts as dead, not alive.
|
|
4236
|
+
const result = runCountedProbe('git', ['ls-files', '--', 'dist/agent-src'], { cwd: root });
|
|
4237
|
+
if (!result.ok) {
|
|
4238
|
+
return false;
|
|
4239
|
+
}
|
|
4240
|
+
for (const raw of splitlines(result.stdout)) {
|
|
4241
|
+
const norm = raw.trim().replace(/\\/g, '/');
|
|
4242
|
+
if (!norm || norm.includes('/evals/')) {
|
|
4243
|
+
continue;
|
|
4244
|
+
}
|
|
4245
|
+
if (!norm.startsWith('dist/agent-src/') && !norm.includes('/dist/agent-src/')) {
|
|
4246
|
+
continue;
|
|
4247
|
+
}
|
|
4248
|
+
if (
|
|
4249
|
+
norm.endsWith('/SKILL.md') ||
|
|
4250
|
+
(norm.endsWith('.md') && (norm.includes('/rules/') || norm.includes('/commands/')))
|
|
4251
|
+
) {
|
|
4252
|
+
return true;
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
return false;
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4146
4258
|
export function main(argv: string[]): number {
|
|
4147
4259
|
let args: Args;
|
|
4148
4260
|
try {
|
|
@@ -4162,7 +4274,8 @@ export function main(argv: string[]): number {
|
|
|
4162
4274
|
paths.push(...gather_all_candidate_files(root));
|
|
4163
4275
|
}
|
|
4164
4276
|
if (args.changed) {
|
|
4165
|
-
paths.push(...gather_changed_candidate_files(root));
|
|
4277
|
+
paths.push(...gather_changed_candidate_files(root, args.since));
|
|
4278
|
+
process.stderr.write(`skill_linter --changed: ${last_changed_scope_reason}\n`);
|
|
4166
4279
|
}
|
|
4167
4280
|
for (const raw of args.paths) {
|
|
4168
4281
|
const p = path.isAbsolute(raw) ? raw : path.resolve(root, raw);
|
|
@@ -4183,6 +4296,42 @@ export function main(argv: string[]): number {
|
|
|
4183
4296
|
} else if (args.format === 'json') {
|
|
4184
4297
|
process.stdout.write(`${format_json([])}\n`);
|
|
4185
4298
|
}
|
|
4299
|
+
// A release PR that resolved a real tag span and still matched
|
|
4300
|
+
// nothing needs a discriminator: DEAD scope (the diff errored, or
|
|
4301
|
+
// the corpus filter no longer matches anything at HEAD — the
|
|
4302
|
+
// ADR-051 moved-root blindness) fails; VERIFIED-EMPTY (the base
|
|
4303
|
+
// diff ran cleanly AND the filter provably still sees tracked
|
|
4304
|
+
// corpus files) is a legitimate release that simply changed no
|
|
4305
|
+
// skill/rule — failing it would red every scripts-/docs-only
|
|
4306
|
+
// release forever. Mirrors the scan-scope regime
|
|
4307
|
+
// (`_lib/scan_scope.ts`; exit 2, as `check_iron_law_prominence`).
|
|
4308
|
+
// Ordinary PRs keep the historical exit 0: a change set with no
|
|
4309
|
+
// skill or rule in it is a legitimate empty scope, not a dead one.
|
|
4310
|
+
if (args.changed && _release_scope_requires_findings(root, args.since)) {
|
|
4311
|
+
if (last_changed_base_diff_ok && _changed_filter_matches_head(root)) {
|
|
4312
|
+
process.stderr.write(
|
|
4313
|
+
'✅ verified-empty release scope: the base-span diff ran cleanly and ' +
|
|
4314
|
+
'the corpus filter still matches tracked files at HEAD — this ' +
|
|
4315
|
+
'release genuinely changes no skill/rule file. The gate is alive, ' +
|
|
4316
|
+
'not blind.\n',
|
|
4317
|
+
);
|
|
4318
|
+
} else {
|
|
4319
|
+
try {
|
|
4320
|
+
assertScanned({
|
|
4321
|
+
gate: 'skill_linter --changed (release PR)',
|
|
4322
|
+
scanned: 0,
|
|
4323
|
+
units: 'skill/rule files',
|
|
4324
|
+
roots: [last_changed_scope_reason],
|
|
4325
|
+
});
|
|
4326
|
+
} catch (exc) {
|
|
4327
|
+
if (exc instanceof DeadScopeError) {
|
|
4328
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
4329
|
+
return 2;
|
|
4330
|
+
}
|
|
4331
|
+
throw exc;
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4186
4335
|
process.stderr.write('No matching skill/rule files found.\n');
|
|
4187
4336
|
return 0;
|
|
4188
4337
|
}
|