@event4u/agent-config 9.25.0 → 9.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +58 -143
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/conformance.md +21 -2
- package/dist/agent-src/commands/review/changes.md +6 -5
- package/dist/agent-src/commands/roadmap/next.md +256 -0
- package/dist/agent-src/commands/roadmap.md +12 -4
- package/dist/agent-src/contexts/execution/subagent-routing.md +4 -1
- package/dist/agent-src/rules/active-remediation.md +2 -0
- package/dist/agent-src/rules/analysis-skill-routing.md +2 -0
- package/dist/agent-src/rules/architecture.md +2 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +2 -0
- package/dist/agent-src/rules/artifact-engagement-recording.md +2 -0
- package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
- package/dist/agent-src/rules/autonomous-execution.md +2 -0
- package/dist/agent-src/rules/brand-consistency.md +2 -0
- package/dist/agent-src/rules/brand-source-of-truth.md +2 -0
- package/dist/agent-src/rules/broken-access-control.md +2 -0
- package/dist/agent-src/rules/cli-output-handling.md +2 -0
- package/dist/agent-src/rules/code-comment-discipline.md +2 -0
- package/dist/agent-src/rules/code-provenance.md +2 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +2 -0
- package/dist/agent-src/rules/commit-conventions.md +2 -0
- package/dist/agent-src/rules/communication-through-line.md +2 -0
- package/dist/agent-src/rules/content-quoting-floor.md +2 -0
- package/dist/agent-src/rules/context-hygiene.md +10 -0
- package/dist/agent-src/rules/copilot-routing.md +2 -0
- package/dist/agent-src/rules/cross-source-consistency.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
- package/dist/agent-src/rules/delegation-policy.md +5 -2
- package/dist/agent-src/rules/design-fidelity.md +19 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +120 -0
- package/dist/agent-src/rules/devcontainer-routing.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
- package/dist/agent-src/rules/docker-commands.md +2 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +2 -0
- package/dist/agent-src/rules/domain-safety-pii.md +2 -0
- package/dist/agent-src/rules/domain-safety-retention.md +2 -0
- package/dist/agent-src/rules/downstream-changes.md +2 -0
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/evaluator-independence.md +19 -3
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -0
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -0
- package/dist/agent-src/rules/fast-path-marker-visibility.md +2 -0
- package/dist/agent-src/rules/finance-safety-floor.md +2 -0
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
- package/dist/agent-src/rules/git-history-discipline.md +3 -1
- package/dist/agent-src/rules/guidelines.md +2 -0
- package/dist/agent-src/rules/history-discipline.md +2 -0
- package/dist/agent-src/rules/icon-consistency.md +2 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +2 -0
- package/dist/agent-src/rules/improve-before-implement.md +2 -0
- package/dist/agent-src/rules/invite-challenge.md +2 -0
- package/dist/agent-src/rules/laravel-routing.md +2 -0
- package/dist/agent-src/rules/laravel-translations.md +2 -0
- package/dist/agent-src/rules/legal-safety-floor.md +2 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +2 -0
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +2 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +2 -0
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +2 -0
- package/dist/agent-src/rules/media-governance-routing.md +2 -0
- package/dist/agent-src/rules/media-sync-ground-truth.md +2 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/missing-tool-handling.md +2 -0
- package/dist/agent-src/rules/model-recommendation.md +2 -0
- package/dist/agent-src/rules/no-attribution-footers.md +2 -0
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +2 -0
- package/dist/agent-src/rules/no-pr-progress-comments.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +2 -0
- package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
- package/dist/agent-src/rules/onboarding-gate.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +2 -0
- package/dist/agent-src/rules/package-ci-checks.md +2 -0
- package/dist/agent-src/rules/persona-governance.md +2 -0
- package/dist/agent-src/rules/php-coding.md +2 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +2 -0
- package/dist/agent-src/rules/preservation-guard.md +2 -0
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -0
- package/dist/agent-src/rules/question-not-instruction.md +2 -0
- package/dist/agent-src/rules/reviewer-awareness.md +2 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/role-mode-adherence.md +2 -0
- package/dist/agent-src/rules/rule-type-governance.md +2 -0
- package/dist/agent-src/rules/runtime-safety.md +2 -0
- package/dist/agent-src/rules/scale-discipline.md +2 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +2 -0
- package/dist/agent-src/rules/session-canary.md +63 -19
- package/dist/agent-src/rules/settings-ask-protocol.md +180 -0
- package/dist/agent-src/rules/size-enforcement.md +2 -0
- package/dist/agent-src/rules/skill-improvement-trigger.md +2 -0
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/slash-command-routing-policy.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +2 -0
- package/dist/agent-src/rules/source-discovery-gate.md +2 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/spreadsheet-source-quality.md +2 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
- package/dist/agent-src/rules/symfony-routing.md +2 -0
- package/dist/agent-src/rules/think-before-action.md +2 -0
- package/dist/agent-src/rules/token-budget-discipline.md +8 -3
- package/dist/agent-src/rules/token-efficiency.md +2 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
- package/dist/agent-src/rules/tool-safety.md +2 -0
- package/dist/agent-src/rules/ui-audit-gate.md +7 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
- package/dist/agent-src/rules/upstream-proposal.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +5 -2
- package/dist/agent-src/rules/user-interrupt-priority.md +2 -0
- package/dist/agent-src/skills/recursive-verification/SKILL.md +13 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-SJQMJI7M.js → chunk-3KJIMFTB.js} +1 -1
- package/dist/cli-delegate/{chunk-BSLUHV2F.js → chunk-BCPIOAGV.js} +148 -133
- package/dist/cli-delegate/cmd_conformance.js +1 -1
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +4 -1
- package/dist/cli-delegate/cmd_route_explain.js +4 -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 +191 -120
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +9 -6
- package/dist/hooks/dispatch.js +1148 -349
- package/dist/install/install.mjs +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +571 -417
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +33 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/io/yamlIO.js +85 -0
- package/dist/server/io/yamlIO.js.map +1 -1
- package/dist/server/nicknameResolver.js +116 -0
- package/dist/server/nicknameResolver.js.map +1 -0
- package/dist/server/routes/ping.js +29 -11
- package/dist/server/routes/ping.js.map +1 -1
- package/dist/server/routes/settings.js +1 -12
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/wizard.js +52 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/shared/interactiveContext.js +73 -0
- package/dist/shared/interactiveContext.js.map +1 -0
- package/dist/shared/nicknamePrefill.js +59 -0
- package/dist/shared/nicknamePrefill.js.map +1 -0
- package/dist/shared/settingsAsks.js +128 -0
- package/dist/shared/settingsAsks.js.map +1 -0
- package/dist/shared/settingsCarveOut.js +118 -0
- package/dist/shared/settingsCarveOut.js.map +1 -0
- package/dist/shared/settingsConsent.js +93 -0
- package/dist/shared/settingsConsent.js.map +1 -0
- package/dist/shared/settingsSurface.js +7 -1
- package/dist/shared/settingsSurface.js.map +1 -1
- package/dist/ui/assets/index-DmR0mbk4.js.map +1 -1
- package/package.json +1 -1
- package/src/config/agents-paths.yml +15 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +29 -1
- package/src/config/gate-violation-baselines.json +13 -7
- package/src/scripts/_cli/cmd_doctor.ts +6 -21
- package/src/scripts/_cli/cmd_route_audit.ts +4 -1
- package/src/scripts/_cli/cmd_route_explain.ts +9 -1
- package/src/scripts/_dispatch.bash +18 -0
- package/src/scripts/_lib/git_common_dir.ts +128 -0
- package/src/scripts/_lib/obligation_frequency.ts +391 -0
- package/src/scripts/_lib/session_register.ts +361 -0
- package/src/scripts/ai-video/stitch.sh +27 -9
- package/src/scripts/before_complete_hook.ts +23 -4
- package/src/scripts/bench_ab_v2_run.ts +272 -16
- package/src/scripts/bench_ab_v2_stats.ts +156 -3
- package/src/scripts/check_completion_review.ts +80 -3
- package/src/scripts/check_corpus_staleness.ts +692 -0
- package/src/scripts/check_enforcement_coverage.ts +192 -7
- package/src/scripts/check_gate_coverage.ts +67 -0
- package/src/scripts/check_hook_bundle_freshness.ts +183 -0
- package/src/scripts/check_rule_projection_integrity.ts +364 -0
- package/src/scripts/condense.ts +36 -2
- package/src/scripts/conformance_scan.ts +82 -17
- package/src/scripts/dispatch_r2_reviewer.ts +31 -2
- package/src/scripts/generate_settings_reference.ts +232 -0
- package/src/scripts/git_authorization_hook.ts +75 -10
- package/src/scripts/hook_manifest.yaml +87 -27
- package/src/scripts/hooks/block_no_verify.ts +59 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +199 -16
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/design_slop_hook.ts +13 -1
- package/src/scripts/hooks/dispatch_hook.ts +97 -2
- package/src/scripts/hooks/evidence_independence.ts +40 -6
- package/src/scripts/language_mirror_hook.ts +256 -17
- package/src/scripts/lint_abstraction_thresholds.ts +41 -8
- package/src/scripts/lint_agents_layout.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +29 -0
- package/src/scripts/memory_learn_hook.ts +109 -6
- package/src/scripts/orchestration_backfill.ts +354 -0
- package/src/scripts/pr_url_reminder_hook.ts +17 -2
- package/src/scripts/release.ts +180 -37
- package/src/scripts/schemas/review-findings.schema.json +99 -0
- package/src/scripts/schemas/rule.schema.json +5 -0
- package/src/scripts/session_canary_hook.ts +35 -3
- package/src/scripts/session_register_hook.ts +288 -0
- package/src/scripts/sessions_cli.ts +140 -0
- package/src/scripts/validate_frontmatter.ts +68 -0
- package/src/server/io/yamlIO.ts +87 -0
- package/src/shared/interactiveContext.ts +98 -0
- package/src/shared/nicknamePrefill.ts +96 -0
- package/src/shared/settingsAsks.ts +176 -0
- package/src/shared/settingsCarveOut.ts +143 -0
- package/src/shared/settingsConsent.ts +136 -0
- package/src/shared/settingsSurface.ts +7 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate the human-readable settings reference page.
|
|
4
|
+
*
|
|
5
|
+
* `road-to-zero-ceremony-settings` Phase 3 step 3. The user's global settings
|
|
6
|
+
* file is becoming sparse, so the long-form explanation that used to ride along
|
|
7
|
+
* as template comments has to survive somewhere the user can still read it.
|
|
8
|
+
*
|
|
9
|
+
* The page is derived from TWO sources that already exist, and from nothing
|
|
10
|
+
* else — the point is that there is no third surface to keep in sync:
|
|
11
|
+
*
|
|
12
|
+
* 1. the zod settings schema (`src/server/schemas/settings.ts`), flattened
|
|
13
|
+
* through the same `flattenSurface` the installer uses for its upgrade
|
|
14
|
+
* delta, so type / default / enum / description come from one place;
|
|
15
|
+
* 2. the A/B/C class table (`docs/contracts/settings-classes.md`), parsed by
|
|
16
|
+
* the same `parseSettingsClassRows` the lint and the writer use, so the
|
|
17
|
+
* page cannot claim a class the fence does not enforce.
|
|
18
|
+
*
|
|
19
|
+
* The package VERSION is deliberately absent from the output. Rendering it
|
|
20
|
+
* would make this page drift on every release bump and red `--check` for a
|
|
21
|
+
* change nobody made — the page describes the schema's shape, not the release
|
|
22
|
+
* that shipped it.
|
|
23
|
+
*
|
|
24
|
+
* Produces: docs/settings-reference.md
|
|
25
|
+
*
|
|
26
|
+
* Modes:
|
|
27
|
+
* --check Regenerate to memory and diff against the committed page.
|
|
28
|
+
* (default) Regenerate in place; exit 0 on success.
|
|
29
|
+
*
|
|
30
|
+
* Exit codes: 0 = ok, 1 = drift (--check), 2 = bad usage, 3 = internal.
|
|
31
|
+
*/
|
|
32
|
+
import * as fs from 'node:fs';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
|
+
|
|
36
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
37
|
+
|
|
38
|
+
import { settingsSchema } from '../server/schemas/settings.js';
|
|
39
|
+
import {
|
|
40
|
+
buildSettingsClassIndex,
|
|
41
|
+
parseSettingsClassRows,
|
|
42
|
+
type SettingsClass,
|
|
43
|
+
} from '../shared/settingsClasses.js';
|
|
44
|
+
import { flattenSurface, type SurfaceEntry } from '../shared/settingsSurface.js';
|
|
45
|
+
|
|
46
|
+
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
export const ROOT = path.resolve(_HERE, '..', '..');
|
|
48
|
+
|
|
49
|
+
export const CONTRACT_REL = path.join('docs', 'contracts', 'settings-classes.md');
|
|
50
|
+
export const OUTPUT_REL = path.join('docs', 'settings-reference.md');
|
|
51
|
+
|
|
52
|
+
/** What each class means for the reader. Mirrors the contract's own table. */
|
|
53
|
+
const CLASS_BLURB: Record<SettingsClass, string> = {
|
|
54
|
+
A: 'preference — the agent may set it; you are never asked',
|
|
55
|
+
B: 'consent — the agent asks once, then persists your answer',
|
|
56
|
+
C: 'guarded — only you (GUI or hand-edit) may change it',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** A key with no row in the contract. The lint fails on these separately. */
|
|
60
|
+
const UNCLASSIFIED = '—';
|
|
61
|
+
|
|
62
|
+
export interface ReferenceRow {
|
|
63
|
+
key: string;
|
|
64
|
+
cls: SettingsClass | null;
|
|
65
|
+
entry: SurfaceEntry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Group by top-level segment so the page reads like the template did.
|
|
70
|
+
* Leaves with no dot (e.g. `discipline_profile`) group under `(root)`.
|
|
71
|
+
*/
|
|
72
|
+
export function groupRows(rows: readonly ReferenceRow[]): Map<string, ReferenceRow[]> {
|
|
73
|
+
const groups = new Map<string, ReferenceRow[]>();
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
const dot = row.key.indexOf('.');
|
|
76
|
+
const section = dot === -1 ? '(root)' : row.key.slice(0, dot);
|
|
77
|
+
const bucket = groups.get(section);
|
|
78
|
+
if (bucket === undefined) groups.set(section, [row]);
|
|
79
|
+
else bucket.push(row);
|
|
80
|
+
}
|
|
81
|
+
return groups;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Render a value for a markdown table cell — never let it break the row. */
|
|
85
|
+
export function renderCell(value: unknown): string {
|
|
86
|
+
if (value === undefined) return '';
|
|
87
|
+
const json = JSON.stringify(value);
|
|
88
|
+
if (json === undefined) return '';
|
|
89
|
+
return `\`${json.replace(/\|/g, '\\|')}\``;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Descriptions are free prose; pipes and newlines would break the table. */
|
|
93
|
+
export function renderProse(value: string | undefined): string {
|
|
94
|
+
if (value === undefined || value === '') return '';
|
|
95
|
+
return value.replace(/\s+/g, ' ').replace(/\|/g, '\\|').trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildRows(contractMarkdown: string): ReferenceRow[] {
|
|
99
|
+
const jsonSchema = zodToJsonSchema(settingsSchema, {
|
|
100
|
+
name: 'AgentSettings',
|
|
101
|
+
$refStrategy: 'none',
|
|
102
|
+
target: 'jsonSchema7',
|
|
103
|
+
}) as Parameters<typeof flattenSurface>[0];
|
|
104
|
+
// The version is a required parameter of the shared flattener and is
|
|
105
|
+
// discarded here on purpose — see the module header.
|
|
106
|
+
const surface = flattenSurface(jsonSchema, 'unversioned');
|
|
107
|
+
const classIndex = buildSettingsClassIndex(parseSettingsClassRows(contractMarkdown));
|
|
108
|
+
|
|
109
|
+
return Object.keys(surface.entries)
|
|
110
|
+
.sort()
|
|
111
|
+
.map((key) => ({
|
|
112
|
+
key,
|
|
113
|
+
cls: classIndex.get(key) ?? null,
|
|
114
|
+
entry: surface.entries[key] as SurfaceEntry,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function renderPage(rows: readonly ReferenceRow[]): string {
|
|
119
|
+
const out: string[] = [];
|
|
120
|
+
out.push('# Settings reference — every key, its class, and its default');
|
|
121
|
+
out.push('');
|
|
122
|
+
out.push('> **Generated** by `src/scripts/generate_settings_reference.ts` — do NOT');
|
|
123
|
+
out.push('> hand-edit. Derived from the zod settings schema plus the A/B/C class');
|
|
124
|
+
out.push('> table in [`settings-classes`](contracts/settings-classes.md).');
|
|
125
|
+
out.push('> Drift-checked in CI (`--check`).');
|
|
126
|
+
out.push('');
|
|
127
|
+
out.push('Your global settings file is **sparse**: it records the decisions you');
|
|
128
|
+
out.push('actually made, not every key that exists. A key absent from the file');
|
|
129
|
+
out.push('resolves to the default listed here. This page is where the long-form');
|
|
130
|
+
out.push('explanation lives now that the file no longer carries it as comments.');
|
|
131
|
+
out.push('');
|
|
132
|
+
out.push('**Class** answers one question — *who may write this key.*');
|
|
133
|
+
out.push('');
|
|
134
|
+
for (const cls of ['A', 'B', 'C'] as const) {
|
|
135
|
+
out.push(`- **${cls}** — ${CLASS_BLURB[cls]}`);
|
|
136
|
+
}
|
|
137
|
+
out.push('');
|
|
138
|
+
|
|
139
|
+
const groups = groupRows(rows);
|
|
140
|
+
for (const section of [...groups.keys()].sort()) {
|
|
141
|
+
const bucket = groups.get(section) as ReferenceRow[];
|
|
142
|
+
out.push(`## ${section}`);
|
|
143
|
+
out.push('');
|
|
144
|
+
out.push('| Key | Class | Type | Default | Allowed values | What it does |');
|
|
145
|
+
out.push('|---|---|---|---|---|---|');
|
|
146
|
+
for (const row of bucket) {
|
|
147
|
+
const allowed = row.entry.enum === undefined
|
|
148
|
+
? ''
|
|
149
|
+
: row.entry.enum.map((v) => `\`${String(v)}\``).join(' · ');
|
|
150
|
+
out.push(
|
|
151
|
+
`| \`${row.key}\` | ${row.cls ?? UNCLASSIFIED} | ${row.entry.type} | `
|
|
152
|
+
+ `${renderCell(row.entry.default)} | ${allowed} | ${renderProse(row.entry.description)} |`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
out.push('');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
out.push('## See also');
|
|
159
|
+
out.push('');
|
|
160
|
+
out.push('- [`settings-classes`](contracts/settings-classes.md) — the class contract this page reads.');
|
|
161
|
+
out.push('- [`settings-api`](contracts/settings-api.md) — the read/write surface.');
|
|
162
|
+
out.push('');
|
|
163
|
+
return out.join('\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function generate(root: string): string {
|
|
167
|
+
const contract = fs.readFileSync(path.join(root, CONTRACT_REL), 'utf-8');
|
|
168
|
+
return renderPage(buildRows(contract));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface Args {
|
|
172
|
+
check: boolean;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function parse_args(argv: readonly string[]): Args {
|
|
176
|
+
const args: Args = { check: false };
|
|
177
|
+
for (const a of argv) {
|
|
178
|
+
if (a === '--check') {
|
|
179
|
+
args.check = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
process.stderr.write(`generate_settings_reference: unknown argument: ${a}\n`);
|
|
183
|
+
process.exit(2);
|
|
184
|
+
}
|
|
185
|
+
return args;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function main(argv?: readonly string[]): number {
|
|
189
|
+
const args = parse_args(argv ?? process.argv.slice(2));
|
|
190
|
+
const out_path = path.join(ROOT, OUTPUT_REL);
|
|
191
|
+
|
|
192
|
+
let rendered: string;
|
|
193
|
+
try {
|
|
194
|
+
rendered = generate(ROOT);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
process.stderr.write(`generate_settings_reference: ${String(err)}\n`);
|
|
197
|
+
return 3;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (args.check) {
|
|
201
|
+
let committed: string | null = null;
|
|
202
|
+
try {
|
|
203
|
+
committed = fs.readFileSync(out_path, 'utf-8');
|
|
204
|
+
} catch {
|
|
205
|
+
committed = null;
|
|
206
|
+
}
|
|
207
|
+
if (committed !== rendered) {
|
|
208
|
+
process.stderr.write(
|
|
209
|
+
`❌ ${OUTPUT_REL} is out of date.\n`
|
|
210
|
+
+ ' Regenerate: ./scripts-run src/scripts/generate_settings_reference\n',
|
|
211
|
+
);
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
process.stdout.write(`✅ ${OUTPUT_REL} is up to date.\n`);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fs.mkdirSync(path.dirname(out_path), { recursive: true });
|
|
219
|
+
fs.writeFileSync(out_path, rendered, 'utf-8');
|
|
220
|
+
process.stdout.write(`✅ wrote ${OUTPUT_REL}\n`);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function _invoked_directly(): boolean {
|
|
225
|
+
if (process.argv[1] === undefined) return false;
|
|
226
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
227
|
+
return import.meta.url === argvUrl;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (_invoked_directly()) {
|
|
231
|
+
process.exitCode = main();
|
|
232
|
+
}
|
|
@@ -83,16 +83,65 @@ export const ALL_OPS: readonly GitOp[] = [
|
|
|
83
83
|
* cost latency on every turn and be unauditable.
|
|
84
84
|
*/
|
|
85
85
|
const PHRASES: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
86
|
-
{ op: "commit", re: /\b(commit(e|et|ten
|
|
87
|
-
{ op: "push", re: /\b(push(e|en|
|
|
88
|
-
{ op: "branch", re: /\b(branch
|
|
89
|
-
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
{ op: "commit", re: /\b(commit(e|et|te|ten)?|committe|einchecken)\b/i },
|
|
87
|
+
{ op: "push", re: /\b(push(e|en|t)?|hochladen|hochschieben|raufschieben)\b/i },
|
|
88
|
+
{ op: "branch", re: /\b(branch|feature-branch|zweig)\b/i },
|
|
89
|
+
// A creation verb is required — "schau dir den PR an" is not "open a PR".
|
|
90
|
+
{
|
|
91
|
+
op: "pr-create",
|
|
92
|
+
re: /\b(erstell(e|en)?|mach(e|en)?|leg(e|en)?\s+an|(er)?(ö|oe)ffne|open|create|raise|aufmachen)\b[^.\n]{0,30}\b(pr|pull[- ]request)\b|\b(pr|pull[- ]request)\b[^.\n]{0,20}\b(erstellen|aufmachen|anlegen|(er)?(ö|oe)ffnen)\b/i,
|
|
93
|
+
},
|
|
94
|
+
// `merge` as an ACTION, never as the noun in "merge conflict" / "merge commit".
|
|
95
|
+
{ op: "pr-merge", re: /\b(merge|merg(e|en|st|t)|zusammenf(ü|ue)hren|reinmergen)\b(?!\s*[- ]?(conflict|konflikt|commit|base|queue|state|status))/i },
|
|
96
|
+
// A tag is an ACTION here — bare "Tag" is the German word for day, and
|
|
97
|
+
// "Version" is an ordinary noun. Both authorized a BLOCK op before this.
|
|
98
|
+
{ op: "tag", re: /\b(tagge(n|st)?|tag\s+(setzen|anlegen|erstellen)|git\s+tag|--tags|--follow-tags)\b/i },
|
|
99
|
+
// "die release notes sind falsch" is a noun phrase, not an authorization.
|
|
100
|
+
{
|
|
101
|
+
op: "release",
|
|
102
|
+
re: /\b(releasen?|ver(ö|oe)ffentlich(e|en))\b(?!\s*[- ]?(notes?|branch|candidate|pr\b|datum|date|zweig))/i,
|
|
103
|
+
},
|
|
104
|
+
{ op: "publish", re: /\b(publish(e|en)?|publiziere[n]?)\b/i },
|
|
94
105
|
];
|
|
95
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Lines that are pasted TOOL OUTPUT rather than the user instructing.
|
|
109
|
+
*
|
|
110
|
+
* Round-2 adversarial review: an UNFENCED paste of a `git push … rejected`
|
|
111
|
+
* trace authorized `push` through the prose matcher — i.e. the gate
|
|
112
|
+
* pre-authorized the exact scenario it was built to stop. Fenced pastes were
|
|
113
|
+
* already handled; prose was not.
|
|
114
|
+
*/
|
|
115
|
+
const OUTPUT_LINE =
|
|
116
|
+
/^\s*(To\s+\S+|remote:|error:|fatal:|hint:|warning:|!\s|\?\?\s|\s*\^|[-+]{3}\s|@@\s|\$\s|>\s|\d+\s+(pass|fail)|npm ERR!|Error:|Traceback|at\s+\S+:\d+)/i;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A prompt that ASKS about an operation does not authorize it.
|
|
120
|
+
*
|
|
121
|
+
* `question-not-instruction` states this for the agent; the ledger needs it
|
|
122
|
+
* too. "was macht npm publish eigentlich genau?" authorized a real publish
|
|
123
|
+
* before this check.
|
|
124
|
+
*/
|
|
125
|
+
export function isInterrogative(prose: string): boolean {
|
|
126
|
+
const t = prose.trim();
|
|
127
|
+
if (!t) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const hasImperative =
|
|
131
|
+
/\b(mach|mache|bitte|leg|lege|erstell|erstelle|f(ü|ue)hr|f(ü|ue)hre|setz|setze|starte|los|jetzt|go ahead|do it|ja[,.]?\s|ok[,.]?\s)\b/i.test(
|
|
132
|
+
t,
|
|
133
|
+
);
|
|
134
|
+
if (hasImperative) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return (
|
|
138
|
+
/\?\s*$/.test(t) ||
|
|
139
|
+
/^(was|wie|warum|wieso|weshalb|wann|wer|welche[rs]?|wo|ist|sind|kann|kannst|k(ö|oe)nnen|soll|sollen|darf|d(ü|ue)rfen|why|what|how|when|which|who|is|are|can|could|should|does|do)\b/i.test(
|
|
140
|
+
t,
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
96
145
|
/** Executable commands a user may paste, mapped to the op they authorize. */
|
|
97
146
|
const PASTED_COMMANDS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
98
147
|
{ op: "publish", re: /\bnpm\s+publish\b/i },
|
|
@@ -155,8 +204,15 @@ export function classifyAuthorization(prompt: string): {
|
|
|
155
204
|
const authorized = new Set<GitOp>();
|
|
156
205
|
const evidence: Record<string, string> = {};
|
|
157
206
|
|
|
158
|
-
|
|
159
|
-
|
|
207
|
+
// Drop pasted tool output from the prose before matching (C4), then refuse
|
|
208
|
+
// to read a question as an instruction (C3).
|
|
209
|
+
const instruction = prose
|
|
210
|
+
.split("\n")
|
|
211
|
+
.filter((l) => !OUTPUT_LINE.test(l))
|
|
212
|
+
.join("\n");
|
|
213
|
+
|
|
214
|
+
for (const { op, re } of isInterrogative(instruction) ? [] : PHRASES) {
|
|
215
|
+
const m = re.exec(instruction);
|
|
160
216
|
if (m) {
|
|
161
217
|
authorized.add(op);
|
|
162
218
|
evidence[op] = `prose: "${m[0]}"`;
|
|
@@ -164,6 +220,15 @@ export function classifyAuthorization(prompt: string): {
|
|
|
164
220
|
}
|
|
165
221
|
|
|
166
222
|
for (const fence of fences) {
|
|
223
|
+
// A fence that also carries tool OUTPUT is a transcript of something that
|
|
224
|
+
// already happened, not a command the user is handing over. Pasting
|
|
225
|
+
// $ git push origin main
|
|
226
|
+
// ! [rejected] …
|
|
227
|
+
// is showing a failure, and reading it as "authorized push" is exactly the
|
|
228
|
+
// implicit-continuation misread this gate exists to stop.
|
|
229
|
+
if (fence.split("\n").some((l) => OUTPUT_LINE.test(l))) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
167
232
|
for (const line of _commandLines(fence)) {
|
|
168
233
|
for (const { op, re } of PASTED_COMMANDS) {
|
|
169
234
|
if (re.test(line)) {
|
|
@@ -163,6 +163,25 @@ concerns:
|
|
|
163
163
|
args: []
|
|
164
164
|
fail_closed: false
|
|
165
165
|
severity: advisory
|
|
166
|
+
# Per-concern tool filter — the dispatcher skips this concern in-process on
|
|
167
|
+
# any other tool. See `_concern_matches_tool` in hooks/dispatch_hook.ts for
|
|
168
|
+
# the semantics (absent = every event, "*" = the same, non-tool events are
|
|
169
|
+
# never filtered) and for why the filter is NOT a host `matcher`.
|
|
170
|
+
#
|
|
171
|
+
# This set is not a guess: the hook's entire branch surface is
|
|
172
|
+
# `name === 'Grep' || name === 'Glob'` and `name === 'Read'`
|
|
173
|
+
# (code_graph_nudge_hook.ts), and a test pins the manifest list against it.
|
|
174
|
+
#
|
|
175
|
+
# Deliberately NOT declared on the three blocking PreToolUse guards
|
|
176
|
+
# (block-no-verify, block-kernel-rule-writes, block-config-weakening): their
|
|
177
|
+
# tool sets span host-specific naming variants (`Bash` / `BashTool` /
|
|
178
|
+
# `launch-process` / `launch_process`, `str-replace-editor` /
|
|
179
|
+
# `str_replace_editor` / `save-file` / …), so a list that misses one variant
|
|
180
|
+
# silently disables a security guard on that host. The concerns already
|
|
181
|
+
# re-read `tool_name` and return early; paying one dispatch to keep a guard
|
|
182
|
+
# correct is the right trade, and the cheap filter is reserved for advisory
|
|
183
|
+
# concerns whose set is provable from their own source.
|
|
184
|
+
tools: [Grep, Glob, Read]
|
|
166
185
|
# road-to-reachable-code-memory Phase 4 — session_end learning-sidecar
|
|
167
186
|
# aggregation. Joins the EXISTING session_end concern set (no new platform
|
|
168
187
|
# wiring). Aggregates agents/memory/intake/*.jsonl through the learning
|
|
@@ -279,32 +298,73 @@ concerns:
|
|
|
279
298
|
args: []
|
|
280
299
|
fail_closed: false
|
|
281
300
|
severity: blocking
|
|
282
|
-
#
|
|
301
|
+
# Canary injection — deterministic carrier for the
|
|
283
302
|
# session-canary rule (greet-by-name at task start + reply-close markers
|
|
284
303
|
# as a user-observable context-degradation signal). Gated on
|
|
285
304
|
# `personal.canary_name` in .agent-settings.yml (empty = clean no-op),
|
|
286
305
|
# so unconfigured consumers pay nothing. Emits a `context` block the
|
|
287
306
|
# dispatcher forwards on session_start (hot-context mechanism).
|
|
307
|
+
# Bound in TWO slots on purpose: session_start carries the full contract once,
|
|
308
|
+
# user_prompt_submit carries a one-line beat. The obligation is per TASK and no
|
|
309
|
+
# host has a per-task slot (Cline maps TaskStart/TaskResume onto session_start;
|
|
310
|
+
# Claude Code has no task event), so per-turn — a strict superset — is the
|
|
311
|
+
# reachable covering period. Session scope alone measured a ~13-of-15 miss rate
|
|
312
|
+
# on task starts. Augment has no user_prompt_submit and is a DECLARED gap: its
|
|
313
|
+
# `stop` fires after the reply, so injecting there could not shape the reply the
|
|
314
|
+
# reminder is for, and counting it would be exactly the over-credit this pairing
|
|
315
|
+
# was added to remove.
|
|
288
316
|
# Never blocks. fail_closed: false.
|
|
289
317
|
session-canary:
|
|
290
318
|
script: src/scripts/session_canary_hook.ts
|
|
291
319
|
args: []
|
|
292
320
|
fail_closed: false
|
|
293
321
|
severity: advisory
|
|
322
|
+
# Shared per-session register (road-to-parallel-session-coordination) — writes
|
|
323
|
+
# this session into <git-common-dir>/agent-sessions/<session_id>.json so every
|
|
324
|
+
# other worktree of the repo can see it, and on session_start injects the live
|
|
325
|
+
# foreign sessions as a `context` block (hot-context mechanism).
|
|
326
|
+
#
|
|
327
|
+
# Bound in FOUR slots, with deliberately asymmetric meanings:
|
|
328
|
+
# session_start → register + prune expired + inject foreign sessions
|
|
329
|
+
# user_prompt_submit → heartbeat (pre-reply carrier)
|
|
330
|
+
# stop → heartbeat (post-reply carrier), NOT deregistration
|
|
331
|
+
# session_end → deregister
|
|
332
|
+
#
|
|
333
|
+
# `stop` is per-turn, not session end: on Claude Code the native Stop fires
|
|
334
|
+
# after every assistant reply (see _lib/obligation_frequency.ts), so
|
|
335
|
+
# deregistering there would kill a live session's own record after its first
|
|
336
|
+
# reply. The ONE exception is Cline, whose stop is TaskCancel and whose
|
|
337
|
+
# session_end is TaskComplete — a cancelled Cline task reaches neither, so it
|
|
338
|
+
# would leak its claim until TTL. That exception is an explicit platform
|
|
339
|
+
# allow-list in _lib/session_register.ts, never a condition computed from the
|
|
340
|
+
# frequency lattice: a future platform must be added by a human, deliberately.
|
|
341
|
+
#
|
|
342
|
+
# Correctness rests on heartbeat + TTL alone; deregistration only frees a claim
|
|
343
|
+
# sooner. So Windsurf's missing session_end costs latency, not correctness.
|
|
344
|
+
# Declared non-carriers: copilot (no hook surface), cursor CLI (per-turn slots
|
|
345
|
+
# are IDE-only), cowork (slots wired, lifecycle events inert) — a session there
|
|
346
|
+
# registers but expires by TTL rather than heartbeating, and the concern says so
|
|
347
|
+
# on stderr rather than letting the coverage join read green.
|
|
348
|
+
# Never blocks. fail_closed: false.
|
|
349
|
+
session-register:
|
|
350
|
+
script: src/scripts/session_register_hook.ts
|
|
351
|
+
args: []
|
|
352
|
+
fail_closed: false
|
|
353
|
+
severity: advisory
|
|
294
354
|
|
|
295
355
|
platforms:
|
|
296
356
|
augment:
|
|
297
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
298
|
-
session_end: [chat-history, memory-learn]
|
|
299
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
357
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
358
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
359
|
+
stop: [chat-history, hot-context, verify-before-complete, session-register]
|
|
300
360
|
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
|
|
301
361
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
302
362
|
|
|
303
363
|
claude:
|
|
304
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
305
|
-
session_end: [chat-history, memory-learn]
|
|
306
|
-
stop: [chat-history, hot-context, verify-before-complete, team-review-gate]
|
|
307
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
|
|
364
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
365
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
366
|
+
stop: [chat-history, hot-context, verify-before-complete, team-review-gate, session-register]
|
|
367
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
|
|
308
368
|
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
|
|
309
369
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
310
370
|
|
|
@@ -323,10 +383,10 @@ platforms:
|
|
|
323
383
|
# Decision matrix + upstream blockers tracked in
|
|
324
384
|
# agents/settings/contexts/chat-history-platform-hooks.md § Cowork.
|
|
325
385
|
cowork:
|
|
326
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
327
|
-
session_end: [chat-history, memory-learn]
|
|
328
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
329
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
|
|
386
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
387
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
388
|
+
stop: [chat-history, hot-context, verify-before-complete, session-register]
|
|
389
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
|
|
330
390
|
pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
|
|
331
391
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
332
392
|
|
|
@@ -338,10 +398,10 @@ platforms:
|
|
|
338
398
|
# IDE-only — CLI-only users fall back to /checkpoint per
|
|
339
399
|
# agents/settings/contexts/chat-history-platform-hooks.md.
|
|
340
400
|
cursor:
|
|
341
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
342
|
-
session_end: [chat-history, memory-learn]
|
|
343
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
344
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
|
|
401
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
402
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
403
|
+
stop: [chat-history, hot-context, verify-before-complete, session-register]
|
|
404
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
|
|
345
405
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
346
406
|
|
|
347
407
|
# Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
|
|
@@ -353,10 +413,10 @@ platforms:
|
|
|
353
413
|
# both map to session_start. TaskCancel maps to stop because the
|
|
354
414
|
# session is interrupted with partial state (mirrors Augment Stop).
|
|
355
415
|
cline:
|
|
356
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
357
|
-
session_end: [chat-history, memory-learn]
|
|
358
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
359
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
|
|
416
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
417
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
418
|
+
stop: [chat-history, hot-context, verify-before-complete, session-register]
|
|
419
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
|
|
360
420
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
361
421
|
|
|
362
422
|
# Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
|
|
@@ -372,11 +432,11 @@ platforms:
|
|
|
372
432
|
# surface to record verification commands; documented limitation).
|
|
373
433
|
# minimal-safe-diff is omitted entirely on Windsurf for the same reason.
|
|
374
434
|
windsurf:
|
|
375
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe]
|
|
376
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
435
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
436
|
+
stop: [chat-history, hot-context, verify-before-complete, session-register]
|
|
377
437
|
# language-mirror IS bound here: unlike minimal-safe-diff it needs only the
|
|
378
438
|
# user_prompt_submit slot, which Windsurf has.
|
|
379
|
-
user_prompt_submit: [chat-history, verify-before-complete, language-mirror, git-authorization]
|
|
439
|
+
user_prompt_submit: [chat-history, verify-before-complete, language-mirror, git-authorization, session-canary, session-register]
|
|
380
440
|
|
|
381
441
|
# Phase 7.8 — Gemini CLI. Hooks live at `.gemini/settings.json`
|
|
382
442
|
# (project) or `~/.gemini/settings.json` (user). Per Gemini docs
|
|
@@ -389,10 +449,10 @@ platforms:
|
|
|
389
449
|
# turn-check semantics. AfterAgent fires when the agent loop ends
|
|
390
450
|
# — this is our `stop` slot.
|
|
391
451
|
gemini:
|
|
392
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
|
|
393
|
-
session_end: [chat-history, memory-learn]
|
|
394
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
395
|
-
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
|
|
452
|
+
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
|
|
453
|
+
session_end: [chat-history, memory-learn, session-register]
|
|
454
|
+
stop: [chat-history, hot-context, verify-before-complete, session-register]
|
|
455
|
+
user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
|
|
396
456
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
397
457
|
|
|
398
458
|
# Phase 7.9 — Copilot has no hook surface. Concerns route through
|
|
@@ -329,6 +329,61 @@ function _is_blocked(git_tokens: string[]): [boolean, string] {
|
|
|
329
329
|
return [false, ''];
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Does an unparseable command plausibly INVOKE git?
|
|
334
|
+
*
|
|
335
|
+
* The fail-closed branch below used to test `/\bgit\b/` against the raw command
|
|
336
|
+
* string. That is a mention, not an invocation, and the difference is not
|
|
337
|
+
* academic: measured in the round-5 audit (2026-08-07), a
|
|
338
|
+
* `mkdir -p … && cat > notes.md <<HEREDOC … HEREDOC` with no git anywhere in
|
|
339
|
+
* command position was refused, because the heredoc body named rules such as
|
|
340
|
+
* `git-history-discipline` and contained apostrophes — so shlex failed and the
|
|
341
|
+
* substring matched. The message's own remedy (`git commit -F <file>`) could not
|
|
342
|
+
* apply, because there was no commit.
|
|
343
|
+
*
|
|
344
|
+
* Fail-closed is still the right posture for the case it was written for: a
|
|
345
|
+
* genuinely unparseable command that DOES invoke git. So the test moves from
|
|
346
|
+
* "the string contains git" to "some command position is git", with heredoc
|
|
347
|
+
* bodies removed first — the same data-not-command distinction
|
|
348
|
+
* `block_unauthorized_git` already makes.
|
|
349
|
+
*
|
|
350
|
+
* Deliberately regex-shaped rather than tokenised: shlex has already failed by
|
|
351
|
+
* the time this runs, so no reliable token stream exists. It therefore stays
|
|
352
|
+
* conservative — a `git` at the head of any line or after any separator counts,
|
|
353
|
+
* quoted or not.
|
|
354
|
+
*/
|
|
355
|
+
export function _looks_like_git_invocation(cmd: string, depth = 0): boolean {
|
|
356
|
+
if (depth > 3) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const withoutHeredocs = cmd.replace(
|
|
360
|
+
/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm,
|
|
361
|
+
'<<HEREDOC',
|
|
362
|
+
);
|
|
363
|
+
for (const rawSeg of withoutHeredocs.split(/\n|;|&&|\|\||\||&|\$\(|`/)) {
|
|
364
|
+
// Drop leading env assignments: `GIT_DIR=x git status`.
|
|
365
|
+
const seg = rawSeg
|
|
366
|
+
.trim()
|
|
367
|
+
.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+)+/, '');
|
|
368
|
+
// A `sh -c '<payload>'` payload IS command context — recurse, or the
|
|
369
|
+
// narrowing above would open a bypass this guard did not have before.
|
|
370
|
+
// Caught by the adversarial case, not by review.
|
|
371
|
+
const wrapped =
|
|
372
|
+
/^(?:\S*\/)?(?:ba|z|k)?sh\s+-[a-z]*c\s+(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(seg);
|
|
373
|
+
if (wrapped !== null) {
|
|
374
|
+
if (_looks_like_git_invocation(wrapped[1] ?? wrapped[2] ?? wrapped[3] ?? '', depth + 1)) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
// Command position: optional path prefix, then `git` as its own word.
|
|
380
|
+
if (/^(?:\S*\/)?git(?:\s|$)/.test(seg)) {
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
332
387
|
/** Return (blocked, reason). Fail-closed on parse error for git commands. */
|
|
333
388
|
function _check_command(cmd: string): [boolean, string] {
|
|
334
389
|
let tokens: string[];
|
|
@@ -336,10 +391,12 @@ function _check_command(cmd: string): [boolean, string] {
|
|
|
336
391
|
tokens = shlexSplit(cmd);
|
|
337
392
|
} catch (e) {
|
|
338
393
|
if (e instanceof ShlexError) {
|
|
339
|
-
if (
|
|
394
|
+
if (_looks_like_git_invocation(cmd)) {
|
|
340
395
|
return [
|
|
341
396
|
true,
|
|
342
|
-
'command parse failed (shlex) on a git-containing command — fail-closed (git-history-discipline)'
|
|
397
|
+
'command parse failed (shlex) on a git-containing command — fail-closed (git-history-discipline). \n'
|
|
398
|
+
+ 'Most common cause: an apostrophe or backtick inside a heredoc commit message in the SAME compound command as `git`. \n'
|
|
399
|
+
+ 'Fix: write the message to a file and use `git commit -F <file>` — do not retry the heredoc.',
|
|
343
400
|
];
|
|
344
401
|
}
|
|
345
402
|
return [false, ''];
|