@event4u/agent-config 14.18.0 → 14.20.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 +120 -129
- package/README.md +71 -149
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +44 -43
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +37 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +14 -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/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- 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-AKJGUQNO.js → chunk-C6IPZRDA.js} +484 -353
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-NUMQHLS5.js} +304 -136
- package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
- 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_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +489 -121
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- 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 +2 -2
- 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 +34 -33
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +4455 -3025
- package/dist/install/install.mjs +975 -689
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2087 -1134
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/naming.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.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/design-antipatterns.md +87 -64
- package/docs/guidelines/redundancy-taxonomy.md +28 -0
- package/package.json +1 -1
- package/src/agent-src/README.md +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/ci-local-parity.yml +6 -0
- package/src/config/gate-coverage.yml +225 -0
- package/src/config/gate-violation-baselines.json +18 -6
- package/src/config/release-gate-locality.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/audit_field_provenance.ts +44 -3
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_capability.ts +38 -0
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- 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/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- 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 +93 -0
- package/src/scripts/_lib/release_material.ts +64 -1
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -1
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -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/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/build_rule_trigger_matrix.ts +5 -0
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +66 -9
- package/src/scripts/check_gate_coverage.ts +96 -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 +74 -1
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/conformance_scan.ts +44 -3
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +75 -1
- package/src/scripts/handoff_context_hook.ts +54 -7
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +14 -11
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/git_command_classifier.ts +156 -1
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/session_eol_hook.ts +18 -7
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install.ts +57 -86
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +24 -24
- package/src/scripts/release_publication.ts +6 -2
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/self_repair_cli.ts +31 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/skill_scout.ts +69 -0
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a release may START — the branch it may run from, and whether a dirty
|
|
3
|
+
* working tree is tolerated there.
|
|
4
|
+
*
|
|
5
|
+
* Its own module because `release.ts` sits 456 lines over the 1500-line source
|
|
6
|
+
* ratchet, so growing it is a gate failure by construction. `_lib/` is where
|
|
7
|
+
* `release_highlights.ts` and `release_material.ts` already put the predicates
|
|
8
|
+
* the pipeline and its gates share.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface PositionVerdict {
|
|
12
|
+
proceed: boolean;
|
|
13
|
+
/** Printed before the run continues. Never a refusal. */
|
|
14
|
+
notice?: string;
|
|
15
|
+
/** Printed on refusal (no trailing newline). */
|
|
16
|
+
message?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Where a release may START, resolved as a pure verdict (no I/O, so it is
|
|
21
|
+
* unit-testable — same shape as `confirmGate`).
|
|
22
|
+
*
|
|
23
|
+
* THE DEADLOCK THIS REMOVES.
|
|
24
|
+
*
|
|
25
|
+
* `guard_release_curation` refuses BETWEEN step 2 and step 3: step 1 has
|
|
26
|
+
* already created and checked out `release/{target}`, step 2 has already
|
|
27
|
+
* written the bump, the changelog section and every regenerated derived file,
|
|
28
|
+
* and nothing is committed yet. Its message says to curate the head and re-run
|
|
29
|
+
* `task release`. Measured on 14.19.0, neither re-run reached step 1:
|
|
30
|
+
*
|
|
31
|
+
* - `task release` → `release must run from 'main'` (this check)
|
|
32
|
+
* - `task release --resume` → `working tree is not clean` (this check)
|
|
33
|
+
*
|
|
34
|
+
* So the guard prescribed a remedy the preflight refused, in both spellings,
|
|
35
|
+
* and the only way through was to hand-craft the `release: X.Y.Z` commit the
|
|
36
|
+
* pipeline makes for itself one step later.
|
|
37
|
+
*
|
|
38
|
+
* `docs/release-runbook.md` already claimed this was closed on 2026-09-03 and
|
|
39
|
+
* the drill pins `plain-run-reuses-an-existing-branch` for it. Both were true
|
|
40
|
+
* of the fix that landed — in `checkout_release_branch`, which is STEP 1. The
|
|
41
|
+
* preflight runs before step 1, so from the release branch the run never
|
|
42
|
+
* arrived at the code that had been repaired. The claim was not wrong about
|
|
43
|
+
* step 1; it was wrong that step 1 was reachable from that position.
|
|
44
|
+
*
|
|
45
|
+
* Why `release/{target}` is a legitimate start, with or without `--resume`:
|
|
46
|
+
*
|
|
47
|
+
* It is the position the pipeline itself leaves behind. `--resume` remains
|
|
48
|
+
* meaningful for what it was always for — skipping a COMMIT, a PR, a tag or a
|
|
49
|
+
* Release that already exists — and the `!resume` tag-exists check in
|
|
50
|
+
* `preflight` still refuses a plain re-run of an already-tagged version.
|
|
51
|
+
*
|
|
52
|
+
* Why a dirty tree is accepted THERE and nowhere else:
|
|
53
|
+
*
|
|
54
|
+
* On `release/{target}` the dirt is this pipeline's own step-2 output, and
|
|
55
|
+
* step 3 sweeps it with `git add -A` regardless. Refusing it can therefore
|
|
56
|
+
* only ever fire against the tool's own state. On `main` the same tree is an
|
|
57
|
+
* operator's unrelated work about to be swept into a release commit, so the
|
|
58
|
+
* refusal stays exactly as it was. The accepted files are PRINTED rather than
|
|
59
|
+
* absorbed silently, and `check_release_pr_shape` still refuses anything
|
|
60
|
+
* outside the version-bump allowlist on the resulting PR — two nets under the
|
|
61
|
+
* one place this relaxes.
|
|
62
|
+
*/
|
|
63
|
+
export function preflightPosition(opts: {
|
|
64
|
+
branch: string;
|
|
65
|
+
mainBranch: string;
|
|
66
|
+
releaseBranch: string;
|
|
67
|
+
porcelain: string;
|
|
68
|
+
}): PositionVerdict {
|
|
69
|
+
const { branch, mainBranch, releaseBranch, porcelain } = opts;
|
|
70
|
+
if (branch !== mainBranch && branch !== releaseBranch) {
|
|
71
|
+
return {
|
|
72
|
+
proceed: false,
|
|
73
|
+
message:
|
|
74
|
+
`release must run from '${mainBranch}' or '${releaseBranch}', ` +
|
|
75
|
+
`currently on '${branch}'`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (!porcelain.trim()) {
|
|
79
|
+
return { proceed: true };
|
|
80
|
+
}
|
|
81
|
+
if (branch === mainBranch) {
|
|
82
|
+
return { proceed: false, message: 'working tree is not clean; commit or stash first' };
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
proceed: true,
|
|
86
|
+
notice:
|
|
87
|
+
`working tree is not clean, and on '${releaseBranch}' that is this pipeline's own ` +
|
|
88
|
+
'step-2 output — step 3 commits it with `git add -A`. Files that will be swept ' +
|
|
89
|
+
'into the release commit:\n' +
|
|
90
|
+
porcelain
|
|
91
|
+
.split('\n')
|
|
92
|
+
.filter((l) => l.trim())
|
|
93
|
+
.map((l) => ` ${l}`)
|
|
94
|
+
.join('\n'),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
export type MutationMeasure = 'exact' | 'capped_approximation';
|
|
36
36
|
|
|
37
|
+
import { appliedIds } from './audit_field_provenance.js';
|
|
37
38
|
import type { NoFreeForm } from './runtime_journal.js';
|
|
38
39
|
import { type PrivacyClass } from './privacy_class.js';
|
|
39
40
|
|
|
@@ -55,6 +56,20 @@ export interface ReviewSkippedInput {
|
|
|
55
56
|
/** Stable id (ULID, UUID, or content hash); caller supplies. */
|
|
56
57
|
id: string;
|
|
57
58
|
work_id?: string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Stable rule ids the run ACTUALLY carried. Absent means `[]`, an honest
|
|
61
|
+
* absence — this writer observes a review that did NOT happen, so it
|
|
62
|
+
* usually has no rules observation to offer in either direction.
|
|
63
|
+
*
|
|
64
|
+
* It wrote the literal `['delegation-policy']` on every line until
|
|
65
|
+
* 2026-09-07, which made the whole audit stream's most frequent "pattern" a
|
|
66
|
+
* property of the writer. See `_lib/audit_field_provenance.ts` for the
|
|
67
|
+
* measurement, and note the difference from `skills_applied` below: that
|
|
68
|
+
* field is OMITTED because audit-log-v1 lets absence mean "not recorded",
|
|
69
|
+
* while `rules_applied` is mandatory on every line and `[]` is how absence
|
|
70
|
+
* is spelled there.
|
|
71
|
+
*/
|
|
72
|
+
rules_applied?: string[] | null | undefined;
|
|
58
73
|
}
|
|
59
74
|
|
|
60
75
|
export interface BuiltReviewSkippedLine {
|
|
@@ -102,7 +117,8 @@ export function buildReviewSkippedLine(input: ReviewSkippedInput): BuiltReviewSk
|
|
|
102
117
|
risk_class: input.diff_lines >= HIGH_RISK_DIFF_LINES ? 'high' : 'medium',
|
|
103
118
|
memory: { asks: 0, hits: 0 },
|
|
104
119
|
verify: { claims: 0, first_try_passes: 0 },
|
|
105
|
-
|
|
120
|
+
// COMPUTED, never a producer constant.
|
|
121
|
+
rules_applied: appliedIds(input.rules_applied),
|
|
106
122
|
// MANDATORY on every audit line, same as the orchestration producer.
|
|
107
123
|
privacy_class: PRODUCER_PRIVACY_CLASS,
|
|
108
124
|
// `skills_applied` is OMITTED here, deliberately and not by oversight.
|
|
@@ -174,6 +174,8 @@ import type { DatabaseSync } from 'node:sqlite';
|
|
|
174
174
|
import { current_branch, git_common_dir, git_dir } from './git_common_dir.js';
|
|
175
175
|
import type { TerminalState } from './outcome_envelope.js';
|
|
176
176
|
import { readRunTerminalState, RUN_TERMINAL_STATES } from './outcome_vocabularies.js';
|
|
177
|
+
import { addColumnIfMissing } from './sqlite_schema.js';
|
|
178
|
+
import { readSurface, type Surface } from './surface.js';
|
|
177
179
|
import {
|
|
178
180
|
isSqliteAvailableSync,
|
|
179
181
|
loadSqliteSync,
|
|
@@ -408,6 +410,14 @@ export interface JournalEvent {
|
|
|
408
410
|
at: string;
|
|
409
411
|
/** Bounded identifier: the hook or command name. Never free text. */
|
|
410
412
|
capability: string;
|
|
413
|
+
/**
|
|
414
|
+
* IDE / CLI / cloud dimension of the dispatch. Closed vocabulary, and
|
|
415
|
+
* almost always `unknown`: no host observed here sends a distinguishing
|
|
416
|
+
* marker and the detector refuses to infer one. Recorded anyway because
|
|
417
|
+
* "did this come from the surface the lattice assumed" has no answer at
|
|
418
|
+
* all while the field is absent.
|
|
419
|
+
*/
|
|
420
|
+
surface: Surface;
|
|
411
421
|
/** Imported from `outcome_envelope.ts`; null until an episode terminates. */
|
|
412
422
|
terminal_state: TerminalState | null;
|
|
413
423
|
/** Repo-relative locator for the return. Never an absolute path. */
|
|
@@ -447,6 +457,7 @@ export const JOURNAL_RECORD_KEYS = Object.freeze([
|
|
|
447
457
|
'worktree_id',
|
|
448
458
|
'at',
|
|
449
459
|
'capability',
|
|
460
|
+
'surface',
|
|
450
461
|
'terminal_state',
|
|
451
462
|
'return_ref',
|
|
452
463
|
'verification_ref',
|
|
@@ -759,6 +770,7 @@ function createSchema(db: DatabaseSync): void {
|
|
|
759
770
|
worktree_id TEXT NOT NULL,
|
|
760
771
|
at TEXT NOT NULL,
|
|
761
772
|
capability TEXT NOT NULL,
|
|
773
|
+
surface TEXT NOT NULL DEFAULT 'unknown',
|
|
762
774
|
terminal_state TEXT,
|
|
763
775
|
return_ref TEXT,
|
|
764
776
|
verification_ref TEXT,
|
|
@@ -767,6 +779,9 @@ function createSchema(db: DatabaseSync): void {
|
|
|
767
779
|
amends_seq INTEGER
|
|
768
780
|
)`,
|
|
769
781
|
);
|
|
782
|
+
// A pre-existing row takes the default, which is the honest value: that
|
|
783
|
+
// dispatch was never asked what surface it came from.
|
|
784
|
+
addColumnIfMissing(db, 'journal_event', 'surface', "TEXT NOT NULL DEFAULT 'unknown'");
|
|
770
785
|
db.exec('CREATE INDEX IF NOT EXISTS idx_event_episode ON journal_event(repository_id, episode_id, seq)');
|
|
771
786
|
db.exec('CREATE INDEX IF NOT EXISTS idx_event_session ON journal_event(repository_id, session_id, seq)');
|
|
772
787
|
db.exec('CREATE INDEX IF NOT EXISTS idx_event_task ON journal_event(repository_id, task_id)');
|
|
@@ -984,6 +999,8 @@ export interface RecordEventInput {
|
|
|
984
999
|
prompt_id?: string | null;
|
|
985
1000
|
episode_id?: string | null;
|
|
986
1001
|
terminal_state?: TerminalState | null;
|
|
1002
|
+
/** Closed vocabulary; anything else (including absent) records `unknown`. */
|
|
1003
|
+
surface?: Surface | null;
|
|
987
1004
|
return_ref?: string | null;
|
|
988
1005
|
verification_ref?: string | null;
|
|
989
1006
|
consumption?: Consumption | null;
|
|
@@ -1057,6 +1074,10 @@ export function recordEvent(h: JournalHandle, input: RecordEventInput): JournalE
|
|
|
1057
1074
|
worktree_id,
|
|
1058
1075
|
at,
|
|
1059
1076
|
capability: input.capability,
|
|
1077
|
+
// Tolerant by design: a surface this build cannot place is `unknown`,
|
|
1078
|
+
// never a throw. The field answers "which surface" and must not be able
|
|
1079
|
+
// to fail a write that would otherwise have succeeded.
|
|
1080
|
+
surface: readSurface(input.surface),
|
|
1060
1081
|
terminal_state: input.terminal_state ?? null,
|
|
1061
1082
|
return_ref: requireRef('return_ref', input.return_ref),
|
|
1062
1083
|
verification_ref: requireRef('verification_ref', input.verification_ref),
|
|
@@ -1070,9 +1091,9 @@ export function recordEvent(h: JournalHandle, input: RecordEventInput): JournalE
|
|
|
1070
1091
|
`INSERT INTO journal_event
|
|
1071
1092
|
(event, episode_id, session_id, task_id, prompt_id, boundary_status,
|
|
1072
1093
|
boundary_rule_version, repository_id, worktree_id, at, capability,
|
|
1073
|
-
terminal_state, return_ref, verification_ref, consumption,
|
|
1074
|
-
amends_seq)
|
|
1075
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1094
|
+
surface, terminal_state, return_ref, verification_ref, consumption,
|
|
1095
|
+
retain_until, amends_seq)
|
|
1096
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1076
1097
|
)
|
|
1077
1098
|
.run(
|
|
1078
1099
|
row.event,
|
|
@@ -1086,6 +1107,7 @@ export function recordEvent(h: JournalHandle, input: RecordEventInput): JournalE
|
|
|
1086
1107
|
row.worktree_id,
|
|
1087
1108
|
row.at,
|
|
1088
1109
|
row.capability,
|
|
1110
|
+
row.surface,
|
|
1089
1111
|
row.terminal_state,
|
|
1090
1112
|
row.return_ref,
|
|
1091
1113
|
row.verification_ref,
|
|
@@ -1111,6 +1133,7 @@ function toEvent(r: Record<string, unknown>): JournalEvent {
|
|
|
1111
1133
|
worktree_id: String(r['worktree_id']),
|
|
1112
1134
|
at: String(r['at']),
|
|
1113
1135
|
capability: String(r['capability']),
|
|
1136
|
+
surface: readSurface(r['surface']),
|
|
1114
1137
|
// Tolerant, not a cast: a value this build cannot place reads as null.
|
|
1115
1138
|
terminal_state: readRunTerminalState(r['terminal_state']),
|
|
1116
1139
|
return_ref: (r['return_ref'] as string | null) ?? null,
|
|
@@ -28,6 +28,10 @@ import process from 'node:process';
|
|
|
28
28
|
import { parse as parseYaml } from 'yaml';
|
|
29
29
|
|
|
30
30
|
import { inheritedGitOverrides } from './repo_root.js';
|
|
31
|
+
import {
|
|
32
|
+
checkHostPermissionSettings,
|
|
33
|
+
defaultHostPermissionDeps,
|
|
34
|
+
} from './host_permission_checks.js';
|
|
31
35
|
|
|
32
36
|
/** The verdict shape `cmd_doctor` renders. Mirrors its internal `Dict` rows. */
|
|
33
37
|
export interface WiringCheck {
|
|
@@ -291,7 +295,7 @@ export function checkInheritedGitEnv(env: NodeJS.ProcessEnv = process.env): Wiri
|
|
|
291
295
|
* two spreads.
|
|
292
296
|
*/
|
|
293
297
|
/** The four ids, in render order. Spread into `cmd_doctor`'s two id registries. */
|
|
294
|
-
export const WIRING_CHECK_IDS = ['settings-resolution', 'router-artifact', 'hook-resolution', 'inherited-git-env', 'hooks-kill-switch'] as const;
|
|
298
|
+
export const WIRING_CHECK_IDS = ['settings-resolution', 'router-artifact', 'hook-resolution', 'inherited-git-env', 'hooks-kill-switch', 'host-permission-settings'] as const;
|
|
295
299
|
|
|
296
300
|
|
|
297
301
|
/**
|
|
@@ -333,6 +337,7 @@ export function checkHooksDisabled(env: NodeJS.ProcessEnv = process.env): Wiring
|
|
|
333
337
|
|
|
334
338
|
export function wiringRunners(opts: {
|
|
335
339
|
packageRoot: string;
|
|
340
|
+
projectRoot: string;
|
|
336
341
|
iterOverrides: () => Iterable<[string, unknown, string]>;
|
|
337
342
|
}): Record<string, () => WiringCheck> {
|
|
338
343
|
return {
|
|
@@ -344,6 +349,12 @@ export function wiringRunners(opts: {
|
|
|
344
349
|
'hook-resolution': () => checkHookResolution(opts.packageRoot).check,
|
|
345
350
|
'inherited-git-env': () => checkInheritedGitEnv(),
|
|
346
351
|
'hooks-kill-switch': () => checkHooksDisabled(),
|
|
352
|
+
// The host's own permission settings, which decide how often a call is
|
|
353
|
+
// confirmed. It lives with this family rather than its own because
|
|
354
|
+
// `checkHookWiring` below already reads the same `~/.claude/settings.json`
|
|
355
|
+
// for the same host — the question is the same surface, one key over.
|
|
356
|
+
'host-permission-settings': () =>
|
|
357
|
+
checkHostPermissionSettings(defaultHostPermissionDeps(opts.projectRoot)),
|
|
347
358
|
};
|
|
348
359
|
}
|
|
349
360
|
|
|
@@ -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
|
*
|