@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
|
@@ -40,6 +40,7 @@ import * as path from 'node:path';
|
|
|
40
40
|
|
|
41
41
|
import { is_kernel_rule } from '../_lib/kernel_rules.js';
|
|
42
42
|
import { EDIT_TOOLS } from '../minimal_safe_diff_hook.js';
|
|
43
|
+
import { COMMAND_TOOLS } from '../verify_before_complete_hook.js';
|
|
43
44
|
import { readHookStdin } from './hook_stdin.js';
|
|
44
45
|
|
|
45
46
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -122,25 +123,159 @@ export function targets_kernel_rule(filePath: string): string | null {
|
|
|
122
123
|
return basename.replace(/\.md$/, '');
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
/**
|
|
126
|
+
/** In-place mutators that name their target as a plain argument. */
|
|
127
|
+
const _MUTATOR_VERBS: ReadonlySet<string> = new Set([
|
|
128
|
+
'sed',
|
|
129
|
+
'tee',
|
|
130
|
+
'truncate',
|
|
131
|
+
'rm',
|
|
132
|
+
'shred',
|
|
133
|
+
]);
|
|
134
|
+
/** Verbs whose LAST positional argument is the destination. */
|
|
135
|
+
const _DEST_LAST_VERBS: ReadonlySet<string> = new Set(['mv', 'cp', 'install', 'rsync']);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Shell tokens that, in a command naming a kernel rule path, mean the path is
|
|
139
|
+
* being WRITTEN rather than read. Reads (`cat`, `grep`, `head`, `diff`,
|
|
140
|
+
* `git show`) carry none of these and stay allowed — a kernel rule is
|
|
141
|
+
* immutable, not secret.
|
|
142
|
+
*/
|
|
143
|
+
function _bash_targets_kernel_rule(command: string): string | null {
|
|
144
|
+
// Cheap reject first: no kernel-rule path in the string at all.
|
|
145
|
+
const tokens = command.split(/\s+/).filter((t) => t.length > 0);
|
|
146
|
+
const stripped = tokens.map((t) => t.replace(/^['"]|['"]$/g, ''));
|
|
147
|
+
const ruleOf = (t: string): string | null => targets_kernel_rule(t);
|
|
148
|
+
if (!stripped.some((t) => ruleOf(t) !== null)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 1. Redirection into the path: `> p`, `>> p`, `>p`, `>>p`.
|
|
153
|
+
for (let i = 0; i < stripped.length; i += 1) {
|
|
154
|
+
const tok = stripped[i] as string;
|
|
155
|
+
const inline = /^>{1,2}(.+)$/.exec(tok);
|
|
156
|
+
if (inline) {
|
|
157
|
+
const r = ruleOf(inline[1] as string);
|
|
158
|
+
if (r !== null) {
|
|
159
|
+
return r;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (tok === '>' || tok === '>>') {
|
|
163
|
+
const next = stripped[i + 1];
|
|
164
|
+
if (typeof next === 'string') {
|
|
165
|
+
const r = ruleOf(next);
|
|
166
|
+
if (r !== null) {
|
|
167
|
+
return r;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. A mutator verb anywhere in the pipeline, with the path as an argument.
|
|
174
|
+
// `sed` only counts in its in-place form — `sed 's/x/y/' file` prints.
|
|
175
|
+
const verbs = new Set<string>();
|
|
176
|
+
let prevWasSeparator = true;
|
|
177
|
+
for (const tok of stripped) {
|
|
178
|
+
if (['&&', '||', ';', '|'].includes(tok)) {
|
|
179
|
+
prevWasSeparator = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (prevWasSeparator && !tok.startsWith('-')) {
|
|
183
|
+
verbs.add(path.basename(tok));
|
|
184
|
+
prevWasSeparator = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const sedInPlace = verbs.has('sed') && stripped.some((t) => /^-i/.test(t));
|
|
188
|
+
for (const verb of verbs) {
|
|
189
|
+
if (verb === 'sed' && !sedInPlace) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (_MUTATOR_VERBS.has(verb)) {
|
|
193
|
+
const hit = stripped.map(ruleOf).find((r) => r !== null);
|
|
194
|
+
if (hit !== undefined && hit !== null) {
|
|
195
|
+
return hit;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (_DEST_LAST_VERBS.has(verb)) {
|
|
199
|
+
const last = stripped[stripped.length - 1] as string;
|
|
200
|
+
const r = ruleOf(last);
|
|
201
|
+
if (r !== null) {
|
|
202
|
+
return r;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Return (blocked, reason) for one PreToolUse envelope. Pure — no I/O beyond
|
|
211
|
+
* the arg.
|
|
212
|
+
*
|
|
213
|
+
* Two surfaces, one effect. The Write/Edit branch is the original Layer-1
|
|
214
|
+
* guard. The Bash branch was added by road-to-governance-invariants Phase 1
|
|
215
|
+
* after the S0.2 spike measured the gap: this gate keyed on the TOOL NAME, so
|
|
216
|
+
* `Bash sed -i … src/rules/commit-policy.md` reached the immutable-rule
|
|
217
|
+
* outcome the Write branch refuses, and a two-step
|
|
218
|
+
* `Write docs/staging/<kernel>.md` → `Bash mv … src/rules/<kernel>.md`
|
|
219
|
+
* sequence did the same with every step individually allowed.
|
|
220
|
+
*
|
|
221
|
+
* The Bash branch is deliberately narrow (2026-08-02 council cut, option ii):
|
|
222
|
+
* only redirection into the path, an in-place `sed`, a `tee`/`truncate`/`rm`
|
|
223
|
+
* naming it, or a `mv`/`cp` whose DESTINATION it is. Reads stay allowed — a
|
|
224
|
+
* kernel rule is immutable, not secret — and no attempt is made to understand
|
|
225
|
+
* arbitrary shell. Recognising every conceivable write verb would make this a
|
|
226
|
+
* shell sandbox, which is the failure mode the council named.
|
|
227
|
+
*/
|
|
126
228
|
export function check_envelope(envelope: JsonObject): [boolean, string] {
|
|
127
229
|
const { tool, paths } = _extract(envelope);
|
|
128
|
-
if (!tool
|
|
230
|
+
if (!tool) {
|
|
129
231
|
return [false, ''];
|
|
130
232
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
233
|
+
const deny = (ruleName: string): [boolean, string] => [
|
|
234
|
+
true,
|
|
235
|
+
`kernel rule ${ruleName} is immutable — tighten-only via the override ` +
|
|
236
|
+
'exception registry; see docs/contracts/kernel-membership.md',
|
|
237
|
+
];
|
|
238
|
+
if (EDIT_TOOLS.has(tool)) {
|
|
239
|
+
for (const p of paths) {
|
|
240
|
+
const ruleName = targets_kernel_rule(p);
|
|
241
|
+
if (ruleName !== null) {
|
|
242
|
+
return deny(ruleName);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return [false, ''];
|
|
246
|
+
}
|
|
247
|
+
if (COMMAND_TOOLS.has(tool)) {
|
|
248
|
+
const cmd = _extract_command(envelope);
|
|
249
|
+
if (cmd !== null) {
|
|
250
|
+
const ruleName = _bash_targets_kernel_rule(cmd);
|
|
251
|
+
if (ruleName !== null) {
|
|
252
|
+
return deny(ruleName);
|
|
253
|
+
}
|
|
139
254
|
}
|
|
140
255
|
}
|
|
141
256
|
return [false, ''];
|
|
142
257
|
}
|
|
143
258
|
|
|
259
|
+
/** Best-effort read of a shell command off a PreToolUse envelope. */
|
|
260
|
+
function _extract_command(envelope: JsonObject): string | null {
|
|
261
|
+
const payload = _isObject(envelope['payload']) ? envelope['payload'] : envelope;
|
|
262
|
+
const ti = _isObject(payload['tool_input'])
|
|
263
|
+
? payload['tool_input']
|
|
264
|
+
: _isObject(envelope['tool_input'])
|
|
265
|
+
? envelope['tool_input']
|
|
266
|
+
: null;
|
|
267
|
+
for (const src of [ti, payload]) {
|
|
268
|
+
if (src === null) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const v = src['command'];
|
|
272
|
+
if (typeof v === 'string' && v) {
|
|
273
|
+
return v;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
144
279
|
function _asObject(v: JsonValue | undefined): JsonObject | null {
|
|
145
280
|
return _isObject(v) ? v : null;
|
|
146
281
|
}
|
|
@@ -33,6 +33,34 @@ const _SHELL_SEPARATORS: ReadonlySet<string> = new Set(['&&', '||', ';', '|']);
|
|
|
33
33
|
const _NO_VERIFY_FLAGS: ReadonlySet<string> = new Set(['--no-verify']);
|
|
34
34
|
const _NO_VERIFY_SHORT: ReadonlySet<string> = new Set(['-n']);
|
|
35
35
|
const _HOOKS_PATH_RE = /^core\.hooksPath\s*=/i;
|
|
36
|
+
/** Bare key form, as `git config core.hooksPath <value>` spells it. */
|
|
37
|
+
const _HOOKS_PATH_KEY_RE = /^core\.hooksPath$/i;
|
|
38
|
+
/**
|
|
39
|
+
* `git config` flags that make the invocation a READ or a RESTORE rather than
|
|
40
|
+
* a write. `--unset` removes the override — the opposite of disabling hooks —
|
|
41
|
+
* so it stays allowed, or the gate would block its own remediation.
|
|
42
|
+
*/
|
|
43
|
+
/** Global `git` options that consume the following token as their value. */
|
|
44
|
+
const _GIT_GLOBAL_OPTS_WITH_VALUE: ReadonlySet<string> = new Set([
|
|
45
|
+
'-C',
|
|
46
|
+
'-c',
|
|
47
|
+
'--config',
|
|
48
|
+
'--git-dir',
|
|
49
|
+
'--work-tree',
|
|
50
|
+
'--namespace',
|
|
51
|
+
'--exec-path',
|
|
52
|
+
'--super-prefix',
|
|
53
|
+
]);
|
|
54
|
+
const _CONFIG_READ_OR_RESTORE: ReadonlySet<string> = new Set([
|
|
55
|
+
'--get',
|
|
56
|
+
'--get-all',
|
|
57
|
+
'--get-regexp',
|
|
58
|
+
'--get-urlmatch',
|
|
59
|
+
'--list',
|
|
60
|
+
'-l',
|
|
61
|
+
'--unset',
|
|
62
|
+
'--unset-all',
|
|
63
|
+
]);
|
|
36
64
|
|
|
37
65
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
38
66
|
type JsonObject = { [k: string]: JsonValue };
|
|
@@ -178,7 +206,80 @@ function _git_base(tokens: string[]): string[] | null {
|
|
|
178
206
|
return null;
|
|
179
207
|
}
|
|
180
208
|
|
|
209
|
+
/**
|
|
210
|
+
* `git config … core.hooksPath <value>` — the subcommand form of the hooksPath
|
|
211
|
+
* override, distinct from the `-c` / `--config` global-option form handled in
|
|
212
|
+
* `_is_blocked`.
|
|
213
|
+
*
|
|
214
|
+
* Closed by road-to-governance-invariants Phase 1 after the S0.2 spike measured
|
|
215
|
+
* it: `git-history-discipline` already claimed `core.hooksPath` overrides were
|
|
216
|
+
* "deterministically blocked", and only the inline `-c` form was. That made
|
|
217
|
+
* `git config core.hooksPath /dev/null` → `git commit` a two-step sequence
|
|
218
|
+
* whose every step this gate allowed and whose composition is exactly the
|
|
219
|
+
* outcome the gate exists to prevent.
|
|
220
|
+
*
|
|
221
|
+
* Deliberately narrow, per the 2026-08-02 council cut (option ii — widen only
|
|
222
|
+
* the exact shapes with no plausible legitimate use):
|
|
223
|
+
* - a READ (`--get`, `--list`, or the key with no value) is allowed;
|
|
224
|
+
* - `--unset` is allowed — it restores hooks, it does not disable them;
|
|
225
|
+
* - non-git ways of disabling a hook (`mv`/`chmod`/`rm` on `.git/hooks/*`)
|
|
226
|
+
* are NOT matched here. Recognising them would turn this guard into a
|
|
227
|
+
* shell sandbox and those verbs have ordinary legitimate uses. That gap
|
|
228
|
+
* is published rather than papered over — see the S0.2 spike.
|
|
229
|
+
*/
|
|
230
|
+
function _is_config_hookspath_write(git_tokens: string[]): [boolean, string] {
|
|
231
|
+
const rest = git_tokens.slice(1);
|
|
232
|
+
// The subcommand is the first token that is neither a global option nor
|
|
233
|
+
// the VALUE of one — `git -C /repo config …` must still resolve to
|
|
234
|
+
// `config`, not to `/repo`.
|
|
235
|
+
let subIdx = -1;
|
|
236
|
+
for (let k = 0; k < rest.length; k += 1) {
|
|
237
|
+
const tok = rest[k] as string;
|
|
238
|
+
if (!tok.startsWith('-')) {
|
|
239
|
+
subIdx = k;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
if (_GIT_GLOBAL_OPTS_WITH_VALUE.has(tok)) {
|
|
243
|
+
k += 1; // consume its value
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (subIdx === -1 || rest[subIdx] !== 'config') {
|
|
247
|
+
return [false, ''];
|
|
248
|
+
}
|
|
249
|
+
const args = rest.slice(subIdx + 1);
|
|
250
|
+
if (args.some((t) => _CONFIG_READ_OR_RESTORE.has(t))) {
|
|
251
|
+
return [false, ''];
|
|
252
|
+
}
|
|
253
|
+
for (let k = 0; k < args.length; k += 1) {
|
|
254
|
+
const tok = args[k] as string;
|
|
255
|
+
// `core.hooksPath=<value>` — one token, always a write.
|
|
256
|
+
if (_HOOKS_PATH_RE.test(tok)) {
|
|
257
|
+
return [
|
|
258
|
+
true,
|
|
259
|
+
`'git config ${tok}' disables git hooks via hooksPath (git-history-discipline)`,
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
if (!_HOOKS_PATH_KEY_RE.test(tok)) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
// Bare key: a write only when a value follows.
|
|
266
|
+
const next = args.slice(k + 1).find((t) => !t.startsWith('-'));
|
|
267
|
+
if (next !== undefined) {
|
|
268
|
+
return [
|
|
269
|
+
true,
|
|
270
|
+
`'git config ${tok} ${next}' disables git hooks via hooksPath (git-history-discipline)`,
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
return [false, ''];
|
|
274
|
+
}
|
|
275
|
+
return [false, ''];
|
|
276
|
+
}
|
|
277
|
+
|
|
181
278
|
function _is_blocked(git_tokens: string[]): [boolean, string] {
|
|
279
|
+
const [cfgBlocked, cfgReason] = _is_config_hookspath_write(git_tokens);
|
|
280
|
+
if (cfgBlocked) {
|
|
281
|
+
return [cfgBlocked, cfgReason];
|
|
282
|
+
}
|
|
182
283
|
let i = 1; // skip 'git'
|
|
183
284
|
while (i < git_tokens.length) {
|
|
184
285
|
const tok = git_tokens[i] as string;
|
|
@@ -51,6 +51,27 @@ else
|
|
|
51
51
|
fi
|
|
52
52
|
fi
|
|
53
53
|
|
|
54
|
+
# road-to-local-ci-trust Phase 2: the CI-only repo-content gates, as one task.
|
|
55
|
+
# Deliberately placed AFTER the consistency block above — `task consistency`
|
|
56
|
+
# runs `task sync`, which generates the `.augment/` projection. Several
|
|
57
|
+
# preflight gates read that tree, so on a FRESH worktree (this repo's standard
|
|
58
|
+
# workflow) running preflight first would fail with "produced by regeneration
|
|
59
|
+
# but absent before" — red for a reason the contributor did not cause, which is
|
|
60
|
+
# exactly the anti-pattern this gate set exists to avoid. Measured 15s against
|
|
61
|
+
# the 25s `pre_push_budget_seconds` ceiling in src/config/ci-local-parity.yml.
|
|
62
|
+
# `check_enforcement_coverage` (30.7s) stays out of preflight by design.
|
|
63
|
+
echo "🔍 Preflight — the CI-only repo-content gates..."
|
|
64
|
+
if [ "${AGENT_CONFIG_SKIP_PREPUSH_PREFLIGHT:-}" = "1" ]; then
|
|
65
|
+
echo "⏭️ skipped via AGENT_CONFIG_SKIP_PREPUSH_PREFLIGHT=1"
|
|
66
|
+
elif ! command -v task >/dev/null 2>&1; then
|
|
67
|
+
echo "⚠️ 'task' not found — skipping preflight for this push."
|
|
68
|
+
elif ! task preflight; then
|
|
69
|
+
echo "❌ Preflight failed. These gates run in CI too, so pushing now buys a"
|
|
70
|
+
echo " red run and a fixup re-push. Fix the failures above, or"
|
|
71
|
+
echo " AGENT_CONFIG_SKIP_PREPUSH_PREFLIGHT=1 for a genuine WIP push."
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
54
75
|
echo "🔍 Checking for leftover conflict markers / unmerged paths..."
|
|
55
76
|
if ! ./scripts-run src/scripts/check_no_conflict_markers --quiet; then
|
|
56
77
|
echo "❌ Conflict markers or unmerged paths present. Resolve them (e.g. 'git checkout HEAD -- <file>' or finish the merge), then re-push."
|
|
@@ -127,11 +148,22 @@ if git diff --cached --name-only | grep -qE '^agents/roadmaps(-progress\.md|/)';
|
|
|
127
148
|
./scripts-run .augment/scripts/update_roadmap_progress --check
|
|
128
149
|
rstatus=$?
|
|
129
150
|
if [ $rstatus -ne 0 ]; then
|
|
151
|
+
# The check fails for THREE distinct reasons and prints the real one
|
|
152
|
+
# above: a stale dashboard, an unresolved `[~]` deferred item (Iron
|
|
153
|
+
# Law 3), or a completed-but-unarchived roadmap. Naming only the first
|
|
154
|
+
# sent a maintainer chasing a regeneration that was already correct —
|
|
155
|
+
# and a safety gate whose text points at the wrong cause trains people
|
|
156
|
+
# to distrust the gate, which costs more than the wrong message.
|
|
130
157
|
echo ""
|
|
131
|
-
echo "❌ Commit blocked —
|
|
132
|
-
echo "
|
|
133
|
-
echo "
|
|
134
|
-
echo "
|
|
158
|
+
echo "❌ Commit blocked — the roadmap dashboard gate failed."
|
|
159
|
+
echo " The cause is printed above. The three it reports:"
|
|
160
|
+
echo " · dashboard stale → './agent-config roadmap:progress',"
|
|
161
|
+
echo " then stage agents/roadmaps-progress.md"
|
|
162
|
+
echo " · Iron Law 3 → unresolved '[~]' deferred items. Surface"
|
|
163
|
+
echo " them and ask the user; NOT the agent's"
|
|
164
|
+
echo " to clear (roadmap-management § 4b)"
|
|
165
|
+
echo " · completed roadmap → 'git mv' it into agents/roadmaps/archive/,"
|
|
166
|
+
echo " then regenerate"
|
|
135
167
|
echo " To bypass for an unrelated WIP commit: git commit --no-verify"
|
|
136
168
|
exit 1
|
|
137
169
|
fi
|
package/src/scripts/install.ts
CHANGED
|
@@ -82,6 +82,13 @@ import * as user_global_paths from './_lib/user_global_paths.js';
|
|
|
82
82
|
import * as claude_desktop_bundler from './_lib/claude_desktop_bundler.js';
|
|
83
83
|
import * as claude_settings_hooks from './_lib/claude_settings_hooks.js';
|
|
84
84
|
import { find_project_root_with_anchor, load_agent_settings } from './_lib/agent_settings.js';
|
|
85
|
+
import {
|
|
86
|
+
LEGACY_ALL,
|
|
87
|
+
ruleFileArrives,
|
|
88
|
+
ruleScopeFromSettings,
|
|
89
|
+
type RuleScope,
|
|
90
|
+
} from '../install/rule_scope.js';
|
|
91
|
+
import { RULE_SOURCE_REL } from '../install/wizard-plan.js';
|
|
85
92
|
import { flattenSurface, computeSurfaceDelta, type SettingsSurface } from '../shared/settingsSurface.js';
|
|
86
93
|
import { settingsSchema } from '../server/schemas/settings.js';
|
|
87
94
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
@@ -2730,17 +2737,71 @@ function _inject_package_tag(
|
|
|
2730
2737
|
}
|
|
2731
2738
|
}
|
|
2732
2739
|
|
|
2740
|
+
/**
|
|
2741
|
+
* Would dereferencing `entry` escape the package root?
|
|
2742
|
+
*
|
|
2743
|
+
* `_copy_dir_dereferencing_symlinks` copies the REALPATH of every entry, so a
|
|
2744
|
+
* symlink in the shipped tree pointing outside the package would copy an
|
|
2745
|
+
* arbitrary readable file into the deploy destination — and those destinations
|
|
2746
|
+
* are agent-readable instruction dirs (`~/.claude/rules/`, `~/.codeium/…`). A
|
|
2747
|
+
* tampered tarball carrying `rules/x.md -> ~/.ssh/id_rsa` would land that file
|
|
2748
|
+
* where an agent reads rules.
|
|
2749
|
+
*
|
|
2750
|
+
* Defence-in-depth for `docs/threat-model.md` row b (supply-chain compromise):
|
|
2751
|
+
* the package is the install-time trust anchor, so this is not the primary
|
|
2752
|
+
* control — but the check is one comparison and `package_root` was already
|
|
2753
|
+
* threaded through, so there is no reason to ship without it.
|
|
2754
|
+
*
|
|
2755
|
+
* Both sides are realpath'd before comparing, so a macOS `/var` → `/private/var`
|
|
2756
|
+
* root does not read as an escape. A `null` package_root means the caller did
|
|
2757
|
+
* not scope the copy (several do) — unchanged, permissive behaviour there.
|
|
2758
|
+
*/
|
|
2759
|
+
function _escapes_package_root(resolved: string, package_root: string | null): boolean {
|
|
2760
|
+
if (package_root === null) return false;
|
|
2761
|
+
let root: string;
|
|
2762
|
+
try {
|
|
2763
|
+
root = fs.realpathSync(package_root);
|
|
2764
|
+
} catch {
|
|
2765
|
+
// Unresolvable root — cannot judge containment, so do not block the copy.
|
|
2766
|
+
return false;
|
|
2767
|
+
}
|
|
2768
|
+
return !global_deploy_inventory.is_ancestor(root, resolved);
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
/**
|
|
2772
|
+
* Copy `src` → `dest`, dereferencing symlinks, returning
|
|
2773
|
+
* `[written, skipped, written_paths]`.
|
|
2774
|
+
*
|
|
2775
|
+
* `file_filter` is an optional predicate over the SOURCE path; `false` means the
|
|
2776
|
+
* file is not part of this install and is neither copied nor counted. It MUST be
|
|
2777
|
+
* the same predicate handed to `global_deploy_inventory.expected_deploy_files`
|
|
2778
|
+
* for the same source — that function computes what the reaper considers
|
|
2779
|
+
* "should exist", so filtering one side only either leaves excluded files behind
|
|
2780
|
+
* forever or deletes files the copy just wrote.
|
|
2781
|
+
*/
|
|
2733
2782
|
function _copy_dir_dereferencing_symlinks(
|
|
2734
2783
|
src: string,
|
|
2735
2784
|
dest: string,
|
|
2736
2785
|
force: boolean,
|
|
2737
2786
|
package_root: string | null = null,
|
|
2787
|
+
file_filter: global_deploy_inventory.FileFilter | null = null,
|
|
2738
2788
|
): [number, number, string[]] {
|
|
2739
2789
|
let written = 0;
|
|
2740
2790
|
let skipped = 0;
|
|
2741
2791
|
const written_paths: string[] = [];
|
|
2742
2792
|
if (!pathExists(src)) return [0, 0, written_paths];
|
|
2743
2793
|
if (!isDir(src)) {
|
|
2794
|
+
if (file_filter !== null && !file_filter(src)) return [0, 0, written_paths];
|
|
2795
|
+
let resolved_src = src;
|
|
2796
|
+
try {
|
|
2797
|
+
resolved_src = fs.realpathSync(src);
|
|
2798
|
+
} catch {
|
|
2799
|
+
resolved_src = src;
|
|
2800
|
+
}
|
|
2801
|
+
if (_escapes_package_root(resolved_src, package_root)) {
|
|
2802
|
+
warn(`refused: ${src} dereferences outside the package root — not copied`);
|
|
2803
|
+
return [0, 0, written_paths];
|
|
2804
|
+
}
|
|
2744
2805
|
mkdirp(path.dirname(dest));
|
|
2745
2806
|
const decision = _resolve_file_conflict(dest, force);
|
|
2746
2807
|
if (decision === 'skip') return [0, 1, written_paths];
|
|
@@ -2784,6 +2845,13 @@ function _copy_dir_dereferencing_symlinks(
|
|
|
2784
2845
|
} catch {
|
|
2785
2846
|
resolvedIsDir = false;
|
|
2786
2847
|
}
|
|
2848
|
+
// Containment is checked on the RESOLVED path, before either the
|
|
2849
|
+
// recursion or the copy — a symlinked directory escaping the root would
|
|
2850
|
+
// otherwise pull its whole subtree in.
|
|
2851
|
+
if (_escapes_package_root(resolved, package_root)) {
|
|
2852
|
+
warn(`refused: ${entry} dereferences outside the package root — not copied`);
|
|
2853
|
+
continue;
|
|
2854
|
+
}
|
|
2787
2855
|
if (resolvedIsDir) {
|
|
2788
2856
|
mkdirp(target);
|
|
2789
2857
|
const [sub_w, sub_s, sub_p] = _copy_dir_dereferencing_symlinks(
|
|
@@ -2791,12 +2859,20 @@ function _copy_dir_dereferencing_symlinks(
|
|
|
2791
2859
|
target,
|
|
2792
2860
|
force,
|
|
2793
2861
|
package_root,
|
|
2862
|
+
file_filter,
|
|
2794
2863
|
);
|
|
2795
2864
|
written += sub_w;
|
|
2796
2865
|
skipped += sub_s;
|
|
2797
2866
|
written_paths.push(...sub_p);
|
|
2798
2867
|
continue;
|
|
2799
2868
|
}
|
|
2869
|
+
// The predicate sees the SOURCE path (the rule-scope predicate reads the
|
|
2870
|
+
// file's own frontmatter tags). Skipped files are counted as neither
|
|
2871
|
+
// written nor skipped-as-identical: they are not part of this install,
|
|
2872
|
+
// so the inventory twin must omit them too or the reaper drifts.
|
|
2873
|
+
if (file_filter !== null && !file_filter(entry)) {
|
|
2874
|
+
continue;
|
|
2875
|
+
}
|
|
2800
2876
|
const decision = _resolve_file_conflict(target, force);
|
|
2801
2877
|
if (decision === 'skip') {
|
|
2802
2878
|
skipped += 1;
|
|
@@ -2949,6 +3025,10 @@ function _deploy_global_content(
|
|
|
2949
3025
|
lockfile_path: string,
|
|
2950
3026
|
): Record<string, DeployResult> {
|
|
2951
3027
|
const results: Record<string, DeployResult> = {};
|
|
3028
|
+
// Resolved ONCE per deploy, not per tool: every tool installs the same rule
|
|
3029
|
+
// tree, and re-reading settings per tool could yield different answers
|
|
3030
|
+
// mid-run if the file changed underneath.
|
|
3031
|
+
const rule_scope = _resolve_global_rule_scope(package_root);
|
|
2952
3032
|
for (const tool_id of [...tools].sort()) {
|
|
2953
3033
|
if (tool_id === 'claude-desktop') {
|
|
2954
3034
|
results[tool_id] = _deploy_claude_desktop(force, package_root, lockfile_path);
|
|
@@ -2975,13 +3055,30 @@ function _deploy_global_content(
|
|
|
2975
3055
|
for (const [src_rel, dest_sub] of plan) {
|
|
2976
3056
|
const src = path.join(package_root, src_rel);
|
|
2977
3057
|
const dest = dest_sub ? path.join(anchor, dest_sub) : anchor;
|
|
2978
|
-
|
|
3058
|
+
// ONE filter instance, handed to BOTH the copy and the inventory.
|
|
3059
|
+
// `expected_deploy_files` is what the reaper treats as "should
|
|
3060
|
+
// exist": filtering only the copy would leave previously-installed
|
|
3061
|
+
// maintainer rules on disk forever, and filtering only the
|
|
3062
|
+
// inventory would delete rules the copy just wrote. Both, or
|
|
3063
|
+
// neither — never one.
|
|
3064
|
+
const rule_filter = _rule_filter_for_source(src_rel, rule_scope);
|
|
3065
|
+
const [w, s, paths] = _copy_dir_dereferencing_symlinks(
|
|
3066
|
+
src,
|
|
3067
|
+
dest,
|
|
3068
|
+
force,
|
|
3069
|
+
package_root,
|
|
3070
|
+
rule_filter,
|
|
3071
|
+
);
|
|
2979
3072
|
written_total += w;
|
|
2980
3073
|
skipped_total += s;
|
|
2981
3074
|
written_paths.push(...paths);
|
|
2982
3075
|
current_files = setUnion(
|
|
2983
3076
|
current_files,
|
|
2984
|
-
global_deploy_inventory.expected_deploy_files(
|
|
3077
|
+
global_deploy_inventory.expected_deploy_files(
|
|
3078
|
+
src,
|
|
3079
|
+
dest_sub ? dest_sub : '',
|
|
3080
|
+
rule_filter,
|
|
3081
|
+
),
|
|
2985
3082
|
);
|
|
2986
3083
|
}
|
|
2987
3084
|
// Flat-command → skill-wrapper re-projection for Claude Code
|
|
@@ -3091,6 +3188,9 @@ function _preview_global_reap(
|
|
|
3091
3188
|
): Record<string, string[]> {
|
|
3092
3189
|
const inventory = global_deploy_inventory.load_inventory();
|
|
3093
3190
|
const preview: Record<string, string[]> = {};
|
|
3191
|
+
// The preview must compute the SAME expected set the real deploy will, or
|
|
3192
|
+
// `--dry-run` under-reports the rules a scoped upgrade is about to reap.
|
|
3193
|
+
const rule_scope = _resolve_global_rule_scope(package_root);
|
|
3094
3194
|
for (const tool_id of [...tools].sort()) {
|
|
3095
3195
|
const plan = GLOBAL_DEPLOY_SOURCES[tool_id];
|
|
3096
3196
|
if (plan === undefined) continue;
|
|
@@ -3102,7 +3202,11 @@ function _preview_global_reap(
|
|
|
3102
3202
|
const src = path.join(package_root, src_rel);
|
|
3103
3203
|
current_files = setUnion(
|
|
3104
3204
|
current_files,
|
|
3105
|
-
global_deploy_inventory.expected_deploy_files(
|
|
3205
|
+
global_deploy_inventory.expected_deploy_files(
|
|
3206
|
+
src,
|
|
3207
|
+
dest_sub ? dest_sub : '',
|
|
3208
|
+
_rule_filter_for_source(src_rel, rule_scope),
|
|
3209
|
+
),
|
|
3106
3210
|
);
|
|
3107
3211
|
}
|
|
3108
3212
|
let would_reap: string[] = [];
|
|
@@ -3334,11 +3438,25 @@ function _compute_active_pack_ids(
|
|
|
3334
3438
|
* artefact of any kind.
|
|
3335
3439
|
*/
|
|
3336
3440
|
function _resolve_global_settings_doc(): Record<string, unknown> | null {
|
|
3441
|
+
const p = _resolve_global_settings_path();
|
|
3442
|
+
return p === null ? null : _load_yaml_doc(p);
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
/**
|
|
3446
|
+
* The path `_resolve_global_settings_doc` would read, or `null` when no global
|
|
3447
|
+
* settings artefact exists at all.
|
|
3448
|
+
*
|
|
3449
|
+
* Split out because "absent" and "present but unparseable" are different facts
|
|
3450
|
+
* and one caller must tell them apart: `_load_yaml_doc` maps BOTH to `{}`, which
|
|
3451
|
+
* silently turns a YAML typo into legacy-all rule scoping — defeating a scoping
|
|
3452
|
+
* decision the user made, with no signal. See `_resolve_global_rule_scope`.
|
|
3453
|
+
*/
|
|
3454
|
+
function _resolve_global_settings_path(): string | null {
|
|
3337
3455
|
const root = user_global_paths.event4u_root();
|
|
3338
3456
|
const canonical = path.join(root, 'settings', SETTINGS_FILE);
|
|
3339
|
-
if (pathExists(canonical)) return
|
|
3457
|
+
if (pathExists(canonical)) return canonical;
|
|
3340
3458
|
const legacy = path.join(root, SETTINGS_FILE);
|
|
3341
|
-
if (pathExists(legacy)) return
|
|
3459
|
+
if (pathExists(legacy)) return legacy;
|
|
3342
3460
|
return null;
|
|
3343
3461
|
}
|
|
3344
3462
|
|
|
@@ -3369,6 +3487,84 @@ function _resolve_scoped_projection(
|
|
|
3369
3487
|
return { mode, active_packs };
|
|
3370
3488
|
}
|
|
3371
3489
|
|
|
3490
|
+
/**
|
|
3491
|
+
* Resolve the rule scope governing THIS global deploy — the same way the wizard
|
|
3492
|
+
* does (`src/server/routes/install.ts::_resolveRuleScope`), so the two global
|
|
3493
|
+
* paths cannot ship different rule sets for the same settings.
|
|
3494
|
+
*
|
|
3495
|
+
* Settings resolution mirrors `_resolve_scoped_projection` above: an existing
|
|
3496
|
+
* global settings doc is authoritative, and only a genuinely fresh machine falls
|
|
3497
|
+
* through to the packaged template. Any read or parse failure resolves to
|
|
3498
|
+
* `LEGACY_ALL` — over-shipping is the safe direction, and the compat exclusion
|
|
3499
|
+
* (`source-of-truth.md`) still applies even then.
|
|
3500
|
+
*/
|
|
3501
|
+
function _resolve_global_rule_scope(package_root: string): RuleScope {
|
|
3502
|
+
const settings_path = _resolve_global_settings_path();
|
|
3503
|
+
|
|
3504
|
+
// No global settings artefact at all — a genuinely fresh machine. Fall
|
|
3505
|
+
// through to the packaged template silently: there is no user decision to
|
|
3506
|
+
// contradict, and this is the documented upgrade-compat path.
|
|
3507
|
+
if (settings_path === null) {
|
|
3508
|
+
try {
|
|
3509
|
+
return ruleScopeFromSettings(_load_default_settings(package_root));
|
|
3510
|
+
} catch {
|
|
3511
|
+
return LEGACY_ALL;
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
// A doc EXISTS, so the user has expressed a configuration. Failing to read
|
|
3516
|
+
// it is not the same as not having one: falling back to legacy-all here
|
|
3517
|
+
// ships the maintainer-only rules the user may have deliberately scoped out,
|
|
3518
|
+
// and `_load_yaml_doc` would report that as an indistinguishable `{}`. So
|
|
3519
|
+
// parse it explicitly and be LOUD when it does not parse — over-shipping
|
|
3520
|
+
// stays the safe direction, but it must not be a silent one.
|
|
3521
|
+
let text: string;
|
|
3522
|
+
try {
|
|
3523
|
+
text = readText(settings_path);
|
|
3524
|
+
} catch (e) {
|
|
3525
|
+
warn(
|
|
3526
|
+
`could not read ${settings_path} (${String(e)}) — rule scoping falls back ` +
|
|
3527
|
+
'to legacy-all, so ALL rules including maintainer-only ones will be ' +
|
|
3528
|
+
'installed. Fix the file to restore scoping.',
|
|
3529
|
+
);
|
|
3530
|
+
return LEGACY_ALL;
|
|
3531
|
+
}
|
|
3532
|
+
const parsed = yamlSafeLoad(text);
|
|
3533
|
+
if (!_isPlainObject(parsed)) {
|
|
3534
|
+
warn(
|
|
3535
|
+
`${settings_path} is not a YAML mapping — rule scoping falls back to ` +
|
|
3536
|
+
'legacy-all, so ALL rules including maintainer-only ones will be ' +
|
|
3537
|
+
'installed. Fix the file to restore scoping.',
|
|
3538
|
+
);
|
|
3539
|
+
return LEGACY_ALL;
|
|
3540
|
+
}
|
|
3541
|
+
try {
|
|
3542
|
+
return ruleScopeFromSettings(parsed as Record<string, unknown>);
|
|
3543
|
+
} catch (e) {
|
|
3544
|
+
warn(
|
|
3545
|
+
`could not derive rule scope from ${settings_path} (${String(e)}) — ` +
|
|
3546
|
+
'falling back to legacy-all; ALL rules will be installed.',
|
|
3547
|
+
);
|
|
3548
|
+
return LEGACY_ALL;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
/**
|
|
3553
|
+
* The rule-scope filter for one `GLOBAL_DEPLOY_SOURCES` row, or `null` when the
|
|
3554
|
+
* row is not the rules tree.
|
|
3555
|
+
*
|
|
3556
|
+
* Keyed on `src_rel === RULE_SOURCE_REL` exactly as `expandWizardSources` keys
|
|
3557
|
+
* it — the two must agree on WHICH source gets filtered, not only on the
|
|
3558
|
+
* predicate.
|
|
3559
|
+
*/
|
|
3560
|
+
function _rule_filter_for_source(
|
|
3561
|
+
src_rel: string,
|
|
3562
|
+
scope: RuleScope,
|
|
3563
|
+
): global_deploy_inventory.FileFilter | null {
|
|
3564
|
+
if (src_rel !== RULE_SOURCE_REL) return null;
|
|
3565
|
+
return (srcFile: string): boolean => ruleFileArrives(srcFile, scope);
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3372
3568
|
/**
|
|
3373
3569
|
* Prune every skill/command artefact whose `packs:` frontmatter is
|
|
3374
3570
|
* non-empty AND has no intersection with `active_ids`. Untagged artefacts
|
|
@@ -5035,5 +5231,14 @@ export {
|
|
|
5035
5231
|
_compute_active_pack_ids,
|
|
5036
5232
|
_resolve_global_settings_doc,
|
|
5037
5233
|
_resolve_scoped_projection,
|
|
5234
|
+
// road-to-consistent-rule-scoping Phase 2: the CLI global deploy path had
|
|
5235
|
+
// no direct test — the existing rule-scoping suite covers the PLAN path
|
|
5236
|
+
// (`expandWizardSources`) only, which is why the two diverged unnoticed.
|
|
5237
|
+
_deploy_global_content,
|
|
5238
|
+
// road-to-local-ci-trust: exported for the symlink-confinement tests
|
|
5239
|
+
// (PR #1076 review-gate finding).
|
|
5240
|
+
_copy_dir_dereferencing_symlinks,
|
|
5241
|
+
_preview_global_reap,
|
|
5242
|
+
_resolve_global_rule_scope,
|
|
5038
5243
|
};
|
|
5039
5244
|
export type { Options, PackRecord, DeployResult };
|