@event4u/agent-config 9.12.0 → 9.13.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 +80 -256
- package/README.md +1 -1
- 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/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +89 -67
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +174 -13
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +750 -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/overbuild_lens_contract.ts +196 -0
- package/src/scripts/_lib/release_scope.ts +207 -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_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/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/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 +93 -11
- package/src/scripts/sweep_dead_scan_roots.ts +550 -0
- package/src/scripts/trigger_coverage.ts +11 -37
|
@@ -18,11 +18,32 @@
|
|
|
18
18
|
* (em-dash / colon / comma list), no quoted example,
|
|
19
19
|
* no imperative-verb lead. A bare topic restatement.
|
|
20
20
|
*
|
|
21
|
-
* (d)
|
|
21
|
+
* (a)–(d) are signal-ABSENCE checks over every skill: an imperative house-style
|
|
22
22
|
* description ("Writes Laravel PHP — Eloquent, Artisan …") carries routing
|
|
23
23
|
* signal and passes. The farm-generator's name-echo frontmatter is the
|
|
24
24
|
* must-fail specimen.
|
|
25
25
|
*
|
|
26
|
+
* Two further checks are POSITIVE — they demand something be present — and are
|
|
27
|
+
* therefore scoped to CLUSTERED skills only (road-to-overlap-truth-and-skill-cut
|
|
28
|
+
* Phase 5):
|
|
29
|
+
*
|
|
30
|
+
* (e) clustered-no-sibling-routing — the canonical overlap tool pairs this
|
|
31
|
+
* skill with a same-pack sibling at >= the canonical
|
|
32
|
+
* threshold, and the description never names that
|
|
33
|
+
* sibling. Two skills the router cannot tell apart,
|
|
34
|
+
* neither of which says which one loses.
|
|
35
|
+
* (f) clustered-no-quoted-phrase — same cluster condition, and the description
|
|
36
|
+
* carries no quoted literal user phrase. In a cluster,
|
|
37
|
+
* the user's own words are the only tie-break the
|
|
38
|
+
* router gets.
|
|
39
|
+
*
|
|
40
|
+
* The cluster condition is what keeps these honest. A quoted-phrase mandate
|
|
41
|
+
* across all ~287 skills is noise, not routing — it would fire on skills with no
|
|
42
|
+
* neighbour to be confused with. Today the cluster set is EMPTY (the corpus has
|
|
43
|
+
* no same-pack pair at or above threshold), so (e) and (f) are dormant by
|
|
44
|
+
* construction and wake up exactly when a new skill lands close to an existing
|
|
45
|
+
* one. That is the point.
|
|
46
|
+
*
|
|
26
47
|
* Deterministic, static-config only. Allowlist mirrors lint_skill_originality:
|
|
27
48
|
* `lint_skill_descriptions_allowlist.json`, capped at 20 (autonomous-execution
|
|
28
49
|
* allowlist-growth antipattern — over-cap means the linter is wrong).
|
|
@@ -39,6 +60,7 @@ import * as path from 'node:path';
|
|
|
39
60
|
import process from 'node:process';
|
|
40
61
|
import { fileURLToPath } from 'node:url';
|
|
41
62
|
|
|
63
|
+
import { OVERLAP_THRESHOLD, _cosine, collect } from './audit_skill_overlap.js';
|
|
42
64
|
import { parse_frontmatter } from './skill_overlap.js';
|
|
43
65
|
|
|
44
66
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -57,10 +79,54 @@ const IMPERATIVE_LEAD_RE =
|
|
|
57
79
|
|
|
58
80
|
interface Violation {
|
|
59
81
|
slug: string;
|
|
60
|
-
code:
|
|
82
|
+
code:
|
|
83
|
+
| 'desc-equals-name'
|
|
84
|
+
| 'duplicated-trigger'
|
|
85
|
+
| 'triggers-are-name'
|
|
86
|
+
| 'no-routing-signal'
|
|
87
|
+
| 'clustered-no-sibling-routing'
|
|
88
|
+
| 'clustered-no-quoted-phrase';
|
|
61
89
|
detail: string;
|
|
62
90
|
}
|
|
63
91
|
|
|
92
|
+
/** A quoted literal phrase — `'expand this scene'` / `"film-grade scene"`. */
|
|
93
|
+
const QUOTED_PHRASE_RE = /['"`][^'"`]{3,}['"`]/;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Same-pack neighbours at or above the canonical overlap threshold, keyed by
|
|
97
|
+
* directory slug.
|
|
98
|
+
*
|
|
99
|
+
* Uses the canonical instrument, not a second implementation — one metric, one
|
|
100
|
+
* threshold, one answer. Cross-pack pairs are excluded for the same reason a
|
|
101
|
+
* cross-pack merge is out of scope: they do not compete for the same install.
|
|
102
|
+
*/
|
|
103
|
+
function computeClusters(root: string, threshold: number = OVERLAP_THRESHOLD): Map<string, string[]> {
|
|
104
|
+
const clusters = new Map<string, string[]>();
|
|
105
|
+
const skills = collect(root).map((s) => ({
|
|
106
|
+
slug: path.basename(path.dirname(s.relpath)),
|
|
107
|
+
packs: s.packs,
|
|
108
|
+
vector: s.vector,
|
|
109
|
+
}));
|
|
110
|
+
for (let i = 0; i < skills.length; i += 1) {
|
|
111
|
+
for (let j = i + 1; j < skills.length; j += 1) {
|
|
112
|
+
const a = skills[i]!;
|
|
113
|
+
const b = skills[j]!;
|
|
114
|
+
let sharesPack = false;
|
|
115
|
+
for (const p of a.packs) {
|
|
116
|
+
if (b.packs.has(p)) {
|
|
117
|
+
sharesPack = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!sharesPack) continue;
|
|
122
|
+
if (_cosine(a.vector, b.vector) < threshold) continue;
|
|
123
|
+
clusters.set(a.slug, [...(clusters.get(a.slug) ?? []), b.slug]);
|
|
124
|
+
clusters.set(b.slug, [...(clusters.get(b.slug) ?? []), a.slug]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return clusters;
|
|
128
|
+
}
|
|
129
|
+
|
|
64
130
|
/** A description carries routing signal if it names a condition, leads with an
|
|
65
131
|
* imperative verb, enumerates specifics (em-dash / colon / ≥2 comma segments),
|
|
66
132
|
* or embeds an example query (a quoted phrase). Absence of all four is the defect. */
|
|
@@ -89,7 +155,7 @@ function triggerPhrases(desc: string): string[] {
|
|
|
89
155
|
.filter((p) => p.length >= 3);
|
|
90
156
|
}
|
|
91
157
|
|
|
92
|
-
function analyseSkill(slug: string, fm: Record<string, string
|
|
158
|
+
function analyseSkill(slug: string, fm: Record<string, string>, siblings: readonly string[] = []): Violation[] {
|
|
93
159
|
const out: Violation[] = [];
|
|
94
160
|
const name = fm['name'] ?? slug;
|
|
95
161
|
const desc = (fm['description'] ?? '').trim();
|
|
@@ -137,6 +203,25 @@ function analyseSkill(slug: string, fm: Record<string, string>): Violation[] {
|
|
|
137
203
|
});
|
|
138
204
|
}
|
|
139
205
|
|
|
206
|
+
// (e) + (f) fire ONLY for skills the canonical tool put in a cluster.
|
|
207
|
+
if (siblings.length > 0) {
|
|
208
|
+
const unrouted = siblings.filter((s) => !desc.includes(s));
|
|
209
|
+
if (unrouted.length > 0) {
|
|
210
|
+
out.push({
|
|
211
|
+
slug,
|
|
212
|
+
code: 'clustered-no-sibling-routing',
|
|
213
|
+
detail: `overlaps ${unrouted.map((s) => `\`${s}\``).join(', ')} at or above the canonical threshold, but the description never names ${unrouted.length === 1 ? 'it' : 'them'} — add a sibling-routing clause so the router knows which one loses`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (!QUOTED_PHRASE_RE.test(desc)) {
|
|
217
|
+
out.push({
|
|
218
|
+
slug,
|
|
219
|
+
code: 'clustered-no-quoted-phrase',
|
|
220
|
+
detail: `clustered with ${siblings.map((s) => `\`${s}\``).join(', ')} but carries no quoted literal user phrase — in a cluster the user's own words are the tie-break`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
140
225
|
return out;
|
|
141
226
|
}
|
|
142
227
|
|
|
@@ -192,13 +277,14 @@ function parseArgs(argv: string[]): Args {
|
|
|
192
277
|
function main(argv: string[]): number {
|
|
193
278
|
const args = parseArgs(argv);
|
|
194
279
|
const allow = loadAllowlist();
|
|
280
|
+
const clusters = computeClusters(args.root);
|
|
195
281
|
const violations: Violation[] = [];
|
|
196
282
|
let scanned = 0;
|
|
197
283
|
for (const md of skillMds(args.root)) {
|
|
198
284
|
scanned += 1;
|
|
199
285
|
const slug = path.basename(path.dirname(md));
|
|
200
286
|
const [fm] = parse_frontmatter(fs.readFileSync(md, 'utf-8'));
|
|
201
|
-
for (const v of analyseSkill(slug, fm)) {
|
|
287
|
+
for (const v of analyseSkill(slug, fm, clusters.get(slug) ?? [])) {
|
|
202
288
|
if (!allow.has(`${v.slug}::${v.code}`)) violations.push(v);
|
|
203
289
|
}
|
|
204
290
|
}
|
|
@@ -216,7 +302,9 @@ function main(argv: string[]): number {
|
|
|
216
302
|
return 1;
|
|
217
303
|
}
|
|
218
304
|
if (!args.quiet) {
|
|
219
|
-
process.stdout.write(
|
|
305
|
+
process.stdout.write(
|
|
306
|
+
`✅ lint_skill_descriptions: ${scanned} skill(s) scanned (${clusters.size} clustered), no description defects.\n`,
|
|
307
|
+
);
|
|
220
308
|
}
|
|
221
309
|
return 0;
|
|
222
310
|
}
|
|
@@ -155,8 +155,8 @@ const _TRIGGER_HINT_LIMIT = 6;
|
|
|
155
155
|
/** A short, comma-joined sample of the rule's trigger keywords/phrases. */
|
|
156
156
|
function _trigger_hint(fm: string): string {
|
|
157
157
|
const hits: string[] = [];
|
|
158
|
-
// re.finditer(r'^\s*-\s*(?:keyword|phrase
|
|
159
|
-
const re = /^\s*-\s*(?:keyword|phrase
|
|
158
|
+
// re.finditer(r'^\s*-\s*(?:keyword|phrase):\s*"?(.+?)"?\s*$', fm, re.MULTILINE)
|
|
159
|
+
const re = /^\s*-\s*(?:keyword|phrase):\s*"?(.+?)"?\s*$/gm;
|
|
160
160
|
let m: RegExpExecArray | null;
|
|
161
161
|
while ((m = re.exec(fm)) !== null) {
|
|
162
162
|
hits.push((m[1] as string).trim());
|
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' });
|