@event4u/agent-config 9.11.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 -204
- package/README.md +8 -1
- package/dist/agent-src/commands/fix/ci.md +29 -0
- package/dist/agent-src/commands/pr/create.md +60 -0
- package/dist/agent-src/contexts/authority/kernel-rule-edits.md +18 -2
- 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 +30 -1
- 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/ai-code-blindspots/SKILL.md +1 -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-intelligence/SKILL.md +27 -1
- package/dist/agent-src/skills/design-intelligence/data/font-pairings-reference.csv +74 -74
- package/dist/agent-src/skills/design-intelligence/data/manifest.json +2 -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/design-tokens/scripts/tokens.ts +0 -7
- 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/typography-system/SKILL.md +55 -11
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +180 -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 +162 -17
- 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 +49 -6
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
- package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
- package/dist/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
- package/dist/cli-delegate/{chunk-IR7P7JWA.js → chunk-A3HJ6NTI.js} +271 -59
- package/dist/cli-delegate/chunk-EJYIGZ3T.js +125 -0
- package/dist/cli-delegate/{chunk-RDGRHAAB.js → chunk-HYWSRNUX.js} +6 -86
- package/dist/cli-delegate/{chunk-YMFO6FDN.js → chunk-QFKQLQ7X.js} +313 -81
- package/dist/cli-delegate/{chunk-4SV3XGFA.js → chunk-TEGJU2JX.js} +7 -9
- package/dist/cli-delegate/chunk-TJIAM5FW.js +133 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -7
- package/dist/cli-delegate/cmd_doctor.js +3 -5
- package/dist/cli-delegate/cmd_explain.js +153 -85
- package/dist/cli-delegate/cmd_export.js +5 -3
- package/dist/cli-delegate/cmd_fleet.js +2 -1
- package/dist/cli-delegate/cmd_preflight.js +6 -123
- package/dist/cli-delegate/cmd_prune.js +5 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +5 -3
- package/dist/cli-delegate/cmd_sync.js +5 -3
- package/dist/cli-delegate/cmd_uninstall.js +6 -5
- package/dist/cli-delegate/cmd_validate.js +5 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +116 -72
- 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 +7 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +174 -13
- package/dist/install/install.mjs +605 -412
- package/dist/install/ruleInScope.js +123 -0
- package/dist/install/ruleInScope.js.map +1 -0
- package/dist/install/rule_scope.js +2 -2
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/install/wizard-plan.js +10 -1
- package/dist/install/wizard-plan.js.map +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +710 -2491
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +8 -24
- package/dist/scripts/condense.js.map +1 -1
- 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 +58 -0
- package/llms.txt +63 -27
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +162 -17
- 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 +49 -6
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
- package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
- package/src/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/src/config/agent-settings.template.yml +8 -0
- package/src/install/ruleInScope.ts +133 -0
- package/src/install/rule_scope.ts +2 -2
- package/src/install/wizard-plan.ts +11 -1
- package/src/scripts/_lib/counted_probe.ts +142 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +38 -2
- 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_installer_import_purity.ts +295 -0
- 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 +9 -30
- 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-hooks.sh +36 -4
- package/src/scripts/install.ts +210 -5
- 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/lint_ui_stack_bundles.ts +31 -0
- 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
- package/dist/cli-delegate/chunk-7DSAMZFU.js +0 -356
- package/dist/cli-delegate/chunk-BJGXZNPM.js +0 -266
- package/dist/cli-delegate/chunk-TFG57JJM.js +0 -1969
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
* Usage:
|
|
24
24
|
* ./scripts-run src/scripts/lint_agent_security [--sarif artifacts/agent-security.sarif]
|
|
25
25
|
*/
|
|
26
|
-
import { spawnSync } from "node:child_process";
|
|
27
26
|
import fs from "node:fs";
|
|
28
27
|
import path from "node:path";
|
|
29
28
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
29
|
+
import { runCountedProbe } from "./_lib/counted_probe.js";
|
|
30
30
|
|
|
31
31
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
32
32
|
|
|
@@ -54,9 +54,10 @@ function _run(script: string): [number, Finding[]] {
|
|
|
54
54
|
// (capture_output=True, text=True equivalent). Mirrors the retired Python implementation's
|
|
55
55
|
// `subprocess.run([sys.executable, HERE/<child>.py, "--json"])`, now that the
|
|
56
56
|
// children are TypeScript and the `.py` originals are deleted.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
// Bounded read: a truncated `--json` payload would fail to parse and be
|
|
58
|
+
// swallowed as zero findings — a security aggregate reporting clean because
|
|
59
|
+
// it lost the answer. `runCountedProbe` throws on overflow instead.
|
|
60
|
+
const proc = runCountedProbe(tsxBin, [path.join(_HERE, script), "--json"]);
|
|
60
61
|
let findings: Finding[];
|
|
61
62
|
try {
|
|
62
63
|
const parsed: unknown = JSON.parse((proc.stdout ?? "") || "[]");
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
{
|
|
94
94
|
"file": "src/skills/supply-chain-intake/SKILL.md",
|
|
95
95
|
"lines": [
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
80,
|
|
97
|
+
82
|
|
98
98
|
],
|
|
99
99
|
"reason": "Per-ecosystem check commands in fenced blocks; the pip/composer siblings sit farther apart than the ±10-line window — genuine multi-ecosystem documentation."
|
|
100
100
|
},
|
|
@@ -64,6 +64,65 @@ const STATUS_ORDER: Record<string, number> = { pass: 0, pass_with_warnings: 1, f
|
|
|
64
64
|
* baseline worktree-add failure (→ exit 2 in main). */
|
|
65
65
|
class CalledProcessError extends Error {}
|
|
66
66
|
|
|
67
|
+
/** The baseline ran but produced nothing usable (→ exit 2 in main).
|
|
68
|
+
*
|
|
69
|
+
* Distinct from the two DOCUMENTED empty-baseline degradations (no linter file
|
|
70
|
+
* in the ref; a pre-migration `.py` baseline with no python3). Those are
|
|
71
|
+
* deliberate. This one is a broken run, and returning an empty baseline for it
|
|
72
|
+
* silently reclassifies every existing finding as a "new file" — the gate then
|
|
73
|
+
* fires on an unmodified tree and can never report a real regression, because a
|
|
74
|
+
* regression needs the file present in both maps. */
|
|
75
|
+
class BaselineCollectionError extends Error {}
|
|
76
|
+
|
|
77
|
+
/** Give the detached baseline worktree a resolvable module context.
|
|
78
|
+
*
|
|
79
|
+
* `git worktree add` checks out tracked files only, so the temp tree has no
|
|
80
|
+
* `node_modules` and the linter dies on its first bare import. Symlinking the
|
|
81
|
+
* repo's own install is enough — the baseline runs the ref's linter source
|
|
82
|
+
* against the ref's artefacts; only the dependency resolution is borrowed. */
|
|
83
|
+
function _link_node_modules(tmpdir: string, repo_root: string): void {
|
|
84
|
+
const target = path.join(repo_root, 'node_modules');
|
|
85
|
+
const link = path.join(tmpdir, 'node_modules');
|
|
86
|
+
if (fs.existsSync(link) || !fs.existsSync(target)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
fs.symlinkSync(target, link, 'dir');
|
|
91
|
+
} catch {
|
|
92
|
+
// Non-fatal here: a failed link surfaces as a failed baseline run
|
|
93
|
+
// below, which raises with the real cause rather than degrading.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Re-root the baseline's file keys onto the repo-relative paths the working-tree
|
|
98
|
+
* run emits.
|
|
99
|
+
*
|
|
100
|
+
* The baseline runs with `--repo-root <tmpdir>` and reports absolute paths under
|
|
101
|
+
* that temp worktree; the working-tree run reports paths relative to the repo.
|
|
102
|
+
* Left as-is the two maps share no key, so every comparison is vacuous — the
|
|
103
|
+
* disjoint guard would reject the run outright once the baseline is non-empty. */
|
|
104
|
+
function _relativise(data: LinterJson, tmpdir: string): LinterJson {
|
|
105
|
+
const roots = new Set<string>([tmpdir]);
|
|
106
|
+
try {
|
|
107
|
+
roots.add(fs.realpathSync(tmpdir));
|
|
108
|
+
} catch {
|
|
109
|
+
/* the temp dir is about to be removed; the literal prefix still applies */
|
|
110
|
+
}
|
|
111
|
+
const strip = (f: string): string => {
|
|
112
|
+
for (const r of roots) {
|
|
113
|
+
const prefix = r.endsWith(path.sep) ? r : r + path.sep;
|
|
114
|
+
if (f.startsWith(prefix)) {
|
|
115
|
+
return f.slice(prefix.length);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return f;
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
...data,
|
|
122
|
+
results: (data.results ?? []).map((e) => ({ ...e, file: strip(e.file) })),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
67
126
|
/** Run the linter and return parsed JSON. If ref is null, run on working tree.
|
|
68
127
|
*
|
|
69
128
|
* Mirrors `run_linter_json`: for a ref, add a detached temp worktree, run the
|
|
@@ -91,6 +150,7 @@ function run_linter_json(ref: string | null, repo_root: string): LinterJson {
|
|
|
91
150
|
);
|
|
92
151
|
}
|
|
93
152
|
try {
|
|
153
|
+
_link_node_modules(tmpdir, repo_root);
|
|
94
154
|
// The baseline ref may be pre-migration (`.py`, run via python3) or
|
|
95
155
|
// post-migration (`.ts`, run via tsx). Probe both extensions in both
|
|
96
156
|
// locations and spawn by extension. A historical `.py` ref genuinely
|
|
@@ -139,7 +199,17 @@ function run_linter_json(ref: string | null, repo_root: string): LinterJson {
|
|
|
139
199
|
encoding: 'utf8',
|
|
140
200
|
});
|
|
141
201
|
const out = result.stdout ?? '';
|
|
142
|
-
|
|
202
|
+
if (!out.trim()) {
|
|
203
|
+
const why =
|
|
204
|
+
result.status === 0
|
|
205
|
+
? 'it exited 0 but printed nothing'
|
|
206
|
+
: `it exited ${result.status ?? 'null'}`;
|
|
207
|
+
throw new BaselineCollectionError(
|
|
208
|
+
`the baseline linter for '${ref}' produced no output — ${why}.\n` +
|
|
209
|
+
`stderr: ${(result.stderr ?? '').trim().split('\n').slice(-6).join('\n')}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return _relativise(JSON.parse(out) as LinterJson, tmpdir);
|
|
143
213
|
} finally {
|
|
144
214
|
spawnSync('git', ['-C', repo_root, 'worktree', 'remove', '--force', tmpdir], {
|
|
145
215
|
encoding: 'utf8',
|
|
@@ -433,6 +503,10 @@ function main(): number {
|
|
|
433
503
|
);
|
|
434
504
|
return 2;
|
|
435
505
|
}
|
|
506
|
+
if (e instanceof BaselineCollectionError) {
|
|
507
|
+
process.stderr.write(`Error: ${e.message}\n`);
|
|
508
|
+
return 2;
|
|
509
|
+
}
|
|
436
510
|
throw e;
|
|
437
511
|
}
|
|
438
512
|
|
|
@@ -454,6 +528,22 @@ function main(): number {
|
|
|
454
528
|
return 2;
|
|
455
529
|
}
|
|
456
530
|
|
|
531
|
+
// The same guard's blind spot: an EMPTY baseline against a populated
|
|
532
|
+
// current tree is not comparable either, and it is the worse failure —
|
|
533
|
+
// every existing finding is then reported as a "new file", so the gate
|
|
534
|
+
// fires on an unmodified tree and no real regression can ever surface.
|
|
535
|
+
// The documented empty-baseline degradations (no linter in the ref; a
|
|
536
|
+
// pre-migration `.py` baseline with no python3) print their own warning
|
|
537
|
+
// above and land here on purpose.
|
|
538
|
+
if (!baseKeys.length && currKeys.length) {
|
|
539
|
+
process.stderr.write(
|
|
540
|
+
`Error: the baseline '${args.baseline}' produced no lint results while the ` +
|
|
541
|
+
`current tree produced ${currKeys.length} — runs are not comparable; ` +
|
|
542
|
+
'refusing to report. Every finding would be misreported as new.\n',
|
|
543
|
+
);
|
|
544
|
+
return 2;
|
|
545
|
+
}
|
|
546
|
+
|
|
457
547
|
const delta = compare(baseline_map, current_map);
|
|
458
548
|
|
|
459
549
|
if (args.format === 'json') {
|
|
@@ -528,4 +618,5 @@ export {
|
|
|
528
618
|
format_text,
|
|
529
619
|
format_markdown,
|
|
530
620
|
main,
|
|
621
|
+
_relativise,
|
|
531
622
|
};
|
|
@@ -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
|
}
|
|
@@ -31,6 +31,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
31
31
|
import {
|
|
32
32
|
STACK_BUNDLES,
|
|
33
33
|
type StackBundle,
|
|
34
|
+
compose_bundle,
|
|
34
35
|
} from '../agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.js';
|
|
35
36
|
|
|
36
37
|
const QUIET = process.argv.includes('--quiet');
|
|
@@ -47,6 +48,29 @@ const STACK_NEUTRAL_PACKS: ReadonlySet<string> = new Set(['engineering-base', 'c
|
|
|
47
48
|
|
|
48
49
|
const findings: string[] = [];
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Axis combinations the composition must satisfy.
|
|
53
|
+
*
|
|
54
|
+
* Not exhaustive over the cross-product — that would be thousands of
|
|
55
|
+
* meaningless tuples. These are the shapes a real project produces, including
|
|
56
|
+
* the ones no hand-written lane ever covered (Nuxt over Vue, Astro, Angular,
|
|
57
|
+
* Livewire without Flux).
|
|
58
|
+
*/
|
|
59
|
+
const _AXIS_COMBOS: ReadonlyArray<Record<string, string>> = [
|
|
60
|
+
{ view: 'blade', reactivity: 'livewire', component_lib: 'flux', css: 'tailwind', meta: 'none' },
|
|
61
|
+
{ view: 'blade', reactivity: 'livewire', component_lib: 'none', css: 'tailwind', meta: 'none' },
|
|
62
|
+
{ view: 'blade', reactivity: 'alpine', component_lib: 'none', css: 'tailwind', meta: 'none' },
|
|
63
|
+
{ view: 'blade', reactivity: 'unknown', component_lib: 'none', css: 'none', meta: 'filament' },
|
|
64
|
+
{ view: 'jsx', reactivity: 'react', component_lib: 'radix', css: 'tailwind', meta: 'none' },
|
|
65
|
+
{ view: 'jsx', reactivity: 'react', component_lib: 'none', css: 'tailwind', meta: 'nextjs' },
|
|
66
|
+
{ view: 'vue-sfc', reactivity: 'vue', component_lib: 'none', css: 'none', meta: 'nuxt' },
|
|
67
|
+
{ view: 'svelte-sfc', reactivity: 'svelte', component_lib: 'none', css: 'none', meta: 'none' },
|
|
68
|
+
{ view: 'astro', reactivity: 'unknown', component_lib: 'none', css: 'none', meta: 'astro' },
|
|
69
|
+
{ view: 'angular-html', reactivity: 'angular', component_lib: 'none', css: 'none', meta: 'none' },
|
|
70
|
+
{ view: 'unknown', reactivity: 'htmx', component_lib: 'none', css: 'none', meta: 'none' },
|
|
71
|
+
{ view: 'none', reactivity: 'none', component_lib: 'none', css: 'tailwind', meta: 'none' },
|
|
72
|
+
];
|
|
73
|
+
|
|
50
74
|
function skill_path(name: string): string {
|
|
51
75
|
return path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
52
76
|
}
|
|
@@ -137,6 +161,13 @@ function main(): number {
|
|
|
137
161
|
check_lane(lane, STACK_BUNDLES[lane] as StackBundle);
|
|
138
162
|
}
|
|
139
163
|
|
|
164
|
+
// The composed shape, not just the hand-written lanes. Dispatch derives a
|
|
165
|
+
// bundle from axes now, so a combination nobody wrote down by hand can
|
|
166
|
+
// still be produced — and it has to satisfy the same two properties.
|
|
167
|
+
for (const combo of _AXIS_COMBOS) {
|
|
168
|
+
check_lane(`composed(${JSON.stringify(combo)})`, compose_bundle(combo));
|
|
169
|
+
}
|
|
170
|
+
|
|
140
171
|
// A lane usable as a fallback must be pack-agnostic, else the fallback
|
|
141
172
|
// itself is the thing that fails for an unpredictable consumer.
|
|
142
173
|
const plain = STACK_BUNDLES['plain'];
|
|
@@ -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
|