@event4u/agent-config 14.18.0 → 14.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +169 -130
- package/README.md +71 -149
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +420 -16
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +28 -27
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +2132 -1462
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1619 -1045
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/release-gate-locality.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +93 -0
- package/src/scripts/_lib/release_material.ts +64 -1
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +66 -9
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_release_highlights.ts +74 -1
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/conformance_scan.ts +44 -3
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +75 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +14 -11
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/git_command_classifier.ts +156 -1
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
|
@@ -116,6 +116,22 @@ export const BLOCK_OPS: ReadonlySet<GitOp> = new Set<GitOp>([
|
|
|
116
116
|
"worktree-remove",
|
|
117
117
|
/** `-x` is the flag that takes `.env` and local certificates. */
|
|
118
118
|
"clean-ignored",
|
|
119
|
+
|
|
120
|
+
// ── The consequence vocabulary, added 2026-09-06. Every one is already
|
|
121
|
+
// never-autonomous in the `non-destructive-by-default` prose.
|
|
122
|
+
//
|
|
123
|
+
// MEMBERSHIP HERE IS A COUNTING DECISION, NOT A REFUSAL. The set's name
|
|
124
|
+
// predates ADR-254, which deleted the reader that blocked: the one consumer
|
|
125
|
+
// that acts on it now is `conformance_scan`'s after-the-fact tally, so
|
|
126
|
+
// "blocked" describes what the Hard Floor requires of the model, never what
|
|
127
|
+
// this package does at tool-call time.
|
|
128
|
+
"prod-deploy",
|
|
129
|
+
"prod-data-destroy",
|
|
130
|
+
"prod-infra-change",
|
|
131
|
+
"external-send",
|
|
132
|
+
"money-movement",
|
|
133
|
+
"trunk-force-push",
|
|
134
|
+
"out-of-scope-destruction",
|
|
119
135
|
]);
|
|
120
136
|
|
|
121
137
|
/** Recoverable operations — warned without a this-turn authorization. */
|
|
@@ -154,6 +170,17 @@ const G = "(?:-[A-Za-z-]+(?:=\\S+)?(?:\\s+\\S+)?\\s+)*";
|
|
|
154
170
|
/** Optional absolute/relative path in front of the tool word: `/usr/bin/npm`. */
|
|
155
171
|
const P = "^(?:\\S*\\/)?";
|
|
156
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Tool words the consequence rows classify, as a head anchor.
|
|
175
|
+
*
|
|
176
|
+
* Kept as one constant rather than folded into each row: `commandOp` filters
|
|
177
|
+
* segments by head BEFORE consulting the table, so a row whose tool word is
|
|
178
|
+
* missing here never runs — a silent no-op that looks exactly like a pattern
|
|
179
|
+
* that did not match.
|
|
180
|
+
*/
|
|
181
|
+
const CONSEQUENCE_TOOL_HEAD =
|
|
182
|
+
/^(?:\S*\/)?(terraform|pulumi|kubectl|helm|aws|gcloud|az|vercel|netlify|flyctl|fly|wrangler|serverless|sls|eb|heroku|cap|psql|mysql|mariadb|sqlite3|mongosh|mongo|redis-cli|sendmail|msmtp|mail|mailx|curl|stripe|braintree|paypal|rm)\b/i;
|
|
183
|
+
|
|
157
184
|
/**
|
|
158
185
|
* A `gh api` call that WRITES. Read-only `gh api` is the standard way to inspect
|
|
159
186
|
* GitHub state and must never be refused.
|
|
@@ -267,6 +294,13 @@ const COMMAND_OPS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
|
267
294
|
// Above `push`, and both spellings: `--force` and `--force-with-lease`. The
|
|
268
295
|
// "safe force" is in the same class here because what it is safe against is a
|
|
269
296
|
// stale local view, not a collaborator who pushed after your fetch.
|
|
297
|
+
{
|
|
298
|
+
op: "trunk-force-push",
|
|
299
|
+
re: new RegExp(
|
|
300
|
+
`${P}git\\s+${G}push\\b[^\\n;|&]*(--force\\b|--force-with-lease\\b|\\s-f\\b)[^\\n;|&]*\\b(main|master|trunk|production|prod)\\b`,
|
|
301
|
+
"i",
|
|
302
|
+
),
|
|
303
|
+
},
|
|
270
304
|
{
|
|
271
305
|
op: "force-push",
|
|
272
306
|
re: new RegExp(`${P}git\\s+${G}push\\b[^\\n;|&]*(--force\\b|--force-with-lease\\b|\\s-f\\b)`, "i"),
|
|
@@ -296,6 +330,73 @@ const COMMAND_OPS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
|
|
|
296
330
|
},
|
|
297
331
|
{ op: "close", re: new RegExp(`${P}gh\\s+(pr|issue)\\s+close\\b`, "i") },
|
|
298
332
|
|
|
333
|
+
// ── Consequence operations, added 2026-09-06
|
|
334
|
+
// (`road-to-authorization-that-reaches-further` 2.1).
|
|
335
|
+
//
|
|
336
|
+
// ORDER: above the generic `push`/`commit` fallbacks, because
|
|
337
|
+
// `trunk-force-push` is a `git push` and the most specific pattern must win —
|
|
338
|
+
// the rule this table states at its head.
|
|
339
|
+
//
|
|
340
|
+
// These classify commands the count could not see before. They add NOTHING to
|
|
341
|
+
// any refusal path: `commandOp` and `BLOCK_OPS` have exactly one consumer
|
|
342
|
+
// that acts on them, the after-the-fact tally in `conformance_scan`, and this
|
|
343
|
+
// change does not add a second.
|
|
344
|
+
{
|
|
345
|
+
op: "prod-data-destroy",
|
|
346
|
+
re: new RegExp(
|
|
347
|
+
`${P}(psql|mysql|mariadb|sqlite3|mongosh?)\\b[^\\n]*\\b(drop\\s+(table|database|schema)|truncate|delete\\s+from)\\b` +
|
|
348
|
+
`|${P}aws\\s+s3\\s+(rm|rb)\\b` +
|
|
349
|
+
`|${P}aws\\s+(dynamodb|rds)\\s+delete-\\S+` +
|
|
350
|
+
`|${P}redis-cli\\b[^\\n]*\\bflush(all|db)\\b`,
|
|
351
|
+
"i",
|
|
352
|
+
),
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
op: "prod-infra-change",
|
|
356
|
+
re: new RegExp(
|
|
357
|
+
`${P}terraform\\s+(apply|destroy)\\b` +
|
|
358
|
+
`|${P}pulumi\\s+(up|destroy)\\b` +
|
|
359
|
+
`|${P}kubectl\\s+(apply|delete|scale|drain)\\b` +
|
|
360
|
+
`|${P}helm\\s+(install|upgrade|uninstall|rollback)\\b` +
|
|
361
|
+
`|${P}aws\\s+(iam|ec2|route53|elbv2)\\s+(create|delete|put|update|modify|attach|detach)-\\S+` +
|
|
362
|
+
`|${P}(gcloud|az)\\s+\\S+\\s+(create|delete|update)\\b`,
|
|
363
|
+
"i",
|
|
364
|
+
),
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
op: "prod-deploy",
|
|
368
|
+
re: new RegExp(
|
|
369
|
+
`${P}(vercel|netlify|flyctl|fly|wrangler|serverless|sls|eb|heroku)\\s+${G}deploy\\b` +
|
|
370
|
+
`|${P}(vercel|netlify)\\b[^\\n]*--prod\\b` +
|
|
371
|
+
`|${P}(npm|pnpm|yarn)\\s+run\\s+deploy\\b` +
|
|
372
|
+
`|${P}cap\\s+production\\s+deploy\\b`,
|
|
373
|
+
"i",
|
|
374
|
+
),
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
op: "external-send",
|
|
378
|
+
re: new RegExp(
|
|
379
|
+
`${P}(sendmail|msmtp|mailx?)\\b` +
|
|
380
|
+
`|${P}curl\\b(?=[^\\n]*(?:-X|--request)\\s+(?:POST|PUT|PATCH)\\b)(?=[^\\n]*https?:\\/\\/)`,
|
|
381
|
+
"i",
|
|
382
|
+
),
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
op: "money-movement",
|
|
386
|
+
re: new RegExp(
|
|
387
|
+
`${P}(stripe|braintree|paypal)\\s+\\S*(charge|refund|payout|transfer|payment)\\S*\\b`,
|
|
388
|
+
"i",
|
|
389
|
+
),
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
// `rm -rf` on an ABSOLUTE path or a home-relative one. Deliberately not
|
|
393
|
+
// "outside the working tree" — this function receives a command string and
|
|
394
|
+
// no cwd, so that reading is not decidable here and a classifier that
|
|
395
|
+
// guesses is worse than one that under-reports.
|
|
396
|
+
op: "out-of-scope-destruction",
|
|
397
|
+
re: new RegExp(`${P}rm\\s+(-\\S+\\s+)*-\\S*[rR]\\S*\\s+(-\\S+\\s+)*(~|/)\\S*`, "i"),
|
|
398
|
+
},
|
|
399
|
+
|
|
299
400
|
{ op: "push", re: new RegExp(`${P}git\\s+${G}push\\b`, "i") },
|
|
300
401
|
{ op: "commit", re: new RegExp(`${P}git\\s+${G}commit\\b`, "i") },
|
|
301
402
|
{ op: "branch", re: new RegExp(`${P}git\\s+${G}(checkout\\s+-b|switch\\s+-c)\\b`, "i") },
|
|
@@ -592,7 +693,14 @@ export function commandOp(command: string): GitOp | null {
|
|
|
592
693
|
// unchecked once `publish` was authorized.
|
|
593
694
|
let found: GitOp | null = null;
|
|
594
695
|
for (const seg of invokedSegments(command)) {
|
|
595
|
-
|
|
696
|
+
// Widened 2026-09-06 for the consequence vocabulary. Before this the gate
|
|
697
|
+
// admitted five tool words, so `terraform apply` and `aws s3 rm` were not
|
|
698
|
+
// merely unclassified — they could not reach the table at all, and the
|
|
699
|
+
// count read them as nothing rather than as unmeasured.
|
|
700
|
+
if (
|
|
701
|
+
!/^(?:\S*\/)?(git|gh|npm|pnpm|yarn)\b/.test(seg) &&
|
|
702
|
+
!CONSEQUENCE_TOOL_HEAD.test(seg)
|
|
703
|
+
) {
|
|
596
704
|
continue;
|
|
597
705
|
}
|
|
598
706
|
for (const { op, re } of COMMAND_OPS) {
|
|
@@ -634,6 +742,53 @@ export function mergeTargetOf(command: string): number | null {
|
|
|
634
742
|
return api ? _boundedTarget(api[1] as string) : null;
|
|
635
743
|
}
|
|
636
744
|
|
|
745
|
+
/**
|
|
746
|
+
* The object a consequence command acts on, or `null` when the command does not
|
|
747
|
+
* name one.
|
|
748
|
+
*
|
|
749
|
+
* The sibling of `mergeTargetOf`, generalised past pull-request numbers, and it
|
|
750
|
+
* inherits that function's fail-closed stance verbatim: a command that names no
|
|
751
|
+
* object matches no record, so the caller reports nothing rather than guessing.
|
|
752
|
+
* Under-reporting is the safe direction for a measurement; a guessed object
|
|
753
|
+
* would manufacture a violation out of a parse failure.
|
|
754
|
+
*
|
|
755
|
+
* Normalised to lower case with quotes stripped, so it compares against the
|
|
756
|
+
* prose side's `extractObjects` without either end re-deriving the rule.
|
|
757
|
+
*/
|
|
758
|
+
export function commandObject(op: GitOp, command: string): string | null {
|
|
759
|
+
const patterns = COMMAND_OBJECTS[op];
|
|
760
|
+
if (!patterns) return null;
|
|
761
|
+
for (const re of patterns) {
|
|
762
|
+
const m = new RegExp(re.source, re.flags.replace("g", "")).exec(command);
|
|
763
|
+
const raw = m?.[1];
|
|
764
|
+
if (raw) {
|
|
765
|
+
const value = raw.trim().replace(/^["'`]+|["'`]+$/g, "").replace(/[.,;:]+$/, "").toLowerCase();
|
|
766
|
+
if (value) return value;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** Per-operation object patterns on the COMMAND side. */
|
|
773
|
+
const COMMAND_OBJECTS: Readonly<Record<string, readonly RegExp[]>> = {
|
|
774
|
+
"prod-data-destroy": [
|
|
775
|
+
/\bdrop\s+(?:table|database|schema|collection)\s+(?:if\s+exists\s+)?["'`]?([\w.$-]+)/i,
|
|
776
|
+
/\btruncate\s+(?:table\s+)?["'`]?([\w.$-]+)/i,
|
|
777
|
+
/\bdelete\s+from\s+["'`]?([\w.$-]+)/i,
|
|
778
|
+
/\bs3:\/\/([\w.-]+)/i,
|
|
779
|
+
/--table-name[= ]["'`]?([\w.-]+)/i,
|
|
780
|
+
],
|
|
781
|
+
"external-send": [
|
|
782
|
+
/\bhttps?:\/\/([^\s/"'`]+)/i,
|
|
783
|
+
/([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/,
|
|
784
|
+
],
|
|
785
|
+
"money-movement": [
|
|
786
|
+
/--customer[= ]["'`]?([\w.-]+)/i,
|
|
787
|
+
/--amount[= ]["'`]?([\d.,]+)/i,
|
|
788
|
+
/\b(\d[\d.,]*)\s*(?:eur|usd)\b/i,
|
|
789
|
+
],
|
|
790
|
+
};
|
|
791
|
+
|
|
637
792
|
/** A target only counts inside the API's own range; anything else is no target. */
|
|
638
793
|
function _boundedTarget(digits: string): number | null {
|
|
639
794
|
const n = Number.parseInt(digits, 10);
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host_lowering — the reader for `host_lowering.yaml`.
|
|
3
|
+
*
|
|
4
|
+
* The table is the single place a host's block-exit, emission shape, fail
|
|
5
|
+
* policy and bridge bindings are written. Before it, those five facts lived in
|
|
6
|
+
* `host_semantics.ts`, in five private per-host binding constants in
|
|
7
|
+
* `install.ts`, and in prose; three rounds of prose corrections did not stop
|
|
8
|
+
* the drift, because prose has no single reader.
|
|
9
|
+
*
|
|
10
|
+
* LOADING IS STRICT. A missing or malformed table throws rather than
|
|
11
|
+
* degrading to an empty one. An empty table would resolve `VERIFIED_PLATFORMS`
|
|
12
|
+
* to the empty set, which silently downgrades the one host that actually
|
|
13
|
+
* enforces from blocking to advisory — the exact failure this roadmap's risk
|
|
14
|
+
* register ranks first. A throw is loud; a silent downgrade is not. The
|
|
15
|
+
* dispatcher already reads `hook_manifest.yaml` unconditionally from the same
|
|
16
|
+
* tree, so this adds no new class of failure.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
import { parse as parseYaml } from 'yaml';
|
|
23
|
+
|
|
24
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
25
|
+
const _IN_BUNDLE = typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__;
|
|
26
|
+
const _REPO_ROOT = path.resolve(
|
|
27
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
28
|
+
...(_IN_BUNDLE ? ['..', '..'] : ['..', '..', '..']),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const HOST_LOWERING_PATH = path.join(_REPO_ROOT, 'src', 'scripts', 'hooks', 'host_lowering.yaml');
|
|
32
|
+
|
|
33
|
+
/** Provenance of a row. Every field may be null; `expires` may not. */
|
|
34
|
+
export interface VerifiedBlock {
|
|
35
|
+
docs_at: string | null;
|
|
36
|
+
docs_url: string | null;
|
|
37
|
+
probe_at: string | null;
|
|
38
|
+
host_version: string | null;
|
|
39
|
+
expires: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SlotRow {
|
|
43
|
+
/** Host-native event name(s) for this slot, in emission order. */
|
|
44
|
+
native: string[];
|
|
45
|
+
/** Exit code the host honours as a refusal here, or null if unestablished. */
|
|
46
|
+
block_exit: number | null;
|
|
47
|
+
/** Entry-shape-specific extra (gemini's per-binding matcher). */
|
|
48
|
+
matcher?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SurfaceRow {
|
|
52
|
+
entry_shape: string;
|
|
53
|
+
json_shape: string;
|
|
54
|
+
fail_policy: string;
|
|
55
|
+
timeout_unit: string;
|
|
56
|
+
timeout_default: number | null;
|
|
57
|
+
verified: VerifiedBlock | null;
|
|
58
|
+
/** Insertion-ordered: bridge entries are written in this order. */
|
|
59
|
+
slots: Map<string, SlotRow>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type HostLowering = Map<string, Map<string, SurfaceRow>>;
|
|
63
|
+
|
|
64
|
+
/** The surface every row carries today — see the table header. */
|
|
65
|
+
export const DEFAULT_SURFACE = 'any';
|
|
66
|
+
|
|
67
|
+
function _asString(v: unknown): string | null {
|
|
68
|
+
if (v === null || v === undefined) return null;
|
|
69
|
+
// A bare `2027-09-06` parses as a Date under YAML 1.1 timestamp rules.
|
|
70
|
+
if (v instanceof Date) return v.toISOString().slice(0, 10);
|
|
71
|
+
return String(v);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _parse(text: string): HostLowering {
|
|
75
|
+
const raw = parseYaml(text) as Record<string, unknown> | null;
|
|
76
|
+
const hostsRaw = raw?.['hosts'];
|
|
77
|
+
if (typeof hostsRaw !== 'object' || hostsRaw === null) {
|
|
78
|
+
throw new Error('host_lowering.yaml: missing `hosts:` map');
|
|
79
|
+
}
|
|
80
|
+
const out: HostLowering = new Map();
|
|
81
|
+
for (const [host, hv] of Object.entries(hostsRaw as Record<string, unknown>)) {
|
|
82
|
+
const surfacesRaw = (hv as Record<string, unknown> | null)?.['surfaces'];
|
|
83
|
+
if (typeof surfacesRaw !== 'object' || surfacesRaw === null) {
|
|
84
|
+
throw new Error(`host_lowering.yaml: ${host} has no \`surfaces:\` map`);
|
|
85
|
+
}
|
|
86
|
+
const surfaces = new Map<string, SurfaceRow>();
|
|
87
|
+
for (const [surface, sv] of Object.entries(surfacesRaw as Record<string, unknown>)) {
|
|
88
|
+
const s = sv as Record<string, unknown>;
|
|
89
|
+
const verifiedRaw = s['verified'] as Record<string, unknown> | null | undefined;
|
|
90
|
+
let verified: VerifiedBlock | null = null;
|
|
91
|
+
if (verifiedRaw) {
|
|
92
|
+
const expires = _asString(verifiedRaw['expires']);
|
|
93
|
+
if (!expires) {
|
|
94
|
+
throw new Error(`host_lowering.yaml: ${host}/${surface} verified block has no \`expires\``);
|
|
95
|
+
}
|
|
96
|
+
verified = {
|
|
97
|
+
docs_at: _asString(verifiedRaw['docs_at']),
|
|
98
|
+
docs_url: _asString(verifiedRaw['docs_url']),
|
|
99
|
+
probe_at: _asString(verifiedRaw['probe_at']),
|
|
100
|
+
host_version: _asString(verifiedRaw['host_version']),
|
|
101
|
+
expires,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const slots = new Map<string, SlotRow>();
|
|
105
|
+
for (const [slot, rvRaw] of Object.entries((s['slots'] ?? {}) as Record<string, unknown>)) {
|
|
106
|
+
const rv = rvRaw as Record<string, unknown>;
|
|
107
|
+
const nativeRaw = rv['native'];
|
|
108
|
+
const native = Array.isArray(nativeRaw) ? nativeRaw.map(String) : [String(nativeRaw)];
|
|
109
|
+
const be = rv['block_exit'];
|
|
110
|
+
const row: SlotRow = {
|
|
111
|
+
native,
|
|
112
|
+
block_exit: typeof be === 'number' ? be : null,
|
|
113
|
+
};
|
|
114
|
+
if (typeof rv['matcher'] === 'string') row.matcher = rv['matcher'];
|
|
115
|
+
slots.set(slot, row);
|
|
116
|
+
}
|
|
117
|
+
surfaces.set(surface, {
|
|
118
|
+
entry_shape: String(s['entry_shape'] ?? 'none'),
|
|
119
|
+
json_shape: String(s['json_shape'] ?? 'none'),
|
|
120
|
+
fail_policy: String(s['fail_policy'] ?? 'discard'),
|
|
121
|
+
timeout_unit: String(s['timeout_unit'] ?? 'unknown'),
|
|
122
|
+
timeout_default: typeof s['timeout_default'] === 'number' ? s['timeout_default'] : null,
|
|
123
|
+
verified,
|
|
124
|
+
slots,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
out.set(host, surfaces);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let _cache: HostLowering | null = null;
|
|
133
|
+
|
|
134
|
+
/** Parse the table once per process. Throws on a missing or malformed file. */
|
|
135
|
+
export function loadHostLowering(): HostLowering {
|
|
136
|
+
if (_cache === null) _cache = _parse(fs.readFileSync(HOST_LOWERING_PATH, 'utf-8'));
|
|
137
|
+
return _cache;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Test seam — drop the memoized table so a fixture can be loaded instead. */
|
|
141
|
+
export function _resetHostLoweringCache(table?: HostLowering): void {
|
|
142
|
+
_cache = table ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Parse a fixture table without touching the committed file. */
|
|
146
|
+
export function parseHostLowering(text: string): HostLowering {
|
|
147
|
+
return _parse(text);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function surfaceRow(
|
|
151
|
+
host: string,
|
|
152
|
+
surface: string = DEFAULT_SURFACE,
|
|
153
|
+
table: HostLowering = loadHostLowering(),
|
|
154
|
+
): SurfaceRow | null {
|
|
155
|
+
return table.get(host)?.get(surface) ?? table.get(host)?.get(DEFAULT_SURFACE) ?? null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Is the row's provenance still current?
|
|
160
|
+
*
|
|
161
|
+
* A row past `expires` is treated exactly as `verified: null` — it cannot
|
|
162
|
+
* carry a blocking binding. Comparison is on the calendar date so the answer
|
|
163
|
+
* does not depend on the runner's clock time of day.
|
|
164
|
+
*/
|
|
165
|
+
export function isVerifiedNow(row: SurfaceRow | null, today: string = new Date().toISOString().slice(0, 10)): boolean {
|
|
166
|
+
if (row?.verified == null) return false;
|
|
167
|
+
return row.verified.expires >= today;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Hosts carrying an unexpired `verified` block. */
|
|
171
|
+
export function verifiedPlatforms(table: HostLowering = loadHostLowering()): ReadonlySet<string> {
|
|
172
|
+
const out = new Set<string>();
|
|
173
|
+
for (const [host, surfaces] of table) {
|
|
174
|
+
for (const row of surfaces.values()) {
|
|
175
|
+
if (isVerifiedNow(row)) {
|
|
176
|
+
out.add(host);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Exit code this host honours as a refusal on `slot`, or null. */
|
|
185
|
+
export function blockExitFor(host: string, slot: string, table: HostLowering = loadHostLowering()): number | null {
|
|
186
|
+
const row = surfaceRow(host, DEFAULT_SURFACE, table);
|
|
187
|
+
if (!isVerifiedNow(row)) return null;
|
|
188
|
+
return row?.slots.get(slot)?.block_exit ?? null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** One bridge binding: an agent-config slot lowered onto one native event. */
|
|
192
|
+
export interface HostBinding {
|
|
193
|
+
slot: string;
|
|
194
|
+
native: string;
|
|
195
|
+
matcher: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Ordered bridge bindings for a host.
|
|
200
|
+
*
|
|
201
|
+
* Order is the table's `slots:` insertion order and is part of the generated
|
|
202
|
+
* output — the emitted bridge JSON is keyed by native event, so re-ordering
|
|
203
|
+
* changes bytes. Never sort this.
|
|
204
|
+
*/
|
|
205
|
+
export function hostBindings(host: string, table: HostLowering = loadHostLowering()): HostBinding[] {
|
|
206
|
+
const row = surfaceRow(host, DEFAULT_SURFACE, table);
|
|
207
|
+
if (!row) return [];
|
|
208
|
+
const out: HostBinding[] = [];
|
|
209
|
+
for (const [slot, sr] of row.slots) {
|
|
210
|
+
for (const native of sr.native) {
|
|
211
|
+
out.push({ slot, native, matcher: sr.matcher ?? '' });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# host_lowering — how each host lowers a dispatcher verdict into its own hook
|
|
2
|
+
# contract, and what this tree has actually established about that lowering.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS FILE EXISTS. Before it, five facts about a host lived in five
|
|
5
|
+
# places: `VERIFIED_PLATFORMS` and the block-capable event set in
|
|
6
|
+
# `hooks/host_semantics.ts`, five private per-host binding constants in
|
|
7
|
+
# `install.ts`, and prose in half a dozen docs. Three rounds of corrections to
|
|
8
|
+
# that prose did not stop it drifting, because prose has no single reader. This
|
|
9
|
+
# table is the single reader.
|
|
10
|
+
#
|
|
11
|
+
# WHAT A ROW IS. A row is keyed `(host, surface, slot)`. It describes what this
|
|
12
|
+
# package does and what this package has established — never what the host is
|
|
13
|
+
# capable of. `verified: null` means nobody established anything; it does NOT
|
|
14
|
+
# mean the host cannot enforce.
|
|
15
|
+
#
|
|
16
|
+
# THE `surface` AXIS IS PRESENT AND DEGENERATE, DELIBERATELY. Every row today
|
|
17
|
+
# is `any`, because nothing in this tree has measured an IDE-versus-CLI
|
|
18
|
+
# difference in hook lowering on any host. The key carries the dimension so a
|
|
19
|
+
# future measurement lands as a new row rather than as a rewrite; inventing
|
|
20
|
+
# `ide:`/`cli:` rows nobody measured is the defect this roadmap exists to stop.
|
|
21
|
+
#
|
|
22
|
+
# CONSUMED BY:
|
|
23
|
+
# - src/scripts/hooks/host_semantics.ts (VERIFIED_PLATFORMS, isBlockCapable, emission shape)
|
|
24
|
+
# - src/scripts/_lib/host_bindings.ts (per-host bridge bindings + the install smoke probe)
|
|
25
|
+
# - src/scripts/lint_hook_manifest.ts (schema + expiry gate)
|
|
26
|
+
#
|
|
27
|
+
# COLUMNS
|
|
28
|
+
# block_exit exit code this host honours as a refusal on that slot;
|
|
29
|
+
# `null` = this tree has not established that it honours one.
|
|
30
|
+
# json_shape structured-output envelope the dispatcher may emit.
|
|
31
|
+
# entry_shape shape of the bridge entry the installer writes.
|
|
32
|
+
# fail_policy `propagate` = the dispatcher's exit code reaches the host;
|
|
33
|
+
# `discard` = the trampoline exits 0 regardless.
|
|
34
|
+
# timeout_unit unit the host's own hook-timeout setting is expressed in.
|
|
35
|
+
# timeout_default host default for that timeout. `null` = not established here.
|
|
36
|
+
# verified provenance of the row, or `null`. `expires` is mandatory
|
|
37
|
+
# when the block is present — see lint_hook_manifest.
|
|
38
|
+
# slots ORDERED. Bridge entries are written in this order, so the
|
|
39
|
+
# order is part of the generated output and must not be
|
|
40
|
+
# sorted. `native` may be a list where one slot has two
|
|
41
|
+
# native events (cline session_start).
|
|
42
|
+
schema_version: 1
|
|
43
|
+
|
|
44
|
+
hosts:
|
|
45
|
+
claude:
|
|
46
|
+
surfaces:
|
|
47
|
+
any:
|
|
48
|
+
entry_shape: claude-settings-hooks
|
|
49
|
+
json_shape: hook-specific-output
|
|
50
|
+
fail_policy: propagate
|
|
51
|
+
timeout_unit: seconds
|
|
52
|
+
timeout_default: null
|
|
53
|
+
# Transcribed from the behaviour host_semantics.ts already implements —
|
|
54
|
+
# this row is a description of the status quo, not a new claim.
|
|
55
|
+
# docs_at/host_version are null because the original read was never
|
|
56
|
+
# dated; that absence is the record, and the lint reports it.
|
|
57
|
+
verified:
|
|
58
|
+
docs_at: null
|
|
59
|
+
docs_url: https://docs.claude.com/en/docs/claude-code/hooks
|
|
60
|
+
probe_at: null
|
|
61
|
+
host_version: null
|
|
62
|
+
expires: 2027-09-06
|
|
63
|
+
slots:
|
|
64
|
+
session_start: { native: SessionStart, block_exit: null }
|
|
65
|
+
session_end: { native: SessionEnd, block_exit: null }
|
|
66
|
+
stop: { native: Stop, block_exit: 2 }
|
|
67
|
+
user_prompt_submit: { native: UserPromptSubmit, block_exit: 2 }
|
|
68
|
+
pre_tool_use: { native: PreToolUse, block_exit: 2 }
|
|
69
|
+
post_tool_use: { native: PostToolUse, block_exit: null }
|
|
70
|
+
pre_compact: { native: PreCompact, block_exit: null }
|
|
71
|
+
subagent_start: { native: SubagentStart, block_exit: null }
|
|
72
|
+
subagent_stop: { native: SubagentStop, block_exit: null }
|
|
73
|
+
|
|
74
|
+
augment:
|
|
75
|
+
surfaces:
|
|
76
|
+
any:
|
|
77
|
+
entry_shape: augment-settings-hooks
|
|
78
|
+
json_shape: none
|
|
79
|
+
# augment-dispatcher.sh exits 0 unconditionally: a verdict reaches the
|
|
80
|
+
# trampoline and stops there.
|
|
81
|
+
fail_policy: discard
|
|
82
|
+
timeout_unit: unknown
|
|
83
|
+
timeout_default: null
|
|
84
|
+
verified: null
|
|
85
|
+
slots:
|
|
86
|
+
session_start: { native: SessionStart, block_exit: null }
|
|
87
|
+
session_end: { native: SessionEnd, block_exit: null }
|
|
88
|
+
stop: { native: Stop, block_exit: null }
|
|
89
|
+
pre_tool_use: { native: PreToolUse, block_exit: null }
|
|
90
|
+
post_tool_use: { native: PostToolUse, block_exit: null }
|
|
91
|
+
|
|
92
|
+
cursor:
|
|
93
|
+
surfaces:
|
|
94
|
+
any:
|
|
95
|
+
entry_shape: cursor-hooks-json
|
|
96
|
+
json_shape: none
|
|
97
|
+
fail_policy: propagate
|
|
98
|
+
timeout_unit: unknown
|
|
99
|
+
timeout_default: null
|
|
100
|
+
verified: null
|
|
101
|
+
slots:
|
|
102
|
+
session_start: { native: sessionStart, block_exit: null }
|
|
103
|
+
session_end: { native: sessionEnd, block_exit: null }
|
|
104
|
+
stop: { native: stop, block_exit: null }
|
|
105
|
+
user_prompt_submit: { native: beforeSubmitPrompt, block_exit: null }
|
|
106
|
+
post_tool_use: { native: postToolUse, block_exit: null }
|
|
107
|
+
|
|
108
|
+
cline:
|
|
109
|
+
surfaces:
|
|
110
|
+
any:
|
|
111
|
+
entry_shape: cline-per-event-script
|
|
112
|
+
json_shape: none
|
|
113
|
+
# The generated per-event script swallows the dispatcher's exit code
|
|
114
|
+
# and prints `{}`.
|
|
115
|
+
fail_policy: discard
|
|
116
|
+
timeout_unit: unknown
|
|
117
|
+
timeout_default: null
|
|
118
|
+
verified: null
|
|
119
|
+
slots:
|
|
120
|
+
session_start: { native: [TaskStart, TaskResume], block_exit: null }
|
|
121
|
+
session_end: { native: TaskComplete, block_exit: null }
|
|
122
|
+
stop: { native: TaskCancel, block_exit: null }
|
|
123
|
+
user_prompt_submit: { native: UserPromptSubmit, block_exit: null }
|
|
124
|
+
post_tool_use: { native: PostToolUse, block_exit: null }
|
|
125
|
+
|
|
126
|
+
windsurf:
|
|
127
|
+
surfaces:
|
|
128
|
+
any:
|
|
129
|
+
entry_shape: windsurf-hooks-json
|
|
130
|
+
json_shape: none
|
|
131
|
+
fail_policy: propagate
|
|
132
|
+
timeout_unit: unknown
|
|
133
|
+
timeout_default: null
|
|
134
|
+
verified: null
|
|
135
|
+
slots:
|
|
136
|
+
session_start: { native: post_setup_worktree, block_exit: null }
|
|
137
|
+
user_prompt_submit: { native: pre_user_prompt, block_exit: null }
|
|
138
|
+
stop: { native: post_cascade_response, block_exit: null }
|
|
139
|
+
|
|
140
|
+
gemini:
|
|
141
|
+
surfaces:
|
|
142
|
+
any:
|
|
143
|
+
entry_shape: gemini-settings-hooks
|
|
144
|
+
json_shape: none
|
|
145
|
+
fail_policy: propagate
|
|
146
|
+
timeout_unit: unknown
|
|
147
|
+
timeout_default: null
|
|
148
|
+
# A live deny probe was attempted on 2026-09-06 against Gemini CLI
|
|
149
|
+
# 0.50.0 in an isolated scratch workspace and could not run: the CLI
|
|
150
|
+
# aborts with IneligibleTierError before any turn begins, so no tool
|
|
151
|
+
# call and therefore no BeforeTool hook is reachable. An unrunnable
|
|
152
|
+
# probe establishes nothing, so this stays null.
|
|
153
|
+
verified: null
|
|
154
|
+
slots:
|
|
155
|
+
session_start: { native: SessionStart, block_exit: null, matcher: "" }
|
|
156
|
+
session_end: { native: SessionEnd, block_exit: null, matcher: "" }
|
|
157
|
+
stop: { native: AfterAgent, block_exit: null, matcher: "" }
|
|
158
|
+
user_prompt_submit: { native: BeforeAgent, block_exit: null, matcher: "" }
|
|
159
|
+
post_tool_use: { native: AfterTool, block_exit: null, matcher: ".*" }
|
|
160
|
+
|
|
161
|
+
cowork:
|
|
162
|
+
surfaces:
|
|
163
|
+
any:
|
|
164
|
+
entry_shape: none
|
|
165
|
+
json_shape: none
|
|
166
|
+
# cowork-dispatcher.sh exits 0 unconditionally, and the host is
|
|
167
|
+
# upstream-blocked from reading any settings source this package writes.
|
|
168
|
+
fail_policy: discard
|
|
169
|
+
timeout_unit: unknown
|
|
170
|
+
timeout_default: null
|
|
171
|
+
verified: null
|
|
172
|
+
slots: {}
|
|
173
|
+
|
|
174
|
+
copilot:
|
|
175
|
+
surfaces:
|
|
176
|
+
any:
|
|
177
|
+
entry_shape: none
|
|
178
|
+
json_shape: none
|
|
179
|
+
fail_policy: discard
|
|
180
|
+
timeout_unit: unknown
|
|
181
|
+
timeout_default: null
|
|
182
|
+
# `fallback_only: true` in hook_manifest.yaml. This package binds
|
|
183
|
+
# nothing here and has measured nothing here.
|
|
184
|
+
verified: null
|
|
185
|
+
slots: {}
|