@event4u/agent-config 9.11.0 → 9.12.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 +52 -0
- 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/design-fidelity.md +1 -1
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +1 -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-tokens/scripts/tokens.ts +0 -7
- 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/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +9 -5
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +9 -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/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 +36 -14
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/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 +405 -2429
- package/dist/scripts/condense.js +6 -22
- package/dist/scripts/condense.js.map +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +14 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +9 -5
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +9 -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/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/global_deploy_inventory.ts +38 -2
- package/src/scripts/check_installer_import_purity.ts +295 -0
- package/src/scripts/condense.ts +7 -28
- package/src/scripts/install-hooks.sh +36 -4
- package/src/scripts/install.ts +210 -5
- package/src/scripts/lint_ui_stack_bundles.ts +31 -0
- 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
|
@@ -254,6 +254,13 @@ function json_string_ascii(s: string): string {
|
|
|
254
254
|
return out;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Predicate over a SOURCE file path — `true` = the file arrives in the install.
|
|
259
|
+
* Identical shape to `PlanSource.fileFilter` (`src/install/plan.ts`), so the
|
|
260
|
+
* plan path and the deploy path can share one predicate instance.
|
|
261
|
+
*/
|
|
262
|
+
export type FileFilter = (srcFile: string) => boolean;
|
|
263
|
+
|
|
257
264
|
/**
|
|
258
265
|
* Anchor-relative paths the deploy of `src` → `<anchor>/<dest_rel>` maintains
|
|
259
266
|
* — written OR skipped-as-identical.
|
|
@@ -262,8 +269,20 @@ function json_string_ascii(s: string): string {
|
|
|
262
269
|
* symlinked files resolve to file entries, symlinked subdirectories are walked
|
|
263
270
|
* through their resolved targets. `dest_rel` may be empty (`""`) for plan
|
|
264
271
|
* entries that deploy into the anchor root.
|
|
272
|
+
*
|
|
273
|
+
* `file_filter` MUST be the same predicate the copy was given. This set is what
|
|
274
|
+
* the reaper treats as "should exist"; a file the copy skipped but this set
|
|
275
|
+
* still lists is never reaped, and a file this set omits while the copy wrote it
|
|
276
|
+
* is deleted on the next deploy. Passing the filter to one and not the other is
|
|
277
|
+
* the failure mode the rule-scoping work exists to avoid — see
|
|
278
|
+
* `install._deploy_global_content`, which derives one filter and hands it to
|
|
279
|
+
* both.
|
|
265
280
|
*/
|
|
266
|
-
export function expected_deploy_files(
|
|
281
|
+
export function expected_deploy_files(
|
|
282
|
+
src: string,
|
|
283
|
+
dest_rel: string,
|
|
284
|
+
file_filter: FileFilter | null = null,
|
|
285
|
+
): Set<string> {
|
|
267
286
|
const out = new Set<string>();
|
|
268
287
|
let src_stat: fs.Stats;
|
|
269
288
|
try {
|
|
@@ -273,6 +292,9 @@ export function expected_deploy_files(src: string, dest_rel: string): Set<string
|
|
|
273
292
|
return out;
|
|
274
293
|
}
|
|
275
294
|
if (!src_stat.isDirectory()) {
|
|
295
|
+
if (file_filter !== null && !file_filter(src)) {
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
276
298
|
out.add(as_posix(dest_rel));
|
|
277
299
|
return out;
|
|
278
300
|
}
|
|
@@ -298,6 +320,12 @@ export function expected_deploy_files(src: string, dest_rel: string): Set<string
|
|
|
298
320
|
_walk(fs.realpathSync(entry), rel);
|
|
299
321
|
continue;
|
|
300
322
|
}
|
|
323
|
+
// The predicate sees the SOURCE path, never `rel`: the rule-scope
|
|
324
|
+
// predicate reads the file's own frontmatter tags, so a dest-relative
|
|
325
|
+
// path would not resolve. Same contract as the copy twin.
|
|
326
|
+
if (file_filter !== null && !file_filter(entry)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
301
329
|
out.add(as_posix(rel));
|
|
302
330
|
}
|
|
303
331
|
};
|
|
@@ -554,7 +582,15 @@ function prune_empty_dirs(prune_candidates: Set<string>, anchor_resolved: string
|
|
|
554
582
|
}
|
|
555
583
|
|
|
556
584
|
/** `anchor in node.parents` — is `anchor` a strict ancestor of `node`? */
|
|
557
|
-
|
|
585
|
+
/**
|
|
586
|
+
* Is `node` strictly inside `anchor`? Pure path arithmetic, no I/O — callers
|
|
587
|
+
* pass already-resolved (realpath'd) paths when symlink escape matters.
|
|
588
|
+
*
|
|
589
|
+
* Exported for `install._copy_dir_dereferencing_symlinks`, which must reject a
|
|
590
|
+
* symlink whose target leaves the package root. One definition rather than a
|
|
591
|
+
* second copy of the `path.relative` idiom.
|
|
592
|
+
*/
|
|
593
|
+
export function is_ancestor(anchor: string, node: string): boolean {
|
|
558
594
|
const rel = path.relative(anchor, node);
|
|
559
595
|
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
560
596
|
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Guard against a maintainer-only CLI being bundled into the consumer installer.
|
|
4
|
+
*
|
|
5
|
+
* ## The incident this exists to prevent (road-to-consistent-rule-scoping, 2026-07-31)
|
|
6
|
+
*
|
|
7
|
+
* `src/install/rule_scope.ts` imported the rule-scoping predicate from
|
|
8
|
+
* `src/scripts/condense.ts` — deliberately, so install semantics could not drift
|
|
9
|
+
* from projection semantics. Correct intent. But `condense.ts` is maintainer CLI
|
|
10
|
+
* tooling that ends in a module-level self-invoke:
|
|
11
|
+
*
|
|
12
|
+
* const isMain = _isCliEntry();
|
|
13
|
+
* if (isMain) { process.exit(main()); }
|
|
14
|
+
*
|
|
15
|
+
* `_isCliEntry()` compares `import.meta.url` against `process.argv[1]`. That is
|
|
16
|
+
* right for a file run directly — and wrong once esbuild BUNDLES the module,
|
|
17
|
+
* because `import.meta.url` then resolves to the BUNDLE's own URL, which *is*
|
|
18
|
+
* `argv[1]` when a consumer runs `node dist/install/install.mjs`. The guard
|
|
19
|
+
* fired inside the installer, `condense.main()` ran, found no maintainer source
|
|
20
|
+
* tree in a consumer checkout, printed "No source directory found" and called
|
|
21
|
+
* `process.exit(1)` — killing the install AFTER the payload had already synced.
|
|
22
|
+
*
|
|
23
|
+
* All four `smoke-public-install` legs went red. Nothing caught it earlier:
|
|
24
|
+
* `prepack-check` verifies that imports resolve to SHIPPED paths (they did),
|
|
25
|
+
* the bundle-freshness gate only byte-diffs the rebuilt output, and the
|
|
26
|
+
* behavioural smoke test only runs post-push in its own workflow.
|
|
27
|
+
*
|
|
28
|
+
* ## What this checks
|
|
29
|
+
*
|
|
30
|
+
* The transitive relative-import closure of the installer bundle entry must
|
|
31
|
+
* contain NO module-level `process.exit(` outside the entry itself.
|
|
32
|
+
*
|
|
33
|
+
* "Module-level" is decided structurally, not textually: the AST walk descends
|
|
34
|
+
* through top-level statements and nested blocks/ifs/try but STOPS at any
|
|
35
|
+
* function, method, class, arrow or accessor body. So
|
|
36
|
+
* `function main() { process.exit(1) }` is fine — it only runs when called —
|
|
37
|
+
* while `if (isMain) { process.exit(main()) }` is not, because importing the
|
|
38
|
+
* module runs it.
|
|
39
|
+
*
|
|
40
|
+
* The entry module is exempt: it IS the CLI, and its own exit is the point.
|
|
41
|
+
*
|
|
42
|
+
* ## Why this shape and not the alternatives
|
|
43
|
+
*
|
|
44
|
+
* - **Not a string scan of the built bundle.** That reports a mystery literal
|
|
45
|
+
* with no cause. This reports the import CHAIN
|
|
46
|
+
* (`install.ts → rule_scope.ts → condense.ts`), which is the actionable fact.
|
|
47
|
+
* - **Not "forbid importing condense.ts".** A denylist of one file rots the
|
|
48
|
+
* moment a second CLI module grows an entry guard. The hazard is the
|
|
49
|
+
* module-level exit, so that is what is detected.
|
|
50
|
+
* - **Runs on SOURCE, so it needs no build.** It fails in `task ci` before a
|
|
51
|
+
* push instead of on four CI legs after one.
|
|
52
|
+
*
|
|
53
|
+
* Exit 0 clean, 1 on any finding. No network, no build, no spend.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import * as fs from 'node:fs';
|
|
57
|
+
import * as path from 'node:path';
|
|
58
|
+
import { fileURLToPath } from 'node:url';
|
|
59
|
+
|
|
60
|
+
import ts from 'typescript';
|
|
61
|
+
|
|
62
|
+
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
63
|
+
const REPO_ROOT = path.resolve(_HERE, '..', '..');
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Bundle entry points that ship to consumers as a single file.
|
|
67
|
+
*
|
|
68
|
+
* Keep in sync with the `build:*bundle` scripts in package.json. Each entry is
|
|
69
|
+
* exempt from the check for its own module-level exit — it is the CLI.
|
|
70
|
+
*/
|
|
71
|
+
const BUNDLE_ENTRIES: readonly string[] = [
|
|
72
|
+
'src/scripts/install.ts', // → dist/install/install.mjs (build:install-bundle)
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
interface Finding {
|
|
76
|
+
/** The offending module, repo-relative POSIX. */
|
|
77
|
+
readonly module: string;
|
|
78
|
+
/** 1-indexed line of the offending call. */
|
|
79
|
+
readonly line: number;
|
|
80
|
+
/** How the entry reaches it, entry first. */
|
|
81
|
+
readonly chain: readonly string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function toPosix(p: string): string {
|
|
85
|
+
return p.split(path.sep).join('/');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function relToRepo(abs: string, root: string = REPO_ROOT): string {
|
|
89
|
+
return toPosix(path.relative(root, abs));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a relative import specifier to a source file on disk.
|
|
94
|
+
*
|
|
95
|
+
* The codebase imports with `.js` extensions (NodeNext) while the sources are
|
|
96
|
+
* `.ts`, so try the TypeScript twin first, then the literal path, then an
|
|
97
|
+
* `index` file. Returns `null` for anything unresolvable — a bare package
|
|
98
|
+
* specifier, a type-only path that no longer exists, or a `.json` asset.
|
|
99
|
+
*/
|
|
100
|
+
function resolveImport(fromFile: string, specifier: string): string | null {
|
|
101
|
+
if (!specifier.startsWith('.')) return null;
|
|
102
|
+
const base = path.resolve(path.dirname(fromFile), specifier);
|
|
103
|
+
const candidates = [
|
|
104
|
+
base.replace(/\.js$/, '.ts'),
|
|
105
|
+
base.replace(/\.js$/, '.tsx'),
|
|
106
|
+
base,
|
|
107
|
+
`${base}.ts`,
|
|
108
|
+
path.join(base, 'index.ts'),
|
|
109
|
+
];
|
|
110
|
+
for (const c of candidates) {
|
|
111
|
+
try {
|
|
112
|
+
if (fs.statSync(c).isFile()) return c;
|
|
113
|
+
} catch {
|
|
114
|
+
// next candidate
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Relative import specifiers declared by `source`, in source order. */
|
|
121
|
+
function importSpecifiers(source: ts.SourceFile): string[] {
|
|
122
|
+
const out: string[] = [];
|
|
123
|
+
const visit = (node: ts.Node): void => {
|
|
124
|
+
if (
|
|
125
|
+
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
126
|
+
node.moduleSpecifier !== undefined &&
|
|
127
|
+
ts.isStringLiteral(node.moduleSpecifier)
|
|
128
|
+
) {
|
|
129
|
+
out.push(node.moduleSpecifier.text);
|
|
130
|
+
} else if (
|
|
131
|
+
ts.isCallExpression(node) &&
|
|
132
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword &&
|
|
133
|
+
node.arguments.length > 0 &&
|
|
134
|
+
ts.isStringLiteral(node.arguments[0] as ts.Node)
|
|
135
|
+
) {
|
|
136
|
+
// A dynamic import() is still bundled by esbuild unless code-split,
|
|
137
|
+
// so it carries the same hazard as a static one.
|
|
138
|
+
out.push((node.arguments[0] as ts.StringLiteral).text);
|
|
139
|
+
}
|
|
140
|
+
ts.forEachChild(node, visit);
|
|
141
|
+
};
|
|
142
|
+
ts.forEachChild(source, visit);
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** True for nodes whose body only runs when called — the walk stops here. */
|
|
147
|
+
function isDeferredScope(node: ts.Node): boolean {
|
|
148
|
+
return (
|
|
149
|
+
ts.isFunctionDeclaration(node) ||
|
|
150
|
+
ts.isFunctionExpression(node) ||
|
|
151
|
+
ts.isArrowFunction(node) ||
|
|
152
|
+
ts.isMethodDeclaration(node) ||
|
|
153
|
+
ts.isConstructorDeclaration(node) ||
|
|
154
|
+
ts.isGetAccessorDeclaration(node) ||
|
|
155
|
+
ts.isSetAccessorDeclaration(node) ||
|
|
156
|
+
ts.isClassDeclaration(node) ||
|
|
157
|
+
ts.isClassExpression(node)
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Is `node` a `process.exit(...)` call? */
|
|
162
|
+
function isProcessExitCall(node: ts.Node): boolean {
|
|
163
|
+
if (!ts.isCallExpression(node)) return false;
|
|
164
|
+
const callee = node.expression;
|
|
165
|
+
if (!ts.isPropertyAccessExpression(callee)) return false;
|
|
166
|
+
return (
|
|
167
|
+
ts.isIdentifier(callee.expression) &&
|
|
168
|
+
callee.expression.text === 'process' &&
|
|
169
|
+
callee.name.text === 'exit'
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 1-indexed lines of every `process.exit(` that executes at MODULE LOAD.
|
|
175
|
+
*
|
|
176
|
+
* Descends through statement-level structure (blocks, if/else, try/catch/finally,
|
|
177
|
+
* loops, labels) and stops at any deferred scope, so a call inside a function is
|
|
178
|
+
* not reported while one inside a top-level `if` is.
|
|
179
|
+
*/
|
|
180
|
+
function moduleLevelExitLines(source: ts.SourceFile): number[] {
|
|
181
|
+
const lines: number[] = [];
|
|
182
|
+
const visit = (node: ts.Node): void => {
|
|
183
|
+
if (isDeferredScope(node)) return;
|
|
184
|
+
if (isProcessExitCall(node)) {
|
|
185
|
+
lines.push(source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1);
|
|
186
|
+
}
|
|
187
|
+
ts.forEachChild(node, visit);
|
|
188
|
+
};
|
|
189
|
+
ts.forEachChild(source, visit);
|
|
190
|
+
return lines;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parse(file: string): ts.SourceFile | null {
|
|
194
|
+
let text: string;
|
|
195
|
+
try {
|
|
196
|
+
text = fs.readFileSync(file, 'utf-8');
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return ts.createSourceFile(file, text, ts.ScriptTarget.ESNext, true);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Walk one entry's closure; return every module-level exit found below it.
|
|
205
|
+
*
|
|
206
|
+
* `root` defaults to the repo; tests pass a fixture tree so the chain-reporting
|
|
207
|
+
* itself is covered, not only the per-file detector.
|
|
208
|
+
*/
|
|
209
|
+
function auditEntry(entryRel: string, root: string = REPO_ROOT): Finding[] {
|
|
210
|
+
const entryAbs = path.join(root, entryRel);
|
|
211
|
+
if (!fs.existsSync(entryAbs)) {
|
|
212
|
+
process.stderr.write(
|
|
213
|
+
`check-installer-import-purity: entry ${entryRel} does not exist — ` +
|
|
214
|
+
'update BUNDLE_ENTRIES\n',
|
|
215
|
+
);
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
const findings: Finding[] = [];
|
|
220
|
+
const seen = new Set<string>([entryAbs]);
|
|
221
|
+
// BFS so the reported chain is a shortest path — the least confusing one.
|
|
222
|
+
const queue: Array<{ file: string; chain: string[] }> = [
|
|
223
|
+
{ file: entryAbs, chain: [entryRel] },
|
|
224
|
+
];
|
|
225
|
+
while (queue.length > 0) {
|
|
226
|
+
const { file, chain } = queue.shift() as { file: string; chain: string[] };
|
|
227
|
+
const source = parse(file);
|
|
228
|
+
if (source === null) continue;
|
|
229
|
+
|
|
230
|
+
// The entry is the CLI; its own module-level exit is intended.
|
|
231
|
+
if (file !== entryAbs) {
|
|
232
|
+
for (const line of moduleLevelExitLines(source)) {
|
|
233
|
+
findings.push({ module: relToRepo(file, root), line, chain });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const spec of importSpecifiers(source)) {
|
|
238
|
+
const target = resolveImport(file, spec);
|
|
239
|
+
if (target === null || seen.has(target)) continue;
|
|
240
|
+
seen.add(target);
|
|
241
|
+
queue.push({ file: target, chain: [...chain, relToRepo(target, root)] });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return findings;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function main(): number {
|
|
248
|
+
const all: Finding[] = [];
|
|
249
|
+
for (const entry of BUNDLE_ENTRIES) {
|
|
250
|
+
all.push(...auditEntry(entry));
|
|
251
|
+
}
|
|
252
|
+
if (all.length === 0) {
|
|
253
|
+
process.stdout.write(
|
|
254
|
+
`✅ installer import purity: ${BUNDLE_ENTRIES.length} bundle entr` +
|
|
255
|
+
`${BUNDLE_ENTRIES.length === 1 ? 'y' : 'ies'} reach no module-level ` +
|
|
256
|
+
'process.exit()\n',
|
|
257
|
+
);
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
process.stdout.write('❌ installer import purity — module-level process.exit() reachable:\n');
|
|
261
|
+
for (const f of all) {
|
|
262
|
+
process.stdout.write(`\n ${f.module}:${f.line}\n`);
|
|
263
|
+
process.stdout.write(` reached via: ${f.chain.join(' → ')}\n`);
|
|
264
|
+
}
|
|
265
|
+
process.stdout.write(
|
|
266
|
+
'\n Why this fails the build: esbuild bundles the whole closure into\n' +
|
|
267
|
+
' dist/install/install.mjs. A module-level process.exit() then runs when a\n' +
|
|
268
|
+
' CONSUMER loads the installer — and a CLI entry guard comparing\n' +
|
|
269
|
+
' import.meta.url to process.argv[1] evaluates TRUE inside a bundle, because\n' +
|
|
270
|
+
' the bundle IS argv[1].\n\n' +
|
|
271
|
+
' Fix: move the code the installer actually needs into a module with no CLI\n' +
|
|
272
|
+
' entry and no top-level exit, and let the CLI re-export it. Do NOT relax\n' +
|
|
273
|
+
' this gate — the failure it prevents kills a consumer install after the\n' +
|
|
274
|
+
' payload has already been written.\n',
|
|
275
|
+
);
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const _isCliEntry = (): boolean => {
|
|
280
|
+
if (process.argv[1] === undefined) return false;
|
|
281
|
+
try {
|
|
282
|
+
return (
|
|
283
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
284
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
285
|
+
);
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (_isCliEntry()) {
|
|
292
|
+
process.exit(main());
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export { auditEntry, moduleLevelExitLines, BUNDLE_ENTRIES };
|
package/src/scripts/condense.ts
CHANGED
|
@@ -238,6 +238,13 @@ function _stripChars(s: string, chars: string): string {
|
|
|
238
238
|
// the repo root.
|
|
239
239
|
// =============================================================================
|
|
240
240
|
|
|
241
|
+
// road-to-consistent-rule-scoping: the predicate moved to a side-effect-free
|
|
242
|
+
// module so the consumer installer can import it WITHOUT pulling this file's
|
|
243
|
+
// module-level CLI self-invoke into its bundle. Re-exported here because four
|
|
244
|
+
// in-repo consumers import it from `condense.js`.
|
|
245
|
+
export { rule_in_scope } from '../install/ruleInScope.js';
|
|
246
|
+
import { rule_in_scope } from '../install/ruleInScope.js';
|
|
247
|
+
|
|
241
248
|
const _HERE = path.dirname(fileURLToPath(import.meta.url)); // <repo>/src/scripts
|
|
242
249
|
const _DEFAULT_PROJECT_ROOT = path.resolve(_HERE, '..', '..');
|
|
243
250
|
|
|
@@ -1014,34 +1021,6 @@ function _read_rule_packs(): string[] | null {
|
|
|
1014
1021
|
* `null` (no role filtering), so every existing 2- and 3-arg call site
|
|
1015
1022
|
* compiles and behaves unchanged.
|
|
1016
1023
|
*/
|
|
1017
|
-
export function rule_in_scope(
|
|
1018
|
-
source_path: string,
|
|
1019
|
-
scope: readonly string[] | null,
|
|
1020
|
-
pack_scope: readonly string[] | null = null,
|
|
1021
|
-
role_scope: readonly string[] | null = null,
|
|
1022
|
-
): boolean {
|
|
1023
|
-
if (scope === null && pack_scope === null && role_scope === null) {
|
|
1024
|
-
return true;
|
|
1025
|
-
}
|
|
1026
|
-
const [meta] = _parse_frontmatter(_readText(source_path));
|
|
1027
|
-
if (meta['type'] === 'always' || meta['alwaysApply'] === true) {
|
|
1028
|
-
return true; // kernel always projects
|
|
1029
|
-
}
|
|
1030
|
-
const axis = (key: string, configured: readonly string[] | null): boolean => {
|
|
1031
|
-
if (configured === null) {
|
|
1032
|
-
return true;
|
|
1033
|
-
}
|
|
1034
|
-
const values = Array.isArray(meta[key])
|
|
1035
|
-
? (meta[key] as unknown[]).map((w) => String(w))
|
|
1036
|
-
: [];
|
|
1037
|
-
if (values.length === 0) {
|
|
1038
|
-
return true; // untagged → fail safe: ship it
|
|
1039
|
-
}
|
|
1040
|
-
return values.some((v) => configured.includes(v));
|
|
1041
|
-
};
|
|
1042
|
-
return axis('workspaces', scope) && axis('packs', pack_scope) && axis('roles', role_scope);
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
1024
|
/** List rule basenames under RULES_SOURCE, workspace/pack scope applied. */
|
|
1046
1025
|
function _scoped_rule_basenames(): string[] {
|
|
1047
1026
|
const scope = _read_rule_workspaces();
|
|
@@ -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
|