@event4u/agent-config 14.17.0 → 14.19.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 +207 -77
- package/README.md +72 -150
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/pr/merge.md +35 -44
- 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 +30 -3
- 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/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
- 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 +3 -1
- package/dist/agent-src/skills/design-variations/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/feature-planning/SKILL.md +34 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
- package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -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/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- 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 +29 -2
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +749 -669
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -38
- 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 +2764 -2439
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1381 -1138
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/shared/settingsConsent.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +4 -4
- package/src/config/release-gate-locality.yml +141 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- 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/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_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +265 -1
- package/src/scripts/_lib/release_material.ts +129 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/_lib/unified_diff.ts +121 -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/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 +239 -6
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +99 -14
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/code_graph/build.ts +230 -33
- package/src/scripts/code_graph/extract.ts +168 -33
- package/src/scripts/code_graph/types.ts +37 -2
- package/src/scripts/code_graph/validate.ts +12 -1
- package/src/scripts/conformance_scan.ts +45 -4
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +228 -18
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +47 -30
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
- 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/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- 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_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +6 -1
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/src/shared/settingsConsent.ts +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host_permission_checks — report the host permission settings that produce
|
|
3
|
+
* confirmation prompts, and write none of them.
|
|
4
|
+
*
|
|
5
|
+
* WHY REPORT AND NOT WRITE. Three settings decide how often a normal coding
|
|
6
|
+
* task is interrupted, and all three belong to the consumer:
|
|
7
|
+
* `permissions.deny` rules that cover reads, the working tree's absence from
|
|
8
|
+
* `permissions.additionalDirectories`, and `permissions.defaultMode`. Writing
|
|
9
|
+
* any of them would be this package deciding a consumer's permission posture
|
|
10
|
+
* from inside their repository, which is the one thing a permission surface
|
|
11
|
+
* must never do quietly. `src/templates/consumer-settings/claude-settings.json`
|
|
12
|
+
* carries `enabledPlugins` and `hooks` and zero `permissions` keys; this check
|
|
13
|
+
* exists so that stays true and the consumer still gets the answer.
|
|
14
|
+
*
|
|
15
|
+
* WHY IT NAMES ALL THREE EVEN WHEN ALL THREE ARE ABSENT. Absent is a real
|
|
16
|
+
* answer and the most common one. A check that goes silent on the default
|
|
17
|
+
* project reports "nothing found" and "I did not look" identically, which is
|
|
18
|
+
* the measured-zero-versus-not-measured failure this tree's own report
|
|
19
|
+
* contracts argue against. So each setting is named on every run, with its
|
|
20
|
+
* resolved value or the word absent.
|
|
21
|
+
*
|
|
22
|
+
* READ ONLY, AND FAILURE-TOLERANT. An unreadable or malformed settings file is
|
|
23
|
+
* reported, never repaired: a consumer's JSON is theirs, and a doctor that
|
|
24
|
+
* rewrites what it came to inspect is not a doctor.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from "node:fs";
|
|
27
|
+
import * as os from "node:os";
|
|
28
|
+
import * as path from "node:path";
|
|
29
|
+
|
|
30
|
+
import type { WiringCheck } from "./runtime_wiring_checks.js";
|
|
31
|
+
|
|
32
|
+
/** Injected so a test never reads the developer's own home directory. */
|
|
33
|
+
export interface HostPermissionDeps {
|
|
34
|
+
projectRoot: string;
|
|
35
|
+
homeDir: string;
|
|
36
|
+
readFile: (p: string) => string | null;
|
|
37
|
+
isDirectory: (p: string) => boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The permission block as the host writes it; every field optional. */
|
|
41
|
+
interface PermissionBlock {
|
|
42
|
+
allow?: unknown;
|
|
43
|
+
deny?: unknown;
|
|
44
|
+
ask?: unknown;
|
|
45
|
+
additionalDirectories?: unknown;
|
|
46
|
+
defaultMode?: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One settings layer, in the host's own precedence order. */
|
|
50
|
+
export interface SettingsLayer {
|
|
51
|
+
label: string;
|
|
52
|
+
file: string;
|
|
53
|
+
present: boolean;
|
|
54
|
+
unreadable: boolean;
|
|
55
|
+
permissions: PermissionBlock;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Tool names whose every documented effect is a read, plus the read-shaped
|
|
60
|
+
* `Bash(<cmd>:*)` heads. A deny covering one of these is the class that turns a
|
|
61
|
+
* category-A call back into a prompt, which is why it is worth naming
|
|
62
|
+
* separately from a deny on a write.
|
|
63
|
+
*/
|
|
64
|
+
const READ_SHAPED = [
|
|
65
|
+
"Read", "Glob", "Grep", "LS", "NotebookRead",
|
|
66
|
+
"Bash(cat", "Bash(ls", "Bash(head", "Bash(tail", "Bash(grep",
|
|
67
|
+
"Bash(rg", "Bash(find", "Bash(wc", "Bash(git status", "Bash(git log",
|
|
68
|
+
"Bash(git diff", "Bash(git show",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/** The four `defaultMode` values probed from Claude Code 2.1.263. */
|
|
72
|
+
const KNOWN_MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
|
|
73
|
+
|
|
74
|
+
function _strings(value: unknown): string[] {
|
|
75
|
+
return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Default deps — the only place the real filesystem is touched. */
|
|
79
|
+
export function defaultHostPermissionDeps(projectRoot: string): HostPermissionDeps {
|
|
80
|
+
return {
|
|
81
|
+
projectRoot,
|
|
82
|
+
homeDir: os.homedir(),
|
|
83
|
+
readFile: (p) => {
|
|
84
|
+
try {
|
|
85
|
+
return fs.readFileSync(p, "utf8");
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
isDirectory: (p) => {
|
|
91
|
+
try {
|
|
92
|
+
return fs.statSync(p).isDirectory();
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the three settings layers the host reads, in its precedence order.
|
|
102
|
+
*
|
|
103
|
+
* There was no resolver for this before — `security_audit_config.ts` is the
|
|
104
|
+
* only place all three paths appear together, and only as glob strings — so
|
|
105
|
+
* this is the first code in the tree that reads `permissions` at all.
|
|
106
|
+
*/
|
|
107
|
+
export function readSettingsLayers(deps: HostPermissionDeps): SettingsLayer[] {
|
|
108
|
+
const candidates: Array<[string, string]> = [
|
|
109
|
+
["user-global", path.join(deps.homeDir, ".claude", "settings.json")],
|
|
110
|
+
["project", path.join(deps.projectRoot, ".claude", "settings.json")],
|
|
111
|
+
["project-local", path.join(deps.projectRoot, ".claude", "settings.local.json")],
|
|
112
|
+
];
|
|
113
|
+
return candidates.map(([label, file]) => {
|
|
114
|
+
const raw = deps.readFile(file);
|
|
115
|
+
if (raw === null) {
|
|
116
|
+
return { label, file, present: false, unreadable: false, permissions: {} };
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
120
|
+
const perms = parsed["permissions"];
|
|
121
|
+
return {
|
|
122
|
+
label,
|
|
123
|
+
file,
|
|
124
|
+
present: true,
|
|
125
|
+
unreadable: false,
|
|
126
|
+
permissions:
|
|
127
|
+
perms !== null && typeof perms === "object" ? (perms as PermissionBlock) : {},
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return { label, file, present: true, unreadable: true, permissions: {} };
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Deny rules that cover a read, with the layer each came from. */
|
|
136
|
+
export function readDenyRules(layers: readonly SettingsLayer[]): Array<[string, string]> {
|
|
137
|
+
const hits: Array<[string, string]> = [];
|
|
138
|
+
for (const layer of layers) {
|
|
139
|
+
for (const rule of _strings(layer.permissions.deny)) {
|
|
140
|
+
if (READ_SHAPED.some((prefix) => rule.startsWith(prefix))) {
|
|
141
|
+
hits.push([layer.label, rule]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return hits;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Every `additionalDirectories` entry across the layers; the host unions them. */
|
|
149
|
+
export function additionalDirectories(layers: readonly SettingsLayer[]): string[] {
|
|
150
|
+
const seen: string[] = [];
|
|
151
|
+
for (const layer of layers) {
|
|
152
|
+
for (const dir of _strings(layer.permissions.additionalDirectories)) {
|
|
153
|
+
if (!seen.includes(dir)) seen.push(dir);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return seen;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The winning `defaultMode`, or `null` when no layer sets one. */
|
|
160
|
+
export function resolvedDefaultMode(layers: readonly SettingsLayer[]): string | null {
|
|
161
|
+
let winner: string | null = null;
|
|
162
|
+
for (const layer of layers) {
|
|
163
|
+
const mode = layer.permissions.defaultMode;
|
|
164
|
+
if (typeof mode === "string" && mode.trim()) winner = mode.trim();
|
|
165
|
+
}
|
|
166
|
+
return winner;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Is the project root a LINKED worktree?
|
|
171
|
+
*
|
|
172
|
+
* A linked worktree's `.git` is a file holding a `gitdir:` pointer, never a
|
|
173
|
+
* directory. It matters here because a worktree root is a working directory the
|
|
174
|
+
* host was not started in, which is exactly the case
|
|
175
|
+
* `additionalDirectories` exists to cover — and the case a consumer is least
|
|
176
|
+
* likely to have thought about.
|
|
177
|
+
*/
|
|
178
|
+
export function isLinkedWorktree(deps: HostPermissionDeps): boolean {
|
|
179
|
+
const dotGit = path.join(deps.projectRoot, ".git");
|
|
180
|
+
const raw = deps.readFile(dotGit);
|
|
181
|
+
if (raw === null) return false;
|
|
182
|
+
return !deps.isDirectory(dotGit) && raw.trimStart().startsWith("gitdir:");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The copyable snippet — the consumer pastes it, this package never writes it. */
|
|
186
|
+
export function permissionSnippet(projectRoot: string): string {
|
|
187
|
+
return JSON.stringify(
|
|
188
|
+
{ permissions: { additionalDirectories: [projectRoot], defaultMode: "default" } },
|
|
189
|
+
null,
|
|
190
|
+
2,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The doctor row.
|
|
196
|
+
*
|
|
197
|
+
* `warn` when something reportable was found, `ok` otherwise. Never `fail`:
|
|
198
|
+
* these are the consumer's own choices, and a doctor that fails a build over a
|
|
199
|
+
* setting it is forbidden to write would be asking for a change it will not
|
|
200
|
+
* make.
|
|
201
|
+
*/
|
|
202
|
+
export function checkHostPermissionSettings(deps: HostPermissionDeps): WiringCheck {
|
|
203
|
+
const layers = readSettingsLayers(deps);
|
|
204
|
+
const unreadable = layers.filter((l) => l.unreadable);
|
|
205
|
+
const denies = readDenyRules(layers);
|
|
206
|
+
const dirs = additionalDirectories(layers);
|
|
207
|
+
const mode = resolvedDefaultMode(layers);
|
|
208
|
+
const worktree = isLinkedWorktree(deps);
|
|
209
|
+
const rootListed = dirs.some(
|
|
210
|
+
(d) => path.resolve(d) === path.resolve(deps.projectRoot),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
lines.push(
|
|
215
|
+
denies.length === 0
|
|
216
|
+
? "read-deny rules: absent — no permissions.deny rule covers a read"
|
|
217
|
+
: `read-deny rules: ${denies.length} — ` +
|
|
218
|
+
denies.map(([layer, rule]) => `${rule} (${layer})`).join(", "),
|
|
219
|
+
);
|
|
220
|
+
lines.push(
|
|
221
|
+
rootListed
|
|
222
|
+
? `working tree in additionalDirectories: present (${deps.projectRoot})`
|
|
223
|
+
: `working tree in additionalDirectories: absent (${deps.projectRoot}` +
|
|
224
|
+
`${worktree ? ", a linked worktree" : ""})`,
|
|
225
|
+
);
|
|
226
|
+
lines.push(
|
|
227
|
+
mode === null
|
|
228
|
+
? "permissions.defaultMode: absent — the host's own built-in default applies"
|
|
229
|
+
: `permissions.defaultMode: ${mode}` +
|
|
230
|
+
(KNOWN_MODES.includes(mode) ? "" : " — not a value this tree has probed"),
|
|
231
|
+
);
|
|
232
|
+
for (const layer of unreadable) {
|
|
233
|
+
lines.push(`unreadable: ${layer.file} (${layer.label}) is not valid JSON`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// A linked worktree unlisted in additionalDirectories is the one finding
|
|
237
|
+
// worth surfacing on its own: the host was started somewhere else, so every
|
|
238
|
+
// call into this tree is a call into a directory it was not granted.
|
|
239
|
+
const reportable =
|
|
240
|
+
denies.length > 0 || unreadable.length > 0 || (worktree && !rootListed);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
id: "host-permission-settings",
|
|
244
|
+
status: reportable ? "warn" : "ok",
|
|
245
|
+
message: lines.join("; "),
|
|
246
|
+
remedy:
|
|
247
|
+
"agent-config never writes permissions — paste this into " +
|
|
248
|
+
`${path.join(deps.projectRoot, ".claude", "settings.local.json")} yourself if you want it: ` +
|
|
249
|
+
permissionSnippet(deps.projectRoot).replace(/\s+/g, " "),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - the repo generator (`condense.py generate_claude_skills`), which
|
|
12
12
|
* builds the package's own `.claude/skills/` tree, and
|
|
13
13
|
* - the consumer install finalizer
|
|
14
|
-
* (`
|
|
14
|
+
* (the installer's `finalize_claude_model_tiers`), which rewrites the
|
|
15
15
|
* installed `.claude/skills/` tree so Claude Code performs the
|
|
16
16
|
* per-turn model switch on a consumer with `model.auto_switch: auto`.
|
|
17
17
|
*
|
|
@@ -275,7 +275,7 @@ export function slot_frequency(platform: string, slot: Slot): Frequency {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
/**
|
|
278
|
-
* Platforms
|
|
278
|
+
* Platforms this package binds nothing on.
|
|
279
279
|
*
|
|
280
280
|
* Copilot declares `fallback_only: true` in the manifest — concerns route
|
|
281
281
|
* through rule-only fallback and the dispatcher no-ops. Every hook-declared
|
|
@@ -294,7 +294,7 @@ export function is_hook_capable(platform_block: Record<string, unknown>): boolea
|
|
|
294
294
|
export interface PlatformBinding {
|
|
295
295
|
/** Platform id → slot → concern ids bound in that slot. */
|
|
296
296
|
slots: Map<string, Map<Slot, string[]>>;
|
|
297
|
-
/** Platform ids
|
|
297
|
+
/** Platform ids declared `fallback_only` — this package binds nothing there. */
|
|
298
298
|
fallback_only: Set<string>;
|
|
299
299
|
}
|
|
300
300
|
|
|
@@ -25,7 +25,15 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { spawnSync } from 'node:child_process';
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
CURATED_HEAD_INSTRUCTION,
|
|
30
|
+
MIX_RESPONSE_MARKER,
|
|
31
|
+
MIX_RESPONSE_PLACEHOLDERS,
|
|
32
|
+
PROMISE_OUTCOMES,
|
|
33
|
+
PROMISE_PHRASE,
|
|
34
|
+
PROMISE_READBACK_MARKER,
|
|
35
|
+
extract_changelog_section,
|
|
36
|
+
} from './release_material.js';
|
|
29
37
|
|
|
30
38
|
/** The five curated labels, in the order an operator reads them. */
|
|
31
39
|
export const HEAD_LABELS: readonly string[] = [
|
|
@@ -398,6 +406,7 @@ export function publication_blockers(
|
|
|
398
406
|
sectionBody: string,
|
|
399
407
|
version: string,
|
|
400
408
|
where = '`main`',
|
|
409
|
+
mix: MixObligation | null = null,
|
|
401
410
|
): string[] {
|
|
402
411
|
const out: string[] = [];
|
|
403
412
|
if (sectionBody.includes(DERIVED_MARKER)) {
|
|
@@ -414,9 +423,264 @@ export function publication_blockers(
|
|
|
414
423
|
'comment line from the section and re-run.',
|
|
415
424
|
);
|
|
416
425
|
}
|
|
426
|
+
out.push(...mix_response_blockers(sectionBody, version, where, mix));
|
|
417
427
|
return out;
|
|
418
428
|
}
|
|
419
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Everything that makes a whole SECTION unpublishable — the head-level
|
|
432
|
+
* blockers above plus the two obligations that only exist for a full section.
|
|
433
|
+
*
|
|
434
|
+
* Two levels and not one, because `publication_blockers` is legitimately called
|
|
435
|
+
* with a bare curated head (the prefill tests do exactly that), and a head
|
|
436
|
+
* fragment has no tests footer by construction. Folding a section obligation
|
|
437
|
+
* into the head predicate would make those callers red on a rule that does not
|
|
438
|
+
* apply to what they passed.
|
|
439
|
+
*
|
|
440
|
+
* The TWO cheapest guard sites call this one — `guard_release_curation` (before
|
|
441
|
+
* any commit) and `guard_release_branch_push` (before any remote state). The
|
|
442
|
+
* two publication sites reached through `_refuse_unpublishable` (the annotated
|
|
443
|
+
* tag and the GitHub Release) stay on the head-level predicate: by then the
|
|
444
|
+
* section has already shipped in a merged PR, and adding a refusal there would
|
|
445
|
+
* block a resumed publication over an obligation the release cannot still act
|
|
446
|
+
* on. Stated rather than implied, because "the guard sites call this one" read
|
|
447
|
+
* as all four and is not true of any of them but the two named here.
|
|
448
|
+
*/
|
|
449
|
+
export function section_publication_blockers(
|
|
450
|
+
sectionBody: string,
|
|
451
|
+
version: string,
|
|
452
|
+
where = '`main`',
|
|
453
|
+
mix: MixObligation | null = null,
|
|
454
|
+
): string[] {
|
|
455
|
+
const out = publication_blockers(sectionBody, version, where, mix);
|
|
456
|
+
if (!TEST_FOOTER_RE.test(sectionBody)) {
|
|
457
|
+
out.push(
|
|
458
|
+
`the ${version} section carries no \`Tests: N (+M since PREV)\` footer. ` +
|
|
459
|
+
'`_render_test_trend_line` degrades to `null` when the vitest collection ' +
|
|
460
|
+
'fails, so a missing line means the collection failed during `task release`. ' +
|
|
461
|
+
'Fix the collection, then render the number and put it on the section — ' +
|
|
462
|
+
`the release commit already exists on ${where}, so amend it there. Do not ` +
|
|
463
|
+
'invent the count: `npx vitest list | wc -l` is the same probe the writer runs.',
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* The tests-footer convention, in force for every release since 9.1.0.
|
|
471
|
+
*
|
|
472
|
+
* Duplicated from `release-validation.yml`'s inline check until 2026-09-05,
|
|
473
|
+
* where it was the ONLY copy — so the absence was red at release-PR time and
|
|
474
|
+
* invisible before it. 9.8.0 shipped without the line. The regex is the same
|
|
475
|
+
* one the workflow uses; the workflow's own comment already recorded that the
|
|
476
|
+
* writer "degrades SILENTLY to null on collection failure", which is exactly a
|
|
477
|
+
* defect a local guard should see first.
|
|
478
|
+
*/
|
|
479
|
+
const TEST_FOOTER_RE = /^Tests: \d+/mu;
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* The measured governance-versus-product level, as the writer and the guards
|
|
483
|
+
* both receive it.
|
|
484
|
+
*
|
|
485
|
+
* A plain record and not the measurement itself: this module is reached by the
|
|
486
|
+
* push guard, the pre-commit ask and the CI gate, and only the callers have git
|
|
487
|
+
* to measure with. Passing the reading in keeps the predicate pure and keeps
|
|
488
|
+
* `measure_release_mix`'s git access out of a module three guards import.
|
|
489
|
+
*/
|
|
490
|
+
export interface MixObligation {
|
|
491
|
+
triggered: boolean;
|
|
492
|
+
level: string;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Minimum written characters after the marker — a bare marker is not an answer. */
|
|
496
|
+
export const MIX_RESPONSE_MIN_CHARS = 40;
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* The ONE predicate for the governance-versus-product obligation.
|
|
500
|
+
*
|
|
501
|
+
* `check_release_highlights` refuses this from the CI side and
|
|
502
|
+
* `guard_release_branch_push` from the local side, and until 2026-09-05 only
|
|
503
|
+
* the first of the two knew the obligation existed. That asymmetry is what the
|
|
504
|
+
* contract itself recorded — *"the earliest refusal for this one obligation is
|
|
505
|
+
* the PR, not the push"* (`docs/contracts/CHANGELOG-conventions.md`) — and what
|
|
506
|
+
* cost 14.17.0 a red release PR. Both sides now read this function, so a change
|
|
507
|
+
* to the obligation cannot reach one guard and miss the other.
|
|
508
|
+
*
|
|
509
|
+
* `null` means the measurement did not run (shallow clone, missing tag). That
|
|
510
|
+
* degrades to no blocker, matching the gate's documented stance: this is a
|
|
511
|
+
* governance signal, not a correctness control, and an environment fact must
|
|
512
|
+
* not block a release.
|
|
513
|
+
*/
|
|
514
|
+
export function mix_response_blockers(
|
|
515
|
+
sectionBody: string,
|
|
516
|
+
version: string,
|
|
517
|
+
where = '`main`',
|
|
518
|
+
mix: MixObligation | null = null,
|
|
519
|
+
): string[] {
|
|
520
|
+
if (!mix || !mix.triggered) {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
const idx = sectionBody.indexOf(MIX_RESPONSE_MARKER);
|
|
524
|
+
if (idx === -1) {
|
|
525
|
+
return [
|
|
526
|
+
`the ${version} section owes a governance-versus-product response ` +
|
|
527
|
+
`(${mix.level}) and carries none. Add one \`> ${MIX_RESPONSE_MARKER}\` line ` +
|
|
528
|
+
`immediately under the curated head on ${where}, naming the next cycle's ` +
|
|
529
|
+
'consumer work or a maintainer justification.',
|
|
530
|
+
];
|
|
531
|
+
}
|
|
532
|
+
const block = mix_response_block(sectionBody, idx);
|
|
533
|
+
const left = MIX_RESPONSE_PLACEHOLDERS.filter((t) => block.includes(t));
|
|
534
|
+
if (left.length > 0) {
|
|
535
|
+
return [
|
|
536
|
+
`the ${version} governance-versus-product response still carries the writer's ` +
|
|
537
|
+
`placeholder(s) (${left.map((t) => `\`${t}\``).join(', ')}) — the measured level ` +
|
|
538
|
+
'is the generator\'s, the answer is not, and `CHANGELOG.md` is published to npm. ' +
|
|
539
|
+
`Name the next cycle's consumer work (or the justification) on ${where} and re-run.`,
|
|
540
|
+
];
|
|
541
|
+
}
|
|
542
|
+
if (human_answer(block, mix.level).length < MIX_RESPONSE_MIN_CHARS) {
|
|
543
|
+
return [
|
|
544
|
+
`the ${version} governance-versus-product response carries no written answer ` +
|
|
545
|
+
`beyond the measured level (${mix.level}). The level is the generator's; the ` +
|
|
546
|
+
`response has to name the next cycle's consumer work or a maintainer ` +
|
|
547
|
+
`justification. Write it on ${where}, in the \`> \` block under the marker.`,
|
|
548
|
+
];
|
|
549
|
+
}
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* The promise the PREVIOUS release head made, or null when it made none.
|
|
555
|
+
*
|
|
556
|
+
* Scoped to that section's governance-response block, not to its whole body: a
|
|
557
|
+
* product line that happens to contain the phrase is not a promise the project
|
|
558
|
+
* made about the next cycle.
|
|
559
|
+
*/
|
|
560
|
+
export function previous_promise(changelogText: string, previousVersion: string): string | null {
|
|
561
|
+
const section = extract_changelog_section(changelogText, previousVersion);
|
|
562
|
+
if (section === null) return null;
|
|
563
|
+
const idx = section.body.indexOf(MIX_RESPONSE_MARKER);
|
|
564
|
+
if (idx === -1) return null;
|
|
565
|
+
const block = mix_response_block(section.body, idx);
|
|
566
|
+
return block.includes(PROMISE_PHRASE) ? block : null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The ONE predicate for "did this head answer the previous head's promise?".
|
|
571
|
+
*
|
|
572
|
+
* The defect: `_lib/release_material.ts` generates a `Next cycle ships …` line
|
|
573
|
+
* and nothing has ever read the previous one back, so an unmet promise can be
|
|
574
|
+
* restated indefinitely at zero cost — the debt is booked and never called. A
|
|
575
|
+
* promise nobody reads is not a commitment, it is a sentence.
|
|
576
|
+
*
|
|
577
|
+
* Three outcomes are accepted and one of them must be chosen, which is the
|
|
578
|
+
* whole mechanism: it does not require the promise to have been KEPT, and no
|
|
579
|
+
* ratio, deadline or count is enforced here. Refusing an unkept promise would
|
|
580
|
+
* make the honest answer the expensive one and teach an author to promise less
|
|
581
|
+
* rather than to answer truthfully.
|
|
582
|
+
*
|
|
583
|
+
* Length floor and placeholder scan reuse the governance-response discipline
|
|
584
|
+
* for the same reason it exists there: a bare marker, or the writer's own
|
|
585
|
+
* scaffolding, is not an answer a human wrote.
|
|
586
|
+
*/
|
|
587
|
+
export function promise_readback_blockers(
|
|
588
|
+
sectionBody: string,
|
|
589
|
+
version: string,
|
|
590
|
+
previousVersion: string,
|
|
591
|
+
promise: string | null,
|
|
592
|
+
where = '`main`',
|
|
593
|
+
): string[] {
|
|
594
|
+
if (promise === null) return [];
|
|
595
|
+
const idx = sectionBody.indexOf(PROMISE_READBACK_MARKER);
|
|
596
|
+
if (idx === -1) {
|
|
597
|
+
return [
|
|
598
|
+
`the ${version} section does not answer the ${previousVersion} head's ` +
|
|
599
|
+
`\`${PROMISE_PHRASE}\` promise. Add one \`> ${PROMISE_READBACK_MARKER}\` line ` +
|
|
600
|
+
`immediately under the curated head on ${where}, naming the promise and whether ` +
|
|
601
|
+
`it ${PROMISE_OUTCOMES.join(', ')} (the last one with its reason).`,
|
|
602
|
+
];
|
|
603
|
+
}
|
|
604
|
+
const block = mix_response_block(sectionBody, idx);
|
|
605
|
+
const left = MIX_RESPONSE_PLACEHOLDERS.filter((t) => block.includes(t));
|
|
606
|
+
if (left.length > 0) {
|
|
607
|
+
return [
|
|
608
|
+
`the ${version} previous-cycle answer still carries the writer's placeholder(s) ` +
|
|
609
|
+
`(${left.map((t) => `\`${t}\``).join(', ')}). \`CHANGELOG.md\` is published to npm. ` +
|
|
610
|
+
`Write the outcome on ${where} and re-run.`,
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
const lower = block.toLowerCase();
|
|
614
|
+
const named = PROMISE_OUTCOMES.filter((o) => lower.includes(o));
|
|
615
|
+
if (named.length === 0) {
|
|
616
|
+
return [
|
|
617
|
+
`the ${version} previous-cycle answer names no outcome. It has to say which of ` +
|
|
618
|
+
`${PROMISE_OUTCOMES.join(', ')} applies to the ${previousVersion} promise — an ` +
|
|
619
|
+
'answer that restates the promise without resolving it is the failure this reads ' +
|
|
620
|
+
'back for.',
|
|
621
|
+
];
|
|
622
|
+
}
|
|
623
|
+
if (readback_answer(block).length < MIX_RESPONSE_MIN_CHARS) {
|
|
624
|
+
return [
|
|
625
|
+
`the ${version} previous-cycle answer is shorter than a written answer ` +
|
|
626
|
+
`(${String(MIX_RESPONSE_MIN_CHARS)} characters after the marker). Name what was ` +
|
|
627
|
+
`promised and what became of it on ${where}, not the outcome word alone.`,
|
|
628
|
+
];
|
|
629
|
+
}
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** What the author wrote under the read-back marker, marker and quoting removed. */
|
|
634
|
+
export function readback_answer(block: string): string {
|
|
635
|
+
return block
|
|
636
|
+
.replace(PROMISE_READBACK_MARKER, '')
|
|
637
|
+
.replace(/^[>\s.]+|[>\s.]+$/gu, '')
|
|
638
|
+
.replace(/[>\s]+/gu, ' ')
|
|
639
|
+
.trim();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* The whole response block: the marker's line plus every blockquote line under it.
|
|
644
|
+
*
|
|
645
|
+
* Reading only the FIRST line after the marker was wrong in both directions, and
|
|
646
|
+
* both were reproduced before this was written. It **accepted** a section whose
|
|
647
|
+
* placeholder line had simply been deleted, because the machine-written level
|
|
648
|
+
* alone is 54 characters and cleared the floor — the smuggled auto-approval the
|
|
649
|
+
* placeholder exists to prevent. And it **refused** the writer's own two-line
|
|
650
|
+
* template, where the level sits on line 1 and the answer on line 2.
|
|
651
|
+
*
|
|
652
|
+
* Scoping to the block also fixes the placeholder scan, which searched the
|
|
653
|
+
* entire section body: a legitimate changelog line containing
|
|
654
|
+
* `<roadmap or issue>` blocked the release.
|
|
655
|
+
*/
|
|
656
|
+
export function mix_response_block(sectionBody: string, markerIdx: number): string {
|
|
657
|
+
const lines = sectionBody.slice(markerIdx).split('\n');
|
|
658
|
+
const out: string[] = [lines[0] ?? ''];
|
|
659
|
+
for (const line of lines.slice(1)) {
|
|
660
|
+
if (!line.trimStart().startsWith('>')) break;
|
|
661
|
+
out.push(line);
|
|
662
|
+
}
|
|
663
|
+
return out.join('\n');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* What is left of the response block once the generator's own contribution is
|
|
668
|
+
* removed — i.e. the part a human had to write.
|
|
669
|
+
*
|
|
670
|
+
* The measured level is stripped because the writer emitted it; a floor that
|
|
671
|
+
* counted it would be satisfied by the tool on the author's behalf, which is
|
|
672
|
+
* the whole failure this obligation guards against.
|
|
673
|
+
*/
|
|
674
|
+
export function human_answer(block: string, level: string): string {
|
|
675
|
+
return block
|
|
676
|
+
.replace(MIX_RESPONSE_MARKER, '')
|
|
677
|
+
.split(level)
|
|
678
|
+
.join('')
|
|
679
|
+
.replace(/^[>\s.]+|[>\s.]+$/gu, '')
|
|
680
|
+
.replace(/[>\s]+/gu, ' ')
|
|
681
|
+
.trim();
|
|
682
|
+
}
|
|
683
|
+
|
|
420
684
|
/** Labels whose curated value is still the generator's unedited draft. */
|
|
421
685
|
export function stale_draft_labels(
|
|
422
686
|
curated: Readonly<Record<string, string>>,
|