@event4u/agent-config 9.26.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 +57 -229
- package/dist/agent-src/commands/analyze/conformance.md +19 -0
- package/dist/agent-src/commands/roadmap/next.md +65 -3
- 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 +2 -0
- package/dist/agent-src/rules/design-fidelity.md +2 -0
- package/dist/agent-src/rules/design-review-after-ui-write.md +4 -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 +13 -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 +61 -19
- package/dist/agent-src/rules/settings-ask-protocol.md +4 -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 +2 -0
- 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 +4 -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 +2 -0
- package/dist/agent-src/rules/user-interrupt-priority.md +2 -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_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 +109 -109
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.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 +796 -289
- package/dist/install/install.mjs +1 -1
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +571 -417
- 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/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/settingsCarveOut.js +118 -0
- package/dist/shared/settingsCarveOut.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/evaluator-budgets.json +4 -4
- package/src/scripts/_cli/cmd_doctor.ts +6 -21
- 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/check_enforcement_coverage.ts +192 -7
- package/src/scripts/check_rule_projection_integrity.ts +364 -0
- package/src/scripts/condense.ts +36 -2
- package/src/scripts/conformance_scan.ts +35 -0
- package/src/scripts/generate_settings_reference.ts +232 -0
- package/src/scripts/hook_manifest.yaml +68 -27
- package/src/scripts/hooks/block_no_verify.ts +56 -1
- package/src/scripts/hooks/block_unauthorized_git.ts +67 -1
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/language_mirror_hook.ts +56 -6
- package/src/scripts/orchestration_backfill.ts +354 -0
- package/src/scripts/release.ts +142 -36
- 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/settingsCarveOut.ts +143 -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
|
+
}
|
|
@@ -298,32 +298,73 @@ concerns:
|
|
|
298
298
|
args: []
|
|
299
299
|
fail_closed: false
|
|
300
300
|
severity: blocking
|
|
301
|
-
#
|
|
301
|
+
# Canary injection — deterministic carrier for the
|
|
302
302
|
# session-canary rule (greet-by-name at task start + reply-close markers
|
|
303
303
|
# as a user-observable context-degradation signal). Gated on
|
|
304
304
|
# `personal.canary_name` in .agent-settings.yml (empty = clean no-op),
|
|
305
305
|
# so unconfigured consumers pay nothing. Emits a `context` block the
|
|
306
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.
|
|
307
316
|
# Never blocks. fail_closed: false.
|
|
308
317
|
session-canary:
|
|
309
318
|
script: src/scripts/session_canary_hook.ts
|
|
310
319
|
args: []
|
|
311
320
|
fail_closed: false
|
|
312
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
|
|
313
354
|
|
|
314
355
|
platforms:
|
|
315
356
|
augment:
|
|
316
|
-
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]
|
|
317
|
-
session_end: [chat-history, memory-learn]
|
|
318
|
-
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]
|
|
319
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]
|
|
320
361
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
321
362
|
|
|
322
363
|
claude:
|
|
323
|
-
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]
|
|
324
|
-
session_end: [chat-history, memory-learn]
|
|
325
|
-
stop: [chat-history, hot-context, verify-before-complete, team-review-gate]
|
|
326
|
-
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]
|
|
327
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]
|
|
328
369
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
329
370
|
|
|
@@ -342,10 +383,10 @@ platforms:
|
|
|
342
383
|
# Decision matrix + upstream blockers tracked in
|
|
343
384
|
# agents/settings/contexts/chat-history-platform-hooks.md § Cowork.
|
|
344
385
|
cowork:
|
|
345
|
-
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]
|
|
346
|
-
session_end: [chat-history, memory-learn]
|
|
347
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
348
|
-
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]
|
|
349
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]
|
|
350
391
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
351
392
|
|
|
@@ -357,10 +398,10 @@ platforms:
|
|
|
357
398
|
# IDE-only — CLI-only users fall back to /checkpoint per
|
|
358
399
|
# agents/settings/contexts/chat-history-platform-hooks.md.
|
|
359
400
|
cursor:
|
|
360
|
-
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]
|
|
361
|
-
session_end: [chat-history, memory-learn]
|
|
362
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
363
|
-
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]
|
|
364
405
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
365
406
|
|
|
366
407
|
# Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
|
|
@@ -372,10 +413,10 @@ platforms:
|
|
|
372
413
|
# both map to session_start. TaskCancel maps to stop because the
|
|
373
414
|
# session is interrupted with partial state (mirrors Augment Stop).
|
|
374
415
|
cline:
|
|
375
|
-
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]
|
|
376
|
-
session_end: [chat-history, memory-learn]
|
|
377
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
378
|
-
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]
|
|
379
420
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
380
421
|
|
|
381
422
|
# Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
|
|
@@ -391,11 +432,11 @@ platforms:
|
|
|
391
432
|
# surface to record verification commands; documented limitation).
|
|
392
433
|
# minimal-safe-diff is omitted entirely on Windsurf for the same reason.
|
|
393
434
|
windsurf:
|
|
394
|
-
session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe]
|
|
395
|
-
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]
|
|
396
437
|
# language-mirror IS bound here: unlike minimal-safe-diff it needs only the
|
|
397
438
|
# user_prompt_submit slot, which Windsurf has.
|
|
398
|
-
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]
|
|
399
440
|
|
|
400
441
|
# Phase 7.8 — Gemini CLI. Hooks live at `.gemini/settings.json`
|
|
401
442
|
# (project) or `~/.gemini/settings.json` (user). Per Gemini docs
|
|
@@ -408,10 +449,10 @@ platforms:
|
|
|
408
449
|
# turn-check semantics. AfterAgent fires when the agent loop ends
|
|
409
450
|
# — this is our `stop` slot.
|
|
410
451
|
gemini:
|
|
411
|
-
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]
|
|
412
|
-
session_end: [chat-history, memory-learn]
|
|
413
|
-
stop: [chat-history, hot-context, verify-before-complete]
|
|
414
|
-
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]
|
|
415
456
|
post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
|
|
416
457
|
|
|
417
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,7 +391,7 @@ 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
397
|
'command parse failed (shlex) on a git-containing command — fail-closed (git-history-discipline). \n'
|
|
@@ -143,6 +143,62 @@ export function extractCommand(envelope: JsonObject): string | null {
|
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Split on shell separators that are OUTSIDE quotes.
|
|
148
|
+
*
|
|
149
|
+
* Separators recognised, matching the raw regex this replaces: newline, `;`,
|
|
150
|
+
* `&&`, `||`, `|`, `&`. A separator inside `'…'` or `"…"` is a literal, so it
|
|
151
|
+
* does not split. Backslash escapes the next character outside single quotes,
|
|
152
|
+
* per POSIX.
|
|
153
|
+
*
|
|
154
|
+
* Deliberately NOT a full tokeniser. `shlexSplit` in `block_no_verify` throws on
|
|
155
|
+
* an unbalanced quote, which is the right behaviour for a guard that fail-closes;
|
|
156
|
+
* this guard must classify every command it sees, including a malformed one. So
|
|
157
|
+
* an unterminated quote here degrades to "the rest of the string is quoted",
|
|
158
|
+
* which yields one segment starting at the real command word — the conservative
|
|
159
|
+
* outcome, since the command word is what `commandOp` matches on.
|
|
160
|
+
*/
|
|
161
|
+
export function splitOutsideQuotes(command: string): string[] {
|
|
162
|
+
const parts: string[] = [];
|
|
163
|
+
let cur = "";
|
|
164
|
+
let quote: '"' | "'" | null = null;
|
|
165
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
166
|
+
const ch = command[i] as string;
|
|
167
|
+
if (quote !== null) {
|
|
168
|
+
cur += ch;
|
|
169
|
+
if (ch === quote) {
|
|
170
|
+
quote = null;
|
|
171
|
+
} else if (ch === "\\" && quote === '"' && i + 1 < command.length) {
|
|
172
|
+
cur += command[i + 1] as string;
|
|
173
|
+
i += 1;
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (ch === '"' || ch === "'") {
|
|
178
|
+
quote = ch;
|
|
179
|
+
cur += ch;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (ch === "\\" && i + 1 < command.length) {
|
|
183
|
+
cur += ch + (command[i + 1] as string);
|
|
184
|
+
i += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ch === "\n" || ch === ";" || ch === "|" || ch === "&") {
|
|
188
|
+
// `&&` and `||` are two-character separators; consume the pair.
|
|
189
|
+
if ((ch === "|" || ch === "&") && command[i + 1] === ch) {
|
|
190
|
+
i += 1;
|
|
191
|
+
}
|
|
192
|
+
parts.push(cur);
|
|
193
|
+
cur = "";
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
cur += ch;
|
|
197
|
+
}
|
|
198
|
+
parts.push(cur);
|
|
199
|
+
return parts;
|
|
200
|
+
}
|
|
201
|
+
|
|
146
202
|
/**
|
|
147
203
|
* Split a shell command into the segments that are actually INVOKED.
|
|
148
204
|
*
|
|
@@ -159,6 +215,16 @@ export function extractCommand(envelope: JsonObject): string | null {
|
|
|
159
215
|
* Quoted payloads are NOT simply discarded — `sh -c "npm publish"` really does
|
|
160
216
|
* publish. Those are unwrapped and re-split, so closing the false positive does
|
|
161
217
|
* not open a bypass.
|
|
218
|
+
*
|
|
219
|
+
* The separator split is QUOTE-AWARE, and that is not a refinement — it is the
|
|
220
|
+
* same false-positive class the paragraph above describes, surviving in the one
|
|
221
|
+
* place the round-2 fix did not reach. Measured in the round-5 audit
|
|
222
|
+
* (2026-08-07): a read-only `grep -n -E "…|npm publish|…"` was refused with
|
|
223
|
+
* "Blocked: `publish` with no authorization in this turn's prompt", because a
|
|
224
|
+
* raw `split` on `|` cut the quoted alternation into a segment beginning with
|
|
225
|
+
* `npm`. A `|` inside quotes is data, exactly like a heredoc body; only an
|
|
226
|
+
* unquoted one is a pipe. Splitting outside quotes closes that without
|
|
227
|
+
* discarding quoted payloads, so `sh -c "npm publish"` still unwraps below.
|
|
162
228
|
*/
|
|
163
229
|
export function invokedSegments(command: string, depth = 0): string[] {
|
|
164
230
|
if (depth > 3) {
|
|
@@ -172,7 +238,7 @@ export function invokedSegments(command: string, depth = 0): string[] {
|
|
|
172
238
|
/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm,
|
|
173
239
|
"<<HEREDOC",
|
|
174
240
|
);
|
|
175
|
-
for (const raw of withoutHeredocs
|
|
241
|
+
for (const raw of splitOutsideQuotes(withoutHeredocs)) {
|
|
176
242
|
let seg = raw.trim();
|
|
177
243
|
if (!seg) {
|
|
178
244
|
continue;
|
|
@@ -41,6 +41,7 @@ import { main as surfaceProbeMain } from '../surface_probe_hook.js';
|
|
|
41
41
|
import { main as teamReviewGateMain } from '../team_review_gate_hook.js';
|
|
42
42
|
import { main as prUrlReminderMain } from '../pr_url_reminder_hook.js';
|
|
43
43
|
import { main as sessionCanaryMain } from '../session_canary_hook.js';
|
|
44
|
+
import { main as sessionRegisterMain } from '../session_register_hook.js';
|
|
44
45
|
import { main as languageMirrorMain } from '../language_mirror_hook.js';
|
|
45
46
|
import { main as gitAuthorizationMain } from '../git_authorization_hook.js';
|
|
46
47
|
import { main as blockUnauthorizedGitMain } from './block_unauthorized_git.js';
|
|
@@ -74,6 +75,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
|
|
|
74
75
|
'src/scripts/team_review_gate_hook.ts': teamReviewGateMain as ConcernMain,
|
|
75
76
|
'src/scripts/pr_url_reminder_hook.ts': prUrlReminderMain as ConcernMain,
|
|
76
77
|
'src/scripts/session_canary_hook.ts': sessionCanaryMain as ConcernMain,
|
|
78
|
+
'src/scripts/session_register_hook.ts': sessionRegisterMain as ConcernMain,
|
|
77
79
|
'src/scripts/language_mirror_hook.ts': languageMirrorMain as ConcernMain,
|
|
78
80
|
'src/scripts/git_authorization_hook.ts': gitAuthorizationMain as ConcernMain,
|
|
79
81
|
'src/scripts/hooks/block_unauthorized_git.ts': blockUnauthorizedGitMain as ConcernMain,
|
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
*
|
|
8
8
|
* WHY THIS EXISTS — the measured defect, not a general reminder.
|
|
9
9
|
*
|
|
10
|
-
* A conformance audit of 30 sessions (2026-08-06) found
|
|
10
|
+
* A conformance audit of 30 sessions (2026-08-06) found assistant turns
|
|
11
11
|
* answering a German prompt in English, across 11 sessions, three of them a
|
|
12
|
-
* 100 % English run. The
|
|
13
|
-
*
|
|
12
|
+
* 100 % English run. The count this comment first carried, ~470, was an early
|
|
13
|
+
* intermediate figure and is superseded: that audit's own final baseline is
|
|
14
|
+
* **626**, and conformance round 5 (2026-08-07) measures **641** over an
|
|
15
|
+
* overlapping-but-not-identical 30-session window. Both are published side by
|
|
16
|
+
* side rather than treated as a trend, because the windows differ. The obvious
|
|
17
|
+
* reading is "the model ignored a rule". The transcripts say otherwise:
|
|
14
18
|
*
|
|
15
19
|
* **47 skill- and slash-command bodies arrive in the `user` role** across 21
|
|
16
20
|
* of the 30 sessions. In the worst session the last user-role content before
|
|
@@ -272,9 +276,9 @@ export function pinText(language: Exclude<Verdict, "und">, source: PinSource = "
|
|
|
272
276
|
`lines, and the recommendation label under any numbered-options block.\n\n` +
|
|
273
277
|
`This pin exists because the trigger is not observable from the transcript ` +
|
|
274
278
|
`alone: slash-command and skill bodies arrive in the user role and can be ` +
|
|
275
|
-
`long and English, which is what drove
|
|
276
|
-
`
|
|
277
|
-
`injected skill body are NOT the trigger — this pin is.\n` +
|
|
279
|
+
`long and English, which is what drove the 626 wrong-language turns measured ` +
|
|
280
|
+
`in the 30-session audit this hook was built from. Tool output, file ` +
|
|
281
|
+
`contents, and an injected skill body are NOT the trigger — this pin is.\n` +
|
|
278
282
|
`</language-pin>`
|
|
279
283
|
);
|
|
280
284
|
}
|
|
@@ -372,6 +376,12 @@ export function run(
|
|
|
372
376
|
if (!prompt) {
|
|
373
377
|
return EXIT_ALLOW;
|
|
374
378
|
}
|
|
379
|
+
// A harness-generated turn is not a chat message. Leave the pin untouched —
|
|
380
|
+
// the same reasoning as an undetermined prompt below, and for the same reason
|
|
381
|
+
// this hook exists at all.
|
|
382
|
+
if (isSyntheticPrompt(prompt)) {
|
|
383
|
+
return EXIT_ALLOW;
|
|
384
|
+
}
|
|
375
385
|
|
|
376
386
|
const session_id = typeof envelope["session_id"] === "string" ? envelope["session_id"] : "";
|
|
377
387
|
const target = path.join(options.consumer_root, STATE_FILE);
|
|
@@ -413,6 +423,46 @@ export function run(
|
|
|
413
423
|
return EXIT_ALLOW;
|
|
414
424
|
}
|
|
415
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Is this "prompt" a harness-generated turn rather than something the human typed?
|
|
428
|
+
*
|
|
429
|
+
* MEASURED, IN THIS HOOK'S OWN AUDIT (round 5, 2026-08-07). The state file read
|
|
430
|
+
* mid-session:
|
|
431
|
+
*
|
|
432
|
+
* { "language": "en", "source": "prompt", "prompt_chars": 6627,
|
|
433
|
+
* "de_markers": 0, "en_markers": 63 }
|
|
434
|
+
*
|
|
435
|
+
* No human wrote those 6,627 characters. They were a background-task completion
|
|
436
|
+
* notification, injected as a user turn, which `user_prompt_submit` sees and this
|
|
437
|
+
* hook classified as the trigger — flipping a German session to `en` for every
|
|
438
|
+
* later turn, and recording `source: "prompt"`, a false provenance claim.
|
|
439
|
+
*
|
|
440
|
+
* This is the SAME defect the hook was built to remove: non-chat content read as
|
|
441
|
+
* the trigger. The header above explains at length why a skill body must not set
|
|
442
|
+
* the pin; `user_prompt_submit` was chosen because "a skill body never reaches
|
|
443
|
+
* it". Synthetic turns do reach it, so the event alone was never sufficient — the
|
|
444
|
+
* content test has to exist too.
|
|
445
|
+
*
|
|
446
|
+
* The markers are structural, not linguistic: the harness stamps these turns
|
|
447
|
+
* with fixed envelope tags. Matching prose would misfire on a human quoting a
|
|
448
|
+
* notification; matching the tags cannot, because a human typing `<task-notification>`
|
|
449
|
+
* is quoting, and a quote does not open at character zero of the turn.
|
|
450
|
+
*
|
|
451
|
+
* Deliberately conservative: an unrecognised synthetic shape falls through and
|
|
452
|
+
* pins as before. Under-filtering keeps today's behaviour; over-filtering would
|
|
453
|
+
* silently stop pinning real prompts, which is the worse failure.
|
|
454
|
+
*/
|
|
455
|
+
export function isSyntheticPrompt(prompt: string): boolean {
|
|
456
|
+
const head = prompt.trimStart().slice(0, 400);
|
|
457
|
+
return (
|
|
458
|
+
head.startsWith("[SYSTEM NOTIFICATION - NOT USER INPUT]") ||
|
|
459
|
+
head.startsWith("<task-notification>") ||
|
|
460
|
+
head.startsWith("<system-reminder>") ||
|
|
461
|
+
head.startsWith("<local-command-caveat>") ||
|
|
462
|
+
/^\[SYSTEM NOTIFICATION\b/.test(head)
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
416
466
|
export function main(argv?: string[]): number {
|
|
417
467
|
const args = argv ?? process.argv.slice(2);
|
|
418
468
|
let consumer_root = process.cwd();
|