@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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release-aware diff scope for content checks.
|
|
3
|
+
*
|
|
4
|
+
* A release PR's own diff is version, changelog, pack metadata and marketplace
|
|
5
|
+
* files: the substantive work arrived in the commits *before* the cut. Any
|
|
6
|
+
* check scoped to "files changed in this PR" therefore measures the release
|
|
7
|
+
* wrapper, not the release. The visible consequence was a 179-commit release
|
|
8
|
+
* whose skill lint reported **"0 skills checked, INCONCLUSIVE"** and merged.
|
|
9
|
+
*
|
|
10
|
+
* The fix is a range, not a new gate: on a release PR the content checks
|
|
11
|
+
* resolve their scope to `previous_release_tag...HEAD`. Everywhere else the
|
|
12
|
+
* base is unchanged.
|
|
13
|
+
*
|
|
14
|
+
* The three release-detection primitives below were written for
|
|
15
|
+
* `self_review_gate.ts`, which already switched its own analysis range this
|
|
16
|
+
* way. They live here so the second consumer reuses the precedent instead of
|
|
17
|
+
* re-deriving it — two copies of "is this a release PR?" is exactly the drift
|
|
18
|
+
* this package has paid for before. `self_review_gate.ts` re-exports them, so
|
|
19
|
+
* its public surface is unchanged.
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A release PR is detected from its own packaging diff (baseRef...HEAD,
|
|
30
|
+
* scoped to CHANGELOG.md + package.json): an added changelog heading and an
|
|
31
|
+
* added package.json version bump, agreeing on the same version. Pure over
|
|
32
|
+
* the patch text — no git call — so it is unit-testable with synthetic
|
|
33
|
+
* patches. See docs/design/release-pr-review-mode.md.
|
|
34
|
+
*/
|
|
35
|
+
export function detectReleaseVersion(patchText: string): string | null {
|
|
36
|
+
const addedLines = patchText
|
|
37
|
+
.split('\n')
|
|
38
|
+
.filter((l) => l.startsWith('+') && !l.startsWith('+++'))
|
|
39
|
+
.map((l) => l.slice(1));
|
|
40
|
+
|
|
41
|
+
let changelogVersion: string | null = null;
|
|
42
|
+
let packageVersion: string | null = null;
|
|
43
|
+
for (const line of addedLines) {
|
|
44
|
+
const changelogMatch = /^##\s*\[(\d+\.\d+\.\d+)\]/.exec(line);
|
|
45
|
+
if (changelogMatch) changelogVersion = changelogMatch[1] ?? null;
|
|
46
|
+
const packageMatch = /^\s*"version"\s*:\s*"(\d+\.\d+\.\d+)"/.exec(line);
|
|
47
|
+
if (packageMatch) packageVersion = packageMatch[1] ?? null;
|
|
48
|
+
}
|
|
49
|
+
if (changelogVersion && packageVersion && changelogVersion === packageVersion) {
|
|
50
|
+
return changelogVersion;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function releaseDetectionPatch(baseRef: string, cwd: string): string {
|
|
56
|
+
const r = spawnSync('git', ['diff', `${baseRef}...HEAD`, '--', 'CHANGELOG.md', 'package.json'], {
|
|
57
|
+
cwd,
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
60
|
+
});
|
|
61
|
+
return (r.stdout ?? '').toString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Impure wrapper: collects the packaging patch via git, then detects. */
|
|
65
|
+
export function detectReleaseVersionFromGit(
|
|
66
|
+
baseRef: string,
|
|
67
|
+
cwd: string = REPO_ROOT,
|
|
68
|
+
): string | null {
|
|
69
|
+
return detectReleaseVersion(releaseDetectionPatch(baseRef, cwd));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `major.minor.patch`, tolerating an optional `v` prefix; null if not semver-shaped. */
|
|
73
|
+
function parseSemver(raw: string): [number, number, number] | null {
|
|
74
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(raw.trim());
|
|
75
|
+
if (!m) return null;
|
|
76
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function semverLessThan(a: [number, number, number], b: [number, number, number]): boolean {
|
|
80
|
+
const [aMajor, aMinor, aPatch] = a;
|
|
81
|
+
const [bMajor, bMinor, bPatch] = b;
|
|
82
|
+
if (aMajor !== bMajor) return aMajor < bMajor;
|
|
83
|
+
if (aMinor !== bMinor) return aMinor < bMinor;
|
|
84
|
+
return aPatch < bPatch;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Highest semver-shaped tag strictly below `version`, or null if none. Pure
|
|
89
|
+
* over the supplied tag list — no git call. Non-semver tags (branch-backup
|
|
90
|
+
* names etc.) are ignored rather than throwing.
|
|
91
|
+
*
|
|
92
|
+
* The filtering is load-bearing, not defensive: this repository carries 152
|
|
93
|
+
* semver tags alongside names like `rebase-backup-pre-squash`, and those sort
|
|
94
|
+
* *above* every version under `git tag --sort=-v:refname`. A naive "newest
|
|
95
|
+
* tag" would resolve the scope to a backup ref.
|
|
96
|
+
*/
|
|
97
|
+
export function pickPreviousTag(version: string, tags: readonly string[]): string | null {
|
|
98
|
+
const target = parseSemver(version);
|
|
99
|
+
if (!target) return null;
|
|
100
|
+
let best: { raw: string; parsed: [number, number, number] } | null = null;
|
|
101
|
+
for (const raw of tags) {
|
|
102
|
+
const parsed = parseSemver(raw);
|
|
103
|
+
if (!parsed) continue;
|
|
104
|
+
if (!semverLessThan(parsed, target)) continue;
|
|
105
|
+
if (!best || semverLessThan(best.parsed, parsed)) best = { raw, parsed };
|
|
106
|
+
}
|
|
107
|
+
return best ? best.raw : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Local semver tags, newest first is not assumed — `pickPreviousTag` orders them. */
|
|
111
|
+
function localTags(cwd: string): string[] {
|
|
112
|
+
const r = spawnSync('git', ['tag', '--list'], { cwd, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
|
113
|
+
if ((r.status ?? 1) !== 0) return [];
|
|
114
|
+
return (r.stdout ?? '')
|
|
115
|
+
.split('\n')
|
|
116
|
+
.map((l) => l.trim())
|
|
117
|
+
.filter((l) => l.length > 0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ContentLintScope {
|
|
121
|
+
/** The ref a diff-scoped content check should compare HEAD against. */
|
|
122
|
+
readonly base: string;
|
|
123
|
+
/** True when the current head is a release PR whose scope was widened. */
|
|
124
|
+
readonly isRelease: boolean;
|
|
125
|
+
/** The release version, when detected. */
|
|
126
|
+
readonly version: string | null;
|
|
127
|
+
/** The tag the scope widened to, when resolved. */
|
|
128
|
+
readonly previousTag: string | null;
|
|
129
|
+
/** One line naming what was resolved and why — for the check's own output. */
|
|
130
|
+
readonly reason: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface ResolveOptions {
|
|
134
|
+
/** Branch a normal PR is compared against. */
|
|
135
|
+
readonly baseRef?: string;
|
|
136
|
+
readonly cwd?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Explicit override, highest precedence. Set by a caller that already
|
|
139
|
+
* knows the range (a workflow input, a `--since` flag).
|
|
140
|
+
*/
|
|
141
|
+
readonly since?: string | null | undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the diff base a content check should use.
|
|
146
|
+
*
|
|
147
|
+
* Precedence: explicit `since` → `RELEASE_LINT_BASE` env → release-PR widening
|
|
148
|
+
* → the ordinary base ref. The release branch is *not* inferred from the
|
|
149
|
+
* branch name alone: the packaging-diff detector is what proves a release, so
|
|
150
|
+
* a branch merely named `release/...` with no version bump stays on the
|
|
151
|
+
* ordinary base rather than silently widening to a whole tag span.
|
|
152
|
+
*
|
|
153
|
+
* Never throws. A release whose previous tag cannot be resolved (the very
|
|
154
|
+
* first release, or a shallow clone with no tags) falls back to the ordinary
|
|
155
|
+
* base and says so in `reason` — a check that cannot widen its scope should
|
|
156
|
+
* report a small scope honestly, not crash the release.
|
|
157
|
+
*/
|
|
158
|
+
export function resolveContentLintScope(opts: ResolveOptions = {}): ContentLintScope {
|
|
159
|
+
const cwd = opts.cwd ?? REPO_ROOT;
|
|
160
|
+
const baseRef = opts.baseRef ?? 'origin/main';
|
|
161
|
+
|
|
162
|
+
const explicit = opts.since ?? process.env['RELEASE_LINT_BASE'] ?? null;
|
|
163
|
+
if (explicit !== null && explicit.trim() !== '') {
|
|
164
|
+
const base = explicit.trim();
|
|
165
|
+
return {
|
|
166
|
+
base,
|
|
167
|
+
isRelease: false,
|
|
168
|
+
version: null,
|
|
169
|
+
previousTag: null,
|
|
170
|
+
reason: `scope pinned to ${base} by an explicit override`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const version = detectReleaseVersionFromGit(baseRef, cwd);
|
|
175
|
+
if (version === null) {
|
|
176
|
+
return {
|
|
177
|
+
base: baseRef,
|
|
178
|
+
isRelease: false,
|
|
179
|
+
version: null,
|
|
180
|
+
previousTag: null,
|
|
181
|
+
reason: `ordinary PR — scope is ${baseRef}...HEAD`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const previousTag = pickPreviousTag(version, localTags(cwd));
|
|
186
|
+
if (previousTag === null) {
|
|
187
|
+
return {
|
|
188
|
+
base: baseRef,
|
|
189
|
+
isRelease: true,
|
|
190
|
+
version,
|
|
191
|
+
previousTag: null,
|
|
192
|
+
reason:
|
|
193
|
+
`release ${version} detected, but no earlier semver tag is reachable — ` +
|
|
194
|
+
`scope stays ${baseRef}...HEAD and therefore sees only the release wrapper`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
base: previousTag,
|
|
200
|
+
isRelease: true,
|
|
201
|
+
version,
|
|
202
|
+
previousTag,
|
|
203
|
+
reason:
|
|
204
|
+
`release ${version} detected — scope widened to ${previousTag}...HEAD ` +
|
|
205
|
+
`so the check sees the release, not the version bump`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scoped-projection predicate — ONE implementation, two callers.
|
|
3
|
+
*
|
|
4
|
+
* `projection.mode: scoped` narrows a GLOBAL deploy to the active profile's
|
|
5
|
+
* packs. Two places need to know exactly which skills survive that prune:
|
|
6
|
+
*
|
|
7
|
+
* 1. `install.ts` — which artefacts it actually writes to the user's tree.
|
|
8
|
+
* 2. `count_scoped_projection.ts` — the number the claims ledger publishes.
|
|
9
|
+
*
|
|
10
|
+
* Before this module those were the same rule written once and *typed* a
|
|
11
|
+
* second time into `docs/CLAIMS.md`. The published pair drifted from the
|
|
12
|
+
* benchmark doc it cited as its own method (217/288 vs 215/286) and grew by
|
|
13
|
+
* one on every skill added, because nothing could compare them. The fix is
|
|
14
|
+
* not a second counter — a second counting path that disagrees with the
|
|
15
|
+
* first is the failure this replaces. It is this module: the installer and
|
|
16
|
+
* the counter call the same functions, so a divergence is impossible rather
|
|
17
|
+
* than merely unlikely.
|
|
18
|
+
*
|
|
19
|
+
* Consumers: `install.ts` (the prune itself) and `count_scoped_projection.ts`
|
|
20
|
+
* (the published number).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import type * as YamlModule from 'yaml';
|
|
27
|
+
|
|
28
|
+
import * as surface_tiers from './surface_tiers.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ESM-safe `require`. A bare `require('yaml')` is `undefined` under tsx (the
|
|
32
|
+
* runner every `./scripts-run` gate uses), so the older lazy-require pattern
|
|
33
|
+
* elsewhere in this tree degrades to "no yaml" silently. `createRequire`
|
|
34
|
+
* keeps the load lazy — `yaml` stays out of the installer bundle's static
|
|
35
|
+
* graph — while actually resolving.
|
|
36
|
+
*/
|
|
37
|
+
const _require = createRequire(import.meta.url);
|
|
38
|
+
|
|
39
|
+
/** `workspaces` values whose packs are ALWAYS active under `scoped` mode. */
|
|
40
|
+
export const SCOPED_ACTIVE_WORKSPACES: ReadonlySet<string> = new Set([
|
|
41
|
+
'engineering',
|
|
42
|
+
'agent-config-maintainer',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export interface PackRecord {
|
|
46
|
+
id: string;
|
|
47
|
+
workspaces: string[];
|
|
48
|
+
requires: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
52
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Lazy YAML safe_load mirroring PyYAML (version 1.1); `null` when absent. */
|
|
56
|
+
function yamlSafeLoad(text: string): unknown {
|
|
57
|
+
let YAML: typeof YamlModule;
|
|
58
|
+
try {
|
|
59
|
+
YAML = _require('yaml') as typeof YamlModule;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return YAML.parse(text, { version: '1.1' }) ?? null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Repo-relative location of the pack vocabulary. */
|
|
71
|
+
export const PACKS_YML_REL = path.join('src', 'config', 'discovery', 'packs.yml');
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse `src/config/discovery/packs.yml` into `{id, workspaces, requires}`
|
|
75
|
+
* records. `requires` falls back to the legacy `requires_hint` field name
|
|
76
|
+
* when `requires` is absent.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately THROWS on a missing file or a parse result that is not a
|
|
79
|
+
* list — an empty active-pack set would be the LEAST safe fallback here
|
|
80
|
+
* (it would prune every tagged artefact, the opposite of the fail-safe
|
|
81
|
+
* contract). Callers wrap this in a try/catch that restores the full tree
|
|
82
|
+
* on any error instead.
|
|
83
|
+
*/
|
|
84
|
+
export function load_packs_registry(package_root: string): PackRecord[] {
|
|
85
|
+
const vocab_path = path.join(package_root, PACKS_YML_REL);
|
|
86
|
+
const data = yamlSafeLoad(fs.readFileSync(vocab_path, 'utf-8'));
|
|
87
|
+
if (!Array.isArray(data)) {
|
|
88
|
+
throw new Error(`packs.yml did not parse to a list: ${vocab_path}`);
|
|
89
|
+
}
|
|
90
|
+
const out: PackRecord[] = [];
|
|
91
|
+
for (const entry of data) {
|
|
92
|
+
if (!isPlainObject(entry)) continue;
|
|
93
|
+
const id = entry['id'];
|
|
94
|
+
if (typeof id !== 'string' || id === '') continue;
|
|
95
|
+
const workspaces_raw = entry['workspaces'];
|
|
96
|
+
const workspaces = Array.isArray(workspaces_raw)
|
|
97
|
+
? workspaces_raw.filter((w): w is string => typeof w === 'string')
|
|
98
|
+
: [];
|
|
99
|
+
const requires_raw = entry['requires'] ?? entry['requires_hint'];
|
|
100
|
+
const requires = Array.isArray(requires_raw)
|
|
101
|
+
? requires_raw.filter((r): r is string => typeof r === 'string')
|
|
102
|
+
: [];
|
|
103
|
+
out.push({ id, workspaces, requires });
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Active pack id set for `scoped` mode: every pack whose `workspaces`
|
|
110
|
+
* intersects `SCOPED_ACTIVE_WORKSPACES`, unioned with `runtime_active_packs`,
|
|
111
|
+
* expanded over the `requires` graph (transitive closure).
|
|
112
|
+
*/
|
|
113
|
+
export function compute_active_pack_ids(
|
|
114
|
+
packs: readonly PackRecord[],
|
|
115
|
+
runtime_active_packs: readonly string[],
|
|
116
|
+
): Set<string> {
|
|
117
|
+
const by_id = new Map<string, PackRecord>();
|
|
118
|
+
for (const p of packs) by_id.set(p.id, p);
|
|
119
|
+
|
|
120
|
+
const active = new Set<string>();
|
|
121
|
+
for (const p of packs) {
|
|
122
|
+
if (p.workspaces.some((w) => SCOPED_ACTIVE_WORKSPACES.has(w))) {
|
|
123
|
+
active.add(p.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const id of runtime_active_packs) {
|
|
127
|
+
active.add(id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let frontier = [...active];
|
|
131
|
+
while (frontier.length > 0) {
|
|
132
|
+
const next: string[] = [];
|
|
133
|
+
for (const id of frontier) {
|
|
134
|
+
const rec = by_id.get(id);
|
|
135
|
+
if (rec === undefined) continue;
|
|
136
|
+
for (const dep of rec.requires) {
|
|
137
|
+
if (!active.has(dep)) {
|
|
138
|
+
active.add(dep);
|
|
139
|
+
next.push(dep);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
frontier = next;
|
|
144
|
+
}
|
|
145
|
+
return active;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The prune predicate itself: `true` when this artefact is DROPPED under
|
|
150
|
+
* `scoped`. Untagged artefacts (empty `packs:` frontmatter) are core/shared
|
|
151
|
+
* and always kept; a tagged artefact survives iff its tags intersect the
|
|
152
|
+
* active set.
|
|
153
|
+
*/
|
|
154
|
+
export function is_pruned_under_scoped(md_path: string, active_ids: ReadonlySet<string>): boolean {
|
|
155
|
+
const packs = surface_tiers.frontmatter_packs(md_path);
|
|
156
|
+
if (packs.size === 0) return false;
|
|
157
|
+
for (const id of packs) {
|
|
158
|
+
if (active_ids.has(id)) return false;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ScopedProjectionStats {
|
|
164
|
+
/** Skills a default `projection.mode: scoped` install deploys. */
|
|
165
|
+
projected: number;
|
|
166
|
+
/** Every skill in the catalog — identical to canonical `count('skills')`. */
|
|
167
|
+
total: number;
|
|
168
|
+
/** `total - projected`; the reduction the claim reports. */
|
|
169
|
+
pruned: number;
|
|
170
|
+
/** Pack ids active by default, sorted — the requires-closure result. */
|
|
171
|
+
active_packs: string[];
|
|
172
|
+
/** The workspaces whose packs are always on under `scoped`. */
|
|
173
|
+
workspaces: string[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Partition a skill catalog with the installer's own predicate.
|
|
178
|
+
*
|
|
179
|
+
* The caller supplies the file set — in practice always
|
|
180
|
+
* `update_counts.iter_skills()`, the walk behind the canonical
|
|
181
|
+
* `count('skills')`, so `projected + pruned === total` holds by
|
|
182
|
+
* construction. Taking it as a parameter (rather than importing
|
|
183
|
+
* `update_counts` here) keeps this module free of a cycle: `update_counts`
|
|
184
|
+
* imports it, not the other way round.
|
|
185
|
+
*
|
|
186
|
+
* `runtime_active_packs` defaults to empty: the published claim is about the
|
|
187
|
+
* DEFAULT for a new install, and a fresh machine has no overlay.
|
|
188
|
+
*/
|
|
189
|
+
export function scoped_projection_stats(
|
|
190
|
+
package_root: string,
|
|
191
|
+
skills: Iterable<string>,
|
|
192
|
+
runtime_active_packs: readonly string[] = [],
|
|
193
|
+
): ScopedProjectionStats {
|
|
194
|
+
const active = compute_active_pack_ids(load_packs_registry(package_root), runtime_active_packs);
|
|
195
|
+
|
|
196
|
+
let projected = 0;
|
|
197
|
+
let total = 0;
|
|
198
|
+
for (const skill of skills) {
|
|
199
|
+
total += 1;
|
|
200
|
+
if (!is_pruned_under_scoped(skill, active)) {
|
|
201
|
+
projected += 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
projected,
|
|
207
|
+
total,
|
|
208
|
+
pruned: total - projected,
|
|
209
|
+
active_packs: [...active].sort(),
|
|
210
|
+
workspaces: [...SCOPED_ACTIVE_WORKSPACES].sort(),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -51,7 +51,14 @@ export interface StanceTallyResult {
|
|
|
51
51
|
consensus: OptionTally | null; // the option clearing the threshold, or null
|
|
52
52
|
split: boolean; // true when no option clears the threshold
|
|
53
53
|
abstain_count: number;
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Members whose stance line was missing or unparseable. They COUNT toward
|
|
56
|
+
* `w_total` (they responded) and back no option — see the
|
|
57
|
+
* refusal-preservation invariant in `tally_stances`. Surfaced by
|
|
58
|
+
* `render_vote_tally` so a shrunken-signal quorum is visible rather than
|
|
59
|
+
* merely computed.
|
|
60
|
+
*/
|
|
61
|
+
needs_repair: string[];
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
// Tolerant of whitespace and case; requires the three explicit fields in order.
|
|
@@ -150,7 +157,23 @@ export function tally_stances(
|
|
|
150
157
|
parsed.push({ member, ...p });
|
|
151
158
|
}
|
|
152
159
|
|
|
153
|
-
|
|
160
|
+
// ── Refusal-preservation invariant (road-to-governance-invariants P2) ──
|
|
161
|
+
//
|
|
162
|
+
// Selection may never rank an option higher *because* a member refused
|
|
163
|
+
// less. A safety refusal is not a scored-down property, and — measured by
|
|
164
|
+
// spike S0.1 — the way this engine could be steered was not a weight but a
|
|
165
|
+
// DENOMINATOR: `w_total` once counted only members whose stance line
|
|
166
|
+
// parsed, so a refusal phrased as prose ("I will not answer this") vanished
|
|
167
|
+
// from the quorum and made consensus EASIER. Same two backers, margin
|
|
168
|
+
// −0.25 and no consensus when the refusal parsed as an abstention;
|
|
169
|
+
// +0.4167 and consensus when it did not parse. Δ 0.6667, outcome flipped.
|
|
170
|
+
//
|
|
171
|
+
// The fix is structural rather than a new gate: a member who RESPONDED
|
|
172
|
+
// counts toward the quorum whether or not its stance line parsed. It backs
|
|
173
|
+
// no option either way, so an unparseable response now behaves exactly like
|
|
174
|
+
// an abstention — the fail-safe direction. Members who never responded are
|
|
175
|
+
// dropped upstream (`!r.error`) and are correctly absent from `members`.
|
|
176
|
+
const w_total = parsed.length + needs_repair.length; // responded, abstain + unparseable included
|
|
154
177
|
const threshold = CONSENSUS_FRACTION * w_total;
|
|
155
178
|
let abstain_count = 0;
|
|
156
179
|
|
|
@@ -208,6 +231,18 @@ export function render_vote_tally(result: StanceTallyResult): string {
|
|
|
208
231
|
if (result.abstain_count > 0) {
|
|
209
232
|
lines.push(`- abstain — ${result.abstain_count} member(s) (raises the bar)`);
|
|
210
233
|
}
|
|
234
|
+
// Refusal divergence, as an OBSERVATION and never a selection input
|
|
235
|
+
// (road-to-governance-invariants P2). A member whose stance did not parse
|
|
236
|
+
// still counts toward the quorum; saying so here is what makes a shrunken
|
|
237
|
+
// signal visible to a reader instead of merely present in a struct field
|
|
238
|
+
// no caller reads. Nothing in the tally consumes this string.
|
|
239
|
+
if (result.needs_repair.length > 0) {
|
|
240
|
+
lines.push(
|
|
241
|
+
`- unparsed — ${result.needs_repair.length} member(s) responded without a ` +
|
|
242
|
+
`readable stance (${result.needs_repair.join(', ')}); counted in the ` +
|
|
243
|
+
'quorum, backing nothing',
|
|
244
|
+
);
|
|
245
|
+
}
|
|
211
246
|
lines.push(`Threshold: ⅔ × ${result.w_total} = ${result.threshold.toFixed(2)}`);
|
|
212
247
|
lines.push(
|
|
213
248
|
result.consensus !== null
|
|
@@ -136,14 +136,14 @@ export function _split_frontmatter(text: string): [Record<string, Json>, string]
|
|
|
136
136
|
interface TriggerSummary {
|
|
137
137
|
path_prefixes: string[];
|
|
138
138
|
keywords: string[];
|
|
139
|
-
|
|
139
|
+
phrases: string[];
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/** Mirror `_trigger_summary`. */
|
|
143
143
|
export function _trigger_summary(triggers: Json): TriggerSummary {
|
|
144
144
|
const paths: string[] = [];
|
|
145
145
|
const keywords: string[] = [];
|
|
146
|
-
const
|
|
146
|
+
const phrases: string[] = [];
|
|
147
147
|
const list: Json[] = Array.isArray(triggers) ? triggers : [];
|
|
148
148
|
for (const entry of list) {
|
|
149
149
|
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
@@ -156,11 +156,11 @@ export function _trigger_summary(triggers: Json): TriggerSummary {
|
|
|
156
156
|
if ('keyword' in e) {
|
|
157
157
|
keywords.push(String(e['keyword']));
|
|
158
158
|
}
|
|
159
|
-
if ('
|
|
160
|
-
|
|
159
|
+
if ('phrase' in e) {
|
|
160
|
+
phrases.push(String(e['phrase']));
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
-
return { path_prefixes: paths, keywords,
|
|
163
|
+
return { path_prefixes: paths, keywords, phrases };
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
interface RuleEntry {
|
|
@@ -208,7 +208,7 @@ export function collect(): RuleEntry[] {
|
|
|
208
208
|
description_chars: pyLen(desc),
|
|
209
209
|
triggers,
|
|
210
210
|
trigger_count:
|
|
211
|
-
triggers.path_prefixes.length + triggers.keywords.length + triggers.
|
|
211
|
+
triggers.path_prefixes.length + triggers.keywords.length + triggers.phrases.length,
|
|
212
212
|
routes_to,
|
|
213
213
|
body_chars: pyLen(body),
|
|
214
214
|
file_chars: pyLen(text),
|
|
@@ -292,7 +292,7 @@ export function render_markdown(rules: RuleEntry[]): string {
|
|
|
292
292
|
const triggers =
|
|
293
293
|
`${r.triggers.path_prefixes.length}p / ` +
|
|
294
294
|
`${r.triggers.keywords.length}k / ` +
|
|
295
|
-
`${r.triggers.
|
|
295
|
+
`${r.triggers.phrases.length}ph`;
|
|
296
296
|
const routes = r.routes_to.map((x) => String(x)).join(', ') || '—';
|
|
297
297
|
lines.push(
|
|
298
298
|
`| ${i} | \`${r.name}\` | ${_pyTruthy(r.tier) ? String(r.tier) : '—'} | ` +
|
|
@@ -302,7 +302,7 @@ export function render_markdown(rules: RuleEntry[]): string {
|
|
|
302
302
|
i++;
|
|
303
303
|
}
|
|
304
304
|
lines.push('');
|
|
305
|
-
lines.push('Trigger key: `Np` = path-prefix, `Nk` = keyword, `
|
|
305
|
+
lines.push('Trigger key: `Np` = path-prefix, `Nk` = keyword, `Nph` = phrase.');
|
|
306
306
|
lines.push('');
|
|
307
307
|
return lines.join('\n');
|
|
308
308
|
}
|
|
@@ -393,7 +393,7 @@ function _ruleToJson(r: RuleEntry): Record<string, Json> {
|
|
|
393
393
|
triggers: {
|
|
394
394
|
path_prefixes: r.triggers.path_prefixes,
|
|
395
395
|
keywords: r.triggers.keywords,
|
|
396
|
-
|
|
396
|
+
phrases: r.triggers.phrases,
|
|
397
397
|
},
|
|
398
398
|
trigger_count: r.trigger_count,
|
|
399
399
|
routes_to: r.routes_to,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* For every auto-rule from `agents/reports/auto-rules-audit.json`:
|
|
17
17
|
*
|
|
18
18
|
* 1. Build a token set from `description`, `triggers[].keyword`,
|
|
19
|
-
* `triggers[].
|
|
19
|
+
* `triggers[].phrase`, and the rule name itself.
|
|
20
20
|
* 2. Index a corpus of skills (`SKILL.md`), contexts, guidelines, and
|
|
21
21
|
* command files.
|
|
22
22
|
* 3. Score `corpus_hits = sum(1 for token in tokens if token in corpus)`.
|
|
@@ -197,9 +197,9 @@ function _ruleTokens(rule: Json): Set<string> {
|
|
|
197
197
|
add(tokens(String(rule['name'] ?? '').replaceAll('-', ' ')));
|
|
198
198
|
const trig = rule['triggers'] ?? {};
|
|
199
199
|
const kw: Json[] = Array.isArray(trig['keywords']) ? trig['keywords'] : [];
|
|
200
|
-
const
|
|
200
|
+
const phrases: Json[] = Array.isArray(trig['phrases']) ? trig['phrases'] : [];
|
|
201
201
|
add(tokens(kw.map((x) => String(x)).join(' ')));
|
|
202
|
-
add(tokens(
|
|
202
|
+
add(tokens(phrases.map((x) => String(x)).join(' ')));
|
|
203
203
|
return out;
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -54,7 +54,7 @@ const _ALL_DIGITS = /^[0-9]+$/;
|
|
|
54
54
|
interface RuleTriggers {
|
|
55
55
|
path_prefixes: string[];
|
|
56
56
|
keywords: string[];
|
|
57
|
-
|
|
57
|
+
phrases: string[];
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
interface RuleEntry {
|
|
@@ -194,7 +194,7 @@ function analyse(rules: RuleEntry[]): Pair[] {
|
|
|
194
194
|
for (const x of keyword_set(r.triggers.keywords.join(' '))) {
|
|
195
195
|
kw.add(x);
|
|
196
196
|
}
|
|
197
|
-
for (const x of keyword_set(r.triggers.
|
|
197
|
+
for (const x of keyword_set(r.triggers.phrases.join(' '))) {
|
|
198
198
|
kw.add(x);
|
|
199
199
|
}
|
|
200
200
|
keywords.push(kw);
|