@event4u/agent-config 14.19.0 → 14.21.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +351 -156
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +10 -0
- package/dist/agent-src/commands/agent-handoff.md +42 -41
- package/dist/agent-src/commands/analyze/inbox.md +1 -1
- package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
- package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
- package/dist/agent-src/commands/analyze.md +12 -7
- package/dist/agent-src/commands/feature/plan.md +55 -16
- package/dist/agent-src/commands/feature/refactor.md +8 -3
- package/dist/agent-src/commands/humanize.md +48 -4
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +11 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
- package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
- package/dist/agent-src/rules/evaluator-independence.md +4 -4
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
- package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +2 -2
- package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/features.md +5 -0
- package/dist/agent-src/templates/roadmaps.md +29 -1
- package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/dist/cli/mcp/dispatch.js +10 -0
- package/dist/cli/mcp/stdio.js +33 -2
- package/dist/cli/mcp/telemetry.js +227 -0
- package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
- package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-2TAQ2K4V.js} +166 -1
- package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-643Z7FIC.js} +79 -12
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-J3RFNKG4.js} +41 -5
- package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
- package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
- package/dist/cli-delegate/cmd_conformance.js +88 -110
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +42 -12
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +59 -902
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +62 -31
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -5
- package/dist/hooks/dispatch.js +3801 -2803
- package/dist/install/install.mjs +396 -168
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +809 -180
- package/dist/scripts/ai_council/content_ceiling.js +63 -0
- package/dist/scripts/ai_council/seat_policy.js +106 -0
- package/dist/scripts/ai_council/transport_resolver.js +21 -0
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/naming.md +1 -1
- package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
- package/docs/guidelines/redundancy-taxonomy.md +28 -0
- package/llms.txt +14 -14
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/src/config/assurance-capability-registry.json +9 -0
- package/src/config/gate-coverage.yml +55 -0
- package/src/config/gate-violation-baselines.json +18 -6
- package/src/config/release-gate-locality.yml +4 -3
- package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
- package/src/scripts/_lib/audit_field_provenance.ts +44 -3
- package/src/scripts/_lib/blocked_by_marker.ts +189 -0
- package/src/scripts/_lib/host_capability.ts +76 -1
- package/src/scripts/_lib/install_reach_checks.ts +33 -1
- package/src/scripts/_lib/mcp_bridge.ts +222 -10
- package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
- package/src/scripts/_lib/orchestration_record.ts +23 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
- package/src/scripts/_lib/release_highlights.ts +19 -211
- package/src/scripts/_lib/release_material.ts +27 -77
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -1
- package/src/scripts/_lib/security_lint.ts +179 -7
- package/src/scripts/_lib/self_repair.ts +141 -6
- package/src/scripts/_lib/self_repair_store.ts +273 -7
- package/src/scripts/_lib/structured_ask.ts +117 -0
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- package/src/scripts/_lib/tty_prompt.ts +76 -0
- package/src/scripts/ai_council/api_transport.ts +163 -0
- package/src/scripts/ai_council/clients.ts +58 -63
- package/src/scripts/ai_council/config.ts +13 -0
- package/src/scripts/ai_council/content_ceiling.ts +79 -0
- package/src/scripts/ai_council/jury_aggregate.ts +178 -0
- package/src/scripts/ai_council/seat_policy.ts +137 -0
- package/src/scripts/ai_council/status_surface.ts +79 -0
- package/src/scripts/ai_council/transport_resolver.ts +59 -1
- package/src/scripts/ai_tells_rules.ts +446 -5
- package/src/scripts/ask_block_census.ts +499 -0
- package/src/scripts/bench_humanizer_eval.ts +264 -40
- package/src/scripts/bench_prose_tells_fp.ts +198 -0
- package/src/scripts/build_rule_trigger_matrix.ts +5 -0
- package/src/scripts/check_branch_work_committed.ts +234 -0
- package/src/scripts/check_mcp_doc_drift.ts +217 -0
- package/src/scripts/check_pack_size.ts +321 -3
- package/src/scripts/check_release_highlights.ts +0 -157
- package/src/scripts/council_cli.ts +18 -27
- package/src/scripts/detect_ai_tells.ts +142 -20
- package/src/scripts/handoff_context_hook.ts +54 -7
- package/src/scripts/harvest_reference_tokens.ts +698 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +31 -1
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
- package/src/scripts/hooks/session_eol_hook.ts +18 -7
- package/src/scripts/install-hooks.sh +52 -0
- package/src/scripts/install.ts +4 -4
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- package/src/scripts/lint_roadmap_blockers.ts +51 -1
- package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/migrate_command_suggestions.ts +1 -1
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/probe_unblocked_ask.ts +135 -2
- package/src/scripts/release.ts +50 -95
- package/src/scripts/release_drill.ts +6 -10
- package/src/scripts/release_env.ts +1 -0
- package/src/scripts/release_publication.ts +40 -31
- package/src/scripts/self_repair_cli.ts +31 -1
- package/src/scripts/self_review_gate.ts +409 -15
- package/src/scripts/skill_scout.ts +69 -0
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
// skipped by every check. Grep-auditable, scoped to the block.
|
|
14
14
|
// 2. Confidence weighting — a match in a doc / example / template / evals
|
|
15
15
|
// file scores at 0.25x; below the FAIL threshold it is a WARN.
|
|
16
|
-
// 3.
|
|
17
|
-
// anywhere in the file suppresses
|
|
16
|
+
// 3. Bound pragma — `<!-- security-lint: allow <check> "<reason>" sha256:<hex> -->`
|
|
17
|
+
// anywhere in the file suppresses ONE check for the specific evidence whose
|
|
18
|
+
// fingerprint is listed. Repeat `sha256:<hex>` once per accepted match. The
|
|
19
|
+
// unbound form (no hash) still suppresses the whole file and reports itself
|
|
20
|
+
// as a `legacy-pragma` finding, because a key lookup with no content
|
|
21
|
+
// binding accepts whatever that line is later changed to say.
|
|
18
22
|
//
|
|
19
23
|
// There is no global allowlist — that is the rejected pattern.
|
|
20
24
|
//
|
|
@@ -27,6 +31,7 @@
|
|
|
27
31
|
// - `report()` reproduces the glyphs (🔴 / ⚠️), the `(-rank, path, line)`
|
|
28
32
|
// sort, the weight note `(weight 0.25)`, and every printed string verbatim.
|
|
29
33
|
|
|
34
|
+
import { createHash } from 'node:crypto';
|
|
30
35
|
import * as fs from 'node:fs';
|
|
31
36
|
import * as path from 'node:path';
|
|
32
37
|
import { fileURLToPath } from 'node:url';
|
|
@@ -61,11 +66,18 @@ export const DEFAULT_SCAN_ROOTS: readonly string[] = [
|
|
|
61
66
|
const _EXAMPLE_PATH =
|
|
62
67
|
/(^|\/)(docs|evals|tests?|fixtures?)(\/|$)|example|template|sample|\/_template/i;
|
|
63
68
|
|
|
64
|
-
//
|
|
69
|
+
// Grammar: `<!-- security-lint: allow <check> "<reason>" [sha256:<hex>]... -->`
|
|
65
70
|
// JS `\w` is ASCII-only by default (matches Python re's `[\w-]` for the ASCII
|
|
66
71
|
// check ids used here). Global flag so finditer-style iteration works.
|
|
72
|
+
//
|
|
73
|
+
// The hash group is OPTIONAL and repeatable. Optional because the unbound form
|
|
74
|
+
// must keep working — a grammar change that invalidates every existing pragma
|
|
75
|
+
// at once turns a hardening into an outage, and the migration is what removes
|
|
76
|
+
// the unbound population, not the parser. Repeatable because one file can
|
|
77
|
+
// legitimately carry several accepted matches of the same check, and a single
|
|
78
|
+
// hash would force either a second pragma line or a whole-file fallback.
|
|
67
79
|
const _PRAGMA =
|
|
68
|
-
/<!--\s*security-lint:\s*allow\s+([A-Za-z0-9_-]+)\s+"([^"]+)"\s*-->/g;
|
|
80
|
+
/<!--\s*security-lint:\s*allow\s+([A-Za-z0-9_-]+)\s+"([^"]+)"((?:\s+sha256:[0-9a-f]{64})*)\s*-->/g;
|
|
69
81
|
|
|
70
82
|
// Python: re.compile(r"^(\s*)(`{3,}|~{3,})\s*([\w-]*)\s*$")
|
|
71
83
|
const _FENCE = /^(\s*)(`{3,}|~{3,})\s*([A-Za-z0-9_-]*)\s*$/;
|
|
@@ -136,6 +148,21 @@ export function path_weight(rel_path: string): number {
|
|
|
136
148
|
return is_example_path(rel_path) ? 0.25 : 1.0;
|
|
137
149
|
}
|
|
138
150
|
|
|
151
|
+
const PROJECTION_PREFIX = 'dist/agent-src/';
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Fold a projected path back onto the source it was copied from.
|
|
155
|
+
*
|
|
156
|
+
* `dist/agent-src/` is a byte-for-byte copy of `src/` with paths rewritten
|
|
157
|
+
* (ADR-201), so an artifact and its projection are the same artifact for the
|
|
158
|
+
* purpose of "which evidence did a human accept". Two identities would mean two
|
|
159
|
+
* fingerprints for one accepted line, and only one of them could be written
|
|
160
|
+
* into the pragma the projection copies verbatim.
|
|
161
|
+
*/
|
|
162
|
+
export function source_identity(rel: string): string {
|
|
163
|
+
return rel.startsWith(PROJECTION_PREFIX) ? `src/${rel.slice(PROJECTION_PREFIX.length)}` : rel;
|
|
164
|
+
}
|
|
165
|
+
|
|
139
166
|
/** A file pre-split into lines with a fence/pragma mask the linters reuse. */
|
|
140
167
|
export class ScannedFile {
|
|
141
168
|
path: string; // absolute or as-given path
|
|
@@ -145,6 +172,8 @@ export class ScannedFile {
|
|
|
145
172
|
in_example_fence: boolean[];
|
|
146
173
|
in_any_fence: boolean[];
|
|
147
174
|
pragmas: Record<string, string>; // check id → reason
|
|
175
|
+
/** check id → the sha256 fingerprints that pragma accepts. Empty = unbound. */
|
|
176
|
+
pragma_hashes: Record<string, readonly string[]>;
|
|
148
177
|
weight: number;
|
|
149
178
|
|
|
150
179
|
constructor(
|
|
@@ -155,6 +184,7 @@ export class ScannedFile {
|
|
|
155
184
|
in_any_fence: boolean[],
|
|
156
185
|
pragmas: Record<string, string>,
|
|
157
186
|
weight: number,
|
|
187
|
+
pragma_hashes: Record<string, readonly string[]> = {},
|
|
158
188
|
) {
|
|
159
189
|
this.path = pathArg;
|
|
160
190
|
this.rel = rel;
|
|
@@ -163,12 +193,82 @@ export class ScannedFile {
|
|
|
163
193
|
this.in_any_fence = in_any_fence;
|
|
164
194
|
this.pragmas = pragmas;
|
|
165
195
|
this.weight = weight;
|
|
196
|
+
this.pragma_hashes = pragma_hashes;
|
|
166
197
|
}
|
|
167
198
|
|
|
168
199
|
pragma_allows(check: string): boolean {
|
|
169
200
|
return Object.prototype.hasOwnProperty.call(this.pragmas, check);
|
|
170
201
|
}
|
|
171
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Which form of pragma this file carries for `check`.
|
|
205
|
+
*
|
|
206
|
+
* `absent` — no pragma; scan normally.
|
|
207
|
+
* `legacy` — a pragma with no fingerprint. Suppresses the whole file, as it
|
|
208
|
+
* always did, and says so: a key lookup accepts whatever the
|
|
209
|
+
* line is later changed to say, which is the defect being paid
|
|
210
|
+
* down, not a mode anybody should choose.
|
|
211
|
+
* `bound` — a pragma listing fingerprints. Suppresses exactly the matches
|
|
212
|
+
* whose evidence hashes to one of them; anything else survives.
|
|
213
|
+
*/
|
|
214
|
+
pragma_form(check: string): 'absent' | 'legacy' | 'bound' {
|
|
215
|
+
if (!this.pragma_allows(check)) return 'absent';
|
|
216
|
+
return (this.pragma_hashes[check] ?? []).length > 0 ? 'bound' : 'legacy';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The fingerprint one finding's evidence hashes to.
|
|
221
|
+
*
|
|
222
|
+
* WHAT IS HASHED, and each part earns its place:
|
|
223
|
+
* check id — the same line may legitimately be accepted for one check
|
|
224
|
+
* and not another.
|
|
225
|
+
* rel path — the location IDENTITY. Not the line NUMBER: an unrelated
|
|
226
|
+
* insert above would otherwise break every pragma in the
|
|
227
|
+
* file, and a suppression that breaks on unrelated edits
|
|
228
|
+
* gets deleted rather than re-derived. A `dist/agent-src/`
|
|
229
|
+
* path folds to its `src/` original first, because the
|
|
230
|
+
* projection is byte-exact by contract (ADR-201) and
|
|
231
|
+
* carries the pragma verbatim — without the fold, every
|
|
232
|
+
* migrated pragma would suppress the source and leave its
|
|
233
|
+
* own copy flagged, which is a gate that reds on a file
|
|
234
|
+
* nobody may edit.
|
|
235
|
+
* matched line, — the evidence. ASCII whitespace runs collapse so a reflow
|
|
236
|
+
* normalized does not invalidate the binding, `sha256:<hex>` tokens are
|
|
237
|
+
* removed, and NOTHING else is stripped: the zero-width
|
|
238
|
+
* characters this suite hunts are exactly what a wider
|
|
239
|
+
* normalizer would erase.
|
|
240
|
+
*
|
|
241
|
+
* WHY THE HASH TOKENS COME OUT. A pragma's own reason string can be the
|
|
242
|
+
* matched evidence — two in this tree are, because they quote the very
|
|
243
|
+
* phrases their check detects in order to explain themselves. Without this
|
|
244
|
+
* the fingerprint would have to be computed over a line that already
|
|
245
|
+
* contains it, which has no fixed point: writing the hash changes the line,
|
|
246
|
+
* which changes the hash. Stripping the tokens makes the fingerprint stable
|
|
247
|
+
* under the act of recording it, and costs nothing elsewhere — a `sha256:`
|
|
248
|
+
* token in ordinary prose is not evidence of anything a linter here checks.
|
|
249
|
+
*
|
|
250
|
+
* Deliberately NOT the whole file: incidental content elsewhere would make
|
|
251
|
+
* every unrelated edit re-fire the pragma, which is the noise that produces
|
|
252
|
+
* blanket suppressions.
|
|
253
|
+
*/
|
|
254
|
+
evidence_fingerprint(finding: Finding): string {
|
|
255
|
+
const text = finding.line > 0 ? (this.lines[finding.line - 1] ?? '') : '';
|
|
256
|
+
const normalized = text
|
|
257
|
+
.replace(/\s*sha256:[0-9a-f]{64}/g, '')
|
|
258
|
+
.replace(/[ \t]+/g, ' ')
|
|
259
|
+
.trim();
|
|
260
|
+
return createHash('sha256')
|
|
261
|
+
.update(`${finding.check}\n${source_identity(this.rel)}\n${normalized}`, 'utf-8')
|
|
262
|
+
.digest('hex');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Drop the findings this file's BOUND pragma for `check` accepts. */
|
|
266
|
+
filter_bound_pragma(check: string, findings: readonly Finding[]): Finding[] {
|
|
267
|
+
const accepted = new Set(this.pragma_hashes[check] ?? []);
|
|
268
|
+
if (accepted.size === 0) return [...findings];
|
|
269
|
+
return findings.filter((f) => !accepted.has(this.evidence_fingerprint(f)));
|
|
270
|
+
}
|
|
271
|
+
|
|
172
272
|
/** Yield [lineno, text] honouring the fence masks (mirrors iter_lines). */
|
|
173
273
|
*iter_lines(
|
|
174
274
|
opts: { skip_example_fence?: boolean; skip_any_fence?: boolean } = {},
|
|
@@ -286,11 +386,21 @@ export function scan_file(filePath: string): ScannedFile {
|
|
|
286
386
|
// Pragmas are explicit, grep-auditable opt-out markers — honour them
|
|
287
387
|
// anywhere in the file.
|
|
288
388
|
const pragmas: Record<string, string> = {};
|
|
389
|
+
const pragmaHashes: Record<string, readonly string[]> = {};
|
|
289
390
|
for (const text of lines) {
|
|
290
391
|
_PRAGMA.lastIndex = 0;
|
|
291
392
|
let m: RegExpExecArray | null;
|
|
292
393
|
while ((m = _PRAGMA.exec(text)) !== null) {
|
|
293
|
-
|
|
394
|
+
const check = m[1] as string;
|
|
395
|
+
pragmas[check] = m[2] as string;
|
|
396
|
+
const hashes = (m[3] ?? '').match(/sha256:([0-9a-f]{64})/g) ?? [];
|
|
397
|
+
// Union across repeated pragmas for one check rather than replace:
|
|
398
|
+
// two lines each accepting one match is a legitimate spelling, and
|
|
399
|
+
// a last-one-wins read would silently drop the first.
|
|
400
|
+
pragmaHashes[check] = [
|
|
401
|
+
...(pragmaHashes[check] ?? []),
|
|
402
|
+
...hashes.map((h) => h.slice('sha256:'.length)),
|
|
403
|
+
];
|
|
294
404
|
if (m.index === _PRAGMA.lastIndex) {
|
|
295
405
|
_PRAGMA.lastIndex += 1;
|
|
296
406
|
}
|
|
@@ -305,6 +415,7 @@ export function scan_file(filePath: string): ScannedFile {
|
|
|
305
415
|
in_any,
|
|
306
416
|
pragmas,
|
|
307
417
|
path_weight(rel),
|
|
418
|
+
pragmaHashes,
|
|
308
419
|
);
|
|
309
420
|
}
|
|
310
421
|
|
|
@@ -330,6 +441,7 @@ export function scan_path(filePath: string, base: string): ScannedFile {
|
|
|
330
441
|
sf.in_any_fence,
|
|
331
442
|
sf.pragmas,
|
|
332
443
|
path_weight(rel),
|
|
444
|
+
sf.pragma_hashes,
|
|
333
445
|
);
|
|
334
446
|
}
|
|
335
447
|
|
|
@@ -379,12 +491,23 @@ function _pathCmp(a: string, b: string): number {
|
|
|
379
491
|
}
|
|
380
492
|
|
|
381
493
|
/** Yield ScannedFile for every matching file under the given roots. */
|
|
494
|
+
/**
|
|
495
|
+
* Walk a corpus.
|
|
496
|
+
*
|
|
497
|
+
* `scanBase` exists for the bounded-root mode every child linter now carries
|
|
498
|
+
* (`--root DIR`): the roots stay relative, the base they resolve against moves.
|
|
499
|
+
* Absent, it is the package root and the walk is exactly what it always was —
|
|
500
|
+
* `scan_path(p, ROOT)` computes the same `rel` and the same `path_weight` as the
|
|
501
|
+
* `scan_file(p)` this used to call, so a flagless run reads the identical corpus
|
|
502
|
+
* with identical finding paths.
|
|
503
|
+
*/
|
|
382
504
|
export function* iter_corpus(
|
|
383
505
|
roots: readonly string[] = DEFAULT_SCAN_ROOTS,
|
|
384
506
|
exts: readonly string[] = ['.md'],
|
|
507
|
+
scanBase: string = ROOT,
|
|
385
508
|
): Generator<ScannedFile> {
|
|
386
509
|
for (const root of roots) {
|
|
387
|
-
const base = path.join(
|
|
510
|
+
const base = path.join(scanBase, root);
|
|
388
511
|
if (!fs.existsSync(base)) {
|
|
389
512
|
continue;
|
|
390
513
|
}
|
|
@@ -396,12 +519,61 @@ export function* iter_corpus(
|
|
|
396
519
|
continue;
|
|
397
520
|
}
|
|
398
521
|
if (st.isFile() && exts.includes(path.extname(p))) {
|
|
399
|
-
yield
|
|
522
|
+
yield scan_path(p, scanBase);
|
|
400
523
|
}
|
|
401
524
|
}
|
|
402
525
|
}
|
|
403
526
|
}
|
|
404
527
|
|
|
528
|
+
/**
|
|
529
|
+
* The prefix a child linter uses to publish what it inspected, under `--json`.
|
|
530
|
+
*
|
|
531
|
+
* WHY STDERR, AND WHY ONLY UNDER `--json`. Under `--json` a child's STDOUT is a
|
|
532
|
+
* findings array with one consumer — `lint_agent_security`, which parses it.
|
|
533
|
+
* A count printed there would corrupt the payload. Stderr is free on that path
|
|
534
|
+
* and already carries the dead-scope diagnostic, so the umbrella reads the
|
|
535
|
+
* count from the same stream it would read a failure on.
|
|
536
|
+
*
|
|
537
|
+
* The umbrella SUMS these across its five children and publishes the total as
|
|
538
|
+
* its own `scanned:` line. The sum counts artifact INSPECTIONS, not distinct
|
|
539
|
+
* artifacts: a file in more than one child's corpus is counted once per child
|
|
540
|
+
* that read it. That is deliberate and is the stronger collapse detector — a
|
|
541
|
+
* distinct-union count would stay high when one child's corpus moved, as long
|
|
542
|
+
* as another child still covered those paths, which is exactly the failure
|
|
543
|
+
* `gate-coverage.yml` exists to catch.
|
|
544
|
+
*/
|
|
545
|
+
/** The check id an unbound pragma reports itself under. */
|
|
546
|
+
export const LEGACY_PRAGMA_CHECK = 'legacy-pragma';
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* The finding an unbound pragma emits about itself.
|
|
550
|
+
*
|
|
551
|
+
* LOW, so it never blocks: the unbound form is legal and this is a
|
|
552
|
+
* migration signal, not a violation. It is a FINDING rather than a printed
|
|
553
|
+
* aside so it travels every path the linters already have — the human report,
|
|
554
|
+
* the `--json` payload the umbrella aggregates, and the SARIF file — without
|
|
555
|
+
* a second reporting channel that only one of them reads.
|
|
556
|
+
*/
|
|
557
|
+
export function legacy_pragma_finding(sf: ScannedFile, check: string): Finding {
|
|
558
|
+
return new Finding(
|
|
559
|
+
sf.rel,
|
|
560
|
+
0,
|
|
561
|
+
LEGACY_PRAGMA_CHECK,
|
|
562
|
+
'LOW',
|
|
563
|
+
`unbound \`security-lint: allow ${check}\` pragma suppresses this whole file — ` +
|
|
564
|
+
'it accepts whatever the matched text is later changed to say. Bind it with ' +
|
|
565
|
+
'`sha256:<hex>` per accepted match.',
|
|
566
|
+
sf.weight,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export const CHILD_SCANNED_PREFIX = 'scanned: ';
|
|
571
|
+
|
|
572
|
+
/** Publish a child linter's inspected count on stderr. See {@link CHILD_SCANNED_PREFIX}. */
|
|
573
|
+
export function report_child_scanned(count: number): void {
|
|
574
|
+
process.stderr.write(`${CHILD_SCANNED_PREFIX}${String(count)}\n`);
|
|
575
|
+
}
|
|
576
|
+
|
|
405
577
|
/**
|
|
406
578
|
* Print findings grouped by severity; return an exit code.
|
|
407
579
|
*
|
|
@@ -30,14 +30,27 @@ import { createHash } from 'node:crypto';
|
|
|
30
30
|
import { redact_low_impact_entry } from '../ai_council/redact_low_impact_entry.js';
|
|
31
31
|
import { is_kernel_rule } from './kernel_rules.js';
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Where a record came from.
|
|
35
|
+
*
|
|
36
|
+
* `user-reported` and `self-detected` both require something to have FIRED — a
|
|
37
|
+
* user complaint, or a deterministic detector. `model-noticed` is the third
|
|
38
|
+
* intake: a model notices an edge case in a rule, or a methodology the user
|
|
39
|
+
* just explained that no skill carries. Nothing fired, and before this source
|
|
40
|
+
* existed such an observation had no record type at all and was handed to a
|
|
41
|
+
* host-native memory tool no reader of this repository can query.
|
|
42
|
+
*
|
|
43
|
+
* It is a SOURCE and not a second store: the record shape, the fingerprint
|
|
44
|
+
* derivation, the noclobber path and the creation cap are all unchanged.
|
|
45
|
+
*/
|
|
46
|
+
export type DefectSource = 'user-reported' | 'self-detected' | 'model-noticed';
|
|
35
47
|
|
|
36
48
|
/** The defect classes a record can carry. One class = one detector. */
|
|
37
49
|
export type DefectClass =
|
|
38
50
|
| 'user-reported'
|
|
39
51
|
| 'council-availability-claim'
|
|
40
|
-
| 'language-mirror'
|
|
52
|
+
| 'language-mirror'
|
|
53
|
+
| 'model-noticed';
|
|
41
54
|
|
|
42
55
|
/**
|
|
43
56
|
* Runtime mirror of `DefectClass`, in declaration order. The upstream issue
|
|
@@ -49,6 +62,7 @@ export const DEFECT_CLASSES: readonly DefectClass[] = [
|
|
|
49
62
|
'user-reported',
|
|
50
63
|
'council-availability-claim',
|
|
51
64
|
'language-mirror',
|
|
65
|
+
'model-noticed',
|
|
52
66
|
];
|
|
53
67
|
|
|
54
68
|
/** How far a record may travel outward. */
|
|
@@ -61,14 +75,94 @@ export interface DefectFinding {
|
|
|
61
75
|
evidence: string;
|
|
62
76
|
/** One line naming what the fix should change. */
|
|
63
77
|
suggested_surface: string;
|
|
78
|
+
/**
|
|
79
|
+
* The assets this record is ABOUT, drawn from a closed vocabulary and
|
|
80
|
+
* resolved against the tree at write time (see `partitionTargets`).
|
|
81
|
+
*
|
|
82
|
+
* `suggested_surface` is free text, so no record could ever be JOINED to
|
|
83
|
+
* the asset it describes — two records about one rule's weakness were two
|
|
84
|
+
* unrelated strings. A target is machine-addressable, which is what makes
|
|
85
|
+
* the per-target occurrence count in `targetCounts` possible at all.
|
|
86
|
+
*
|
|
87
|
+
* An EMPTY list stays legal: a real observation with no identified home is
|
|
88
|
+
* information, not an error. That is deliberately different from an
|
|
89
|
+
* UNRESOLVABLE entry, which lands in `proposes` instead.
|
|
90
|
+
*/
|
|
91
|
+
target?: string[];
|
|
92
|
+
/**
|
|
93
|
+
* Target-shaped strings that do not resolve in the tree — a rule that does
|
|
94
|
+
* not exist yet, a skill the record is proposing. Kept rather than
|
|
95
|
+
* rejected, because "this should exist" is a legitimate observation; kept
|
|
96
|
+
* SEPARATE from `target`, because a silently accepted unresolvable id is
|
|
97
|
+
* worse than free text — it looks joined and joins to nothing.
|
|
98
|
+
*/
|
|
99
|
+
proposes?: string[];
|
|
64
100
|
}
|
|
65
101
|
|
|
102
|
+
/** The four asset kinds a `target` entry may address. */
|
|
103
|
+
export const TARGET_KINDS = ['rule', 'skill', 'command', 'hook'] as const;
|
|
104
|
+
export type TargetKind = (typeof TARGET_KINDS)[number];
|
|
105
|
+
|
|
106
|
+
/** `<kind>:<id>` — closed prefix, id-shaped remainder. Never free text. */
|
|
107
|
+
export const TARGET_RE = /^(rule|skill|command|hook):([a-z0-9][a-z0-9._-]*)$/;
|
|
108
|
+
|
|
109
|
+
/** Shape only — says nothing about whether the id exists. */
|
|
110
|
+
export function isWellFormedTarget(value: string): boolean {
|
|
111
|
+
return TARGET_RE.test(value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Split a well-formed target into its kind and id, or null. */
|
|
115
|
+
export function parseTarget(value: string): { kind: TargetKind; id: string } | null {
|
|
116
|
+
const m = TARGET_RE.exec(value);
|
|
117
|
+
return m === null ? null : { kind: m[1] as TargetKind, id: m[2] as string };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* What a record is doing now.
|
|
122
|
+
*
|
|
123
|
+
* The pair `open | released` could express neither a DECLINED record (which
|
|
124
|
+
* stayed `open` in the queue line for ever) nor a partially actioned one, so
|
|
125
|
+
* the only way a record left the queue was to be released. Six states, closed:
|
|
126
|
+
*
|
|
127
|
+
* - `open` — queued, nothing decided.
|
|
128
|
+
* - `candidate` — accepted as worth acting on, not yet acted on.
|
|
129
|
+
* - `actioned` — a fix or a decision landed.
|
|
130
|
+
* - `declined` — deliberately not acted on. Terminal, and NOT a failure.
|
|
131
|
+
* - `superseded` — another record covers it.
|
|
132
|
+
* - `parked` — waiting on a named condition; REQUIRES `parked_until`,
|
|
133
|
+
* because a park with no wake condition is a deferral
|
|
134
|
+
* wearing a condition's clothes.
|
|
135
|
+
*/
|
|
136
|
+
export type DefectStatus =
|
|
137
|
+
| 'open'
|
|
138
|
+
| 'candidate'
|
|
139
|
+
| 'actioned'
|
|
140
|
+
| 'declined'
|
|
141
|
+
| 'superseded'
|
|
142
|
+
| 'parked';
|
|
143
|
+
|
|
144
|
+
export const DEFECT_STATUSES: readonly DefectStatus[] = [
|
|
145
|
+
'open',
|
|
146
|
+
'candidate',
|
|
147
|
+
'actioned',
|
|
148
|
+
'declined',
|
|
149
|
+
'superseded',
|
|
150
|
+
'parked',
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
/** Statuses that keep a record in the queue line. */
|
|
154
|
+
export const ACTIVE_STATUSES: readonly DefectStatus[] = ['open', 'candidate'];
|
|
155
|
+
|
|
66
156
|
export interface DefectRecord extends DefectFinding {
|
|
67
157
|
fingerprint: string;
|
|
68
158
|
first_seen: string;
|
|
69
159
|
last_seen: string;
|
|
70
160
|
occurrences: number;
|
|
71
|
-
status:
|
|
161
|
+
status: DefectStatus;
|
|
162
|
+
/** MANDATORY when `status === 'parked'`: when the record comes back. */
|
|
163
|
+
parked_until?: string;
|
|
164
|
+
/** One line saying why the record reached a terminal status. */
|
|
165
|
+
resolution?: string;
|
|
72
166
|
/**
|
|
73
167
|
* Failed egress attempts from the most recent `release` call, sanitized at
|
|
74
168
|
* write time. Local-only bookkeeping: `renderReport` never includes them,
|
|
@@ -617,6 +711,38 @@ export function creationCapReached(
|
|
|
617
711
|
return recentCreations(existing, finding.source, now) >= NEW_RECORDS_PER_SOURCE_PER_WINDOW;
|
|
618
712
|
}
|
|
619
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Shape validation for a stored record. Returns the problems, `[]` when valid.
|
|
716
|
+
*
|
|
717
|
+
* The one obligation that is NOT a style nit: a `parked` record without a
|
|
718
|
+
* `parked_until` is a deferral nothing can wake. Everything else here is the
|
|
719
|
+
* closed-vocabulary check the free-text era could not perform.
|
|
720
|
+
*/
|
|
721
|
+
export function validateRecord(record: DefectRecord): string[] {
|
|
722
|
+
const out: string[] = [];
|
|
723
|
+
if (!(DEFECT_STATUSES as readonly string[]).includes(record.status)) {
|
|
724
|
+
out.push(`status '${String(record.status)}' is outside the closed enum`);
|
|
725
|
+
}
|
|
726
|
+
if (record.status === 'parked' && (record.parked_until ?? '').trim() === '') {
|
|
727
|
+
out.push('status is `parked` but `parked_until` is missing — a park with no wake condition is a deferral, not a park');
|
|
728
|
+
}
|
|
729
|
+
for (const value of record.target ?? []) {
|
|
730
|
+
if (!isWellFormedTarget(value)) {
|
|
731
|
+
out.push(`target '${value}' is not \`<rule|skill|command|hook>:<id>\``);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** Union of two id lists, order-stable and de-duplicated. */
|
|
738
|
+
function _mergeIds(a: readonly string[] | undefined, b: readonly string[] | undefined): string[] {
|
|
739
|
+
const out: string[] = [];
|
|
740
|
+
for (const v of [...(a ?? []), ...(b ?? [])]) {
|
|
741
|
+
if (!out.includes(v)) out.push(v);
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
745
|
+
|
|
620
746
|
export function mergeRecord(
|
|
621
747
|
existing: DefectRecord | null,
|
|
622
748
|
finding: DefectFinding,
|
|
@@ -624,13 +750,22 @@ export function mergeRecord(
|
|
|
624
750
|
): DefectRecord {
|
|
625
751
|
const fp = fingerprint(finding.defect_class, finding.evidence);
|
|
626
752
|
if (existing !== null && existing.fingerprint === fp) {
|
|
627
|
-
|
|
753
|
+
const target = _mergeIds(existing.target, finding.target);
|
|
754
|
+
const proposes = _mergeIds(existing.proposes, finding.proposes);
|
|
755
|
+
const merged: DefectRecord = {
|
|
628
756
|
...existing,
|
|
629
757
|
last_seen: now,
|
|
630
758
|
occurrences: existing.occurrences + 1,
|
|
631
|
-
// A record
|
|
759
|
+
// A record re-reported after it left the queue is open again. The
|
|
760
|
+
// widened enum does not change this: a recurrence refutes whatever
|
|
761
|
+
// disposition was taken, which is exactly `recurring-criticism`.
|
|
632
762
|
status: 'open',
|
|
763
|
+
...(target.length > 0 ? { target } : {}),
|
|
764
|
+
...(proposes.length > 0 ? { proposes } : {}),
|
|
633
765
|
};
|
|
766
|
+
// `open` carries no wake condition; a stale one would read as a park.
|
|
767
|
+
delete merged.parked_until;
|
|
768
|
+
return merged;
|
|
634
769
|
}
|
|
635
770
|
return {
|
|
636
771
|
...finding,
|