@event4u/agent-config 17.2.1 → 17.4.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/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
- package/CHANGELOG.md +167 -193
- package/CREDITS.md +2 -2
- package/README.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +16 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
- package/dist/cli/commands/recordTriggerEval.js +1 -1
- package/dist/cli/registry.js +2 -2
- package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-5AYJGUA3.js} +1 -3
- package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
- package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
- package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
- package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-WY5ZZPGL.js} +2 -2
- package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-X6LZ7W5A.js} +73 -10
- package/dist/cli-delegate/cmd_conformance.js +190 -192
- package/dist/cli-delegate/cmd_doctor.js +5 -5
- package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +8 -4
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_update.js +24 -13
- package/dist/cli-delegate/cmd_upgrade.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
- package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
- package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
- package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- 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/code_graph_refresh.chunk.cjs +443 -0
- package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
- package/dist/hooks/dispatch.js +1001 -938
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/egress_engine.chunk.cjs +57 -7
- package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
- package/dist/hooks/update_notice.chunk.cjs +914 -0
- package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
- package/dist/install/install.mjs +6974 -6037
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
- package/dist/mcp/registry-manifest.json +3 -2
- package/dist/mcp/server.json +13 -4
- package/dist/mcp/server.mjs +71202 -63282
- package/dist/scripts/_lib/egress_rules.js +8 -2
- package/dist/scripts/condense.js +18 -15
- package/dist/server/routes/discovery.js +1 -1
- package/dist/server/routes/install.js +1 -1
- package/dist/server/routes/ping.js +1 -1
- package/dist/server/routes/schema.js +4 -4
- package/dist/server/routes/settings.js +2 -6
- package/dist/server/routes/wizard.js +6 -6
- package/dist/server/schemas/settings.js +42 -42
- package/dist/shared/settingsSurface.js +1 -1
- package/dist/shared/userMd/schema.js +1 -1
- package/dist/shared/zodJsonSchema.js +47 -0
- package/dist/ui/assets/index-B0bcIgi0.js +77 -0
- package/dist/ui/index.html +1 -1
- package/package.json +6 -3
- package/src/agent-src/templates/scripts/check_memory.ts +1 -1
- package/src/config/ci-local-parity.yml +11 -0
- package/src/config/continuity-surface.json +9 -2
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/gitignore-block.txt +6 -5
- package/src/config/hook-bundle-budget.json +46 -6
- package/src/config/pack-size-budget.json +12 -1
- package/src/config/packed-binary-manifest.json +18 -0
- package/src/config/sigstore/npm_keys.json +50 -0
- package/src/config/sigstore/trust-material.json +9 -0
- package/src/config/sigstore/trusted_root.json +126 -0
- package/src/config/skill-body-budget.json +7 -1
- package/src/scripts/_cli/cmd_settings_set.ts +3 -2
- package/src/scripts/_cli/cmd_update.ts +48 -20
- package/src/scripts/_cli/cmd_upgrade.ts +7 -1
- package/src/scripts/_cli/upgrade_unattended.ts +90 -76
- package/src/scripts/_dispatch.bash +4 -3
- package/src/scripts/_lib/confusables.ts +0 -19
- package/src/scripts/_lib/egress_normalize.ts +53 -23
- package/src/scripts/_lib/egress_rules.ts +8 -2
- package/src/scripts/_lib/egress_text.ts +5 -3
- package/src/scripts/_lib/egress_token.ts +7 -5
- package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
- package/src/scripts/_lib/git_common_dir.ts +10 -0
- package/src/scripts/_lib/graph_feeder_record.ts +3 -2
- package/src/scripts/_lib/install_reach_checks.ts +32 -2
- package/src/scripts/_lib/lineage_backfill.ts +136 -0
- package/src/scripts/_lib/lineage_ledger.ts +211 -0
- package/src/scripts/_lib/lineage_map.ts +55 -0
- package/src/scripts/_lib/lineage_query.ts +140 -0
- package/src/scripts/_lib/lineage_seed.ts +116 -0
- package/src/scripts/_lib/lineage_watch.ts +249 -0
- package/src/scripts/_lib/link_crypto.ts +8 -4
- package/src/scripts/_lib/listing_manifests.ts +85 -0
- package/src/scripts/_lib/neighbour_census.ts +1 -0
- package/src/scripts/_lib/production_reach.ts +3 -3
- package/src/scripts/_lib/project_registry.ts +227 -0
- package/src/scripts/_lib/provenance_verify.ts +14 -242
- package/src/scripts/_lib/release_settings_surface.ts +2 -6
- package/src/scripts/_lib/sigstore_provenance.ts +177 -0
- package/src/scripts/_lib/source_id.ts +48 -0
- package/src/scripts/_lib/source_shape.ts +4 -0
- package/src/scripts/_lib/unattended_gates.ts +6 -1
- package/src/scripts/adoption_status.ts +231 -27
- package/src/scripts/advisories.ts +26 -10
- package/src/scripts/ai_council/council_egress.ts +5 -6
- package/src/scripts/bench_graph_feeder_latency.ts +10 -3
- package/src/scripts/build_mcp_registry_manifest.ts +23 -12
- package/src/scripts/check_dependency_floors.ts +14 -0
- package/src/scripts/check_egress_mutations.ts +1 -0
- package/src/scripts/check_release_manifest_content.ts +17 -2
- package/src/scripts/check_release_pr_shape.ts +5 -0
- package/src/scripts/code_graph/build.ts +145 -45
- package/src/scripts/code_graph/cli.ts +382 -35
- package/src/scripts/code_graph/conventions_default.ts +247 -0
- package/src/scripts/code_graph/detect.ts +5 -5
- package/src/scripts/code_graph/estate_cli.ts +135 -0
- package/src/scripts/code_graph/estate_index.ts +394 -0
- package/src/scripts/code_graph/estate_manifests.ts +223 -0
- package/src/scripts/code_graph/estate_map.ts +141 -0
- package/src/scripts/code_graph/extract.ts +8 -2
- package/src/scripts/code_graph/extract_more.ts +241 -2
- package/src/scripts/code_graph/graph_budget.ts +158 -0
- package/src/scripts/code_graph/graph_home.ts +80 -0
- package/src/scripts/code_graph/lineage_cli.ts +315 -0
- package/src/scripts/code_graph/loader.ts +10 -2
- package/src/scripts/code_graph/map.ts +195 -53
- package/src/scripts/code_graph/map_model.ts +113 -20
- package/src/scripts/code_graph/map_verify.ts +80 -5
- package/src/scripts/code_graph/map_viewer/viewer.css +18 -0
- package/src/scripts/code_graph/map_viewer/viewer.js +146 -15
- package/src/scripts/code_graph/passes.ts +173 -0
- package/src/scripts/code_graph/project_ref.ts +25 -0
- package/src/scripts/code_graph/projects_cli.ts +254 -0
- package/src/scripts/code_graph/providers/angular.ts +55 -0
- package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
- package/src/scripts/code_graph/providers/catalog.ts +299 -0
- package/src/scripts/code_graph/providers/django.ts +206 -0
- package/src/scripts/code_graph/providers/express.ts +25 -0
- package/src/scripts/code_graph/providers/fastapi.ts +49 -0
- package/src/scripts/code_graph/providers/flask.ts +99 -0
- package/src/scripts/code_graph/providers/go_http.ts +121 -0
- package/src/scripts/code_graph/providers/index.ts +103 -0
- package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
- package/src/scripts/code_graph/providers/nestjs.ts +34 -0
- package/src/scripts/code_graph/providers/nextjs.ts +91 -0
- package/src/scripts/code_graph/providers/nuxt.ts +55 -0
- package/src/scripts/code_graph/providers/rails.ts +204 -0
- package/src/scripts/code_graph/providers/react-router.ts +58 -0
- package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
- package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
- package/src/scripts/code_graph/providers/shared/js.ts +252 -0
- package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
- package/src/scripts/code_graph/providers/shared/php.ts +64 -0
- package/src/scripts/code_graph/providers/shared/python.ts +223 -0
- package/src/scripts/code_graph/providers/spring.ts +175 -0
- package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
- package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
- package/src/scripts/code_graph/providers/symfony.ts +142 -0
- package/src/scripts/code_graph/providers/typeorm.ts +58 -0
- package/src/scripts/code_graph/providers/types.ts +56 -0
- package/src/scripts/code_graph/providers/vue-router.ts +21 -0
- package/src/scripts/code_graph/providers/wordpress.ts +128 -0
- package/src/scripts/code_graph/query.ts +9 -9
- package/src/scripts/code_graph/refresh_cli.ts +81 -0
- package/src/scripts/code_graph/refresh_lock.ts +197 -0
- package/src/scripts/code_graph/repo_identity.ts +169 -0
- package/src/scripts/code_graph/resolution_tiers.ts +19 -7
- package/src/scripts/code_graph/scip_reader.ts +2 -2
- package/src/scripts/code_graph/sqlite_store.ts +31 -28
- package/src/scripts/code_graph/types.ts +79 -41
- package/src/scripts/code_graph/validate.ts +6 -8
- package/src/scripts/condense.ts +15 -15
- package/src/scripts/generate_settings_reference.ts +2 -6
- package/src/scripts/hook_chunks.mjs +11 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +43 -23
- package/src/scripts/hooks/code_graph_refresh_hook.ts +163 -0
- package/src/scripts/hooks/dispatch_hook.ts +6 -7
- package/src/scripts/hooks/host_semantics.ts +72 -0
- package/src/scripts/hooks/lazy_concern.ts +8 -0
- package/src/scripts/hooks/user_notice.ts +105 -0
- package/src/scripts/install-hooks.sh +54 -33
- package/src/scripts/install.ts +2 -6
- package/src/scripts/lint_harvest_provenance.ts +137 -11
- package/src/scripts/lint_hook_manifest.ts +2 -0
- package/src/scripts/lint_marketplace.ts +33 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +70 -15
- package/src/scripts/mcp_server/graph_tools.ts +165 -41
- package/src/scripts/network_sources.ts +24 -6
- package/src/scripts/release.ts +2 -5
- package/src/scripts/release_drill.ts +4 -1
- package/src/scripts/release_env.ts +17 -0
- package/src/scripts/release_mix_taxonomy.json +3 -0
- package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
- package/src/scripts/update_notice_hook.ts +75 -7
- package/src/server/schemas/settings.ts +42 -42
- package/src/shared/settingsSurface.ts +1 -1
- package/src/shared/userMd/schema.ts +1 -1
- package/src/shared/zodJsonSchema.ts +41 -0
- package/src/vendor/grammars/README.md +8 -6
- package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
- package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
- package/dist/ui/assets/index-w70Ado09.js +0 -23
- /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
- /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { blockExitFor, DEFAULT_SURFACE, loadHostLowering, surfaceRow, verifiedPlatforms } from "./host_lowering.js";
|
|
38
|
+
import { hostShowsUserNotice, writeNoticeReceipts, type NoticeBatch } from "./user_notice.js";
|
|
38
39
|
|
|
39
40
|
/** Internal severity, mirroring dispatch_hook's EXIT_ALLOW / BLOCK / WARN. */
|
|
40
41
|
export type Severity = "allow" | "warn" | "block";
|
|
@@ -133,6 +134,77 @@ export function claudeAdditionalContext(event: string, text: string): string {
|
|
|
133
134
|
})}\n`;
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
138
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compose the dispatcher's stdout from its three parts: plain `session_start`
|
|
143
|
+
* context, the native emission, and a user-addressed notice.
|
|
144
|
+
*
|
|
145
|
+
* Without a notice — or on a host with no verified user field — this is the
|
|
146
|
+
* old output byte for byte: context, then emission. With one, the host parses
|
|
147
|
+
* stdout as JSON only when the whole of it is one object, so the parts are
|
|
148
|
+
* MERGED as structures, never concatenated as text: the emission's object is
|
|
149
|
+
* kept, context joins its `hookSpecificOutput.additionalContext`, and the
|
|
150
|
+
* notice joins `systemMessage`. Plain context is accepted on `session_start`
|
|
151
|
+
* only, and an emission that is not one JSON object is never reinterpreted —
|
|
152
|
+
* in either case the notice is dropped and the old output stands.
|
|
153
|
+
*/
|
|
154
|
+
export function withUserNotice(
|
|
155
|
+
platform: string,
|
|
156
|
+
event: string,
|
|
157
|
+
context: string,
|
|
158
|
+
emission: string,
|
|
159
|
+
notice: string,
|
|
160
|
+
): string {
|
|
161
|
+
const legacy = context + emission;
|
|
162
|
+
const text = notice.trim();
|
|
163
|
+
if (!text || !hostShowsUserNotice(platform) || !usesNativeEmission(platform)) return legacy;
|
|
164
|
+
if (context.trim() && event !== "session_start") return legacy;
|
|
165
|
+
let reply: Record<string, unknown> = {};
|
|
166
|
+
if (emission.trim()) {
|
|
167
|
+
try {
|
|
168
|
+
reply = JSON.parse(emission) as Record<string, unknown>;
|
|
169
|
+
} catch {
|
|
170
|
+
return legacy;
|
|
171
|
+
}
|
|
172
|
+
if (!isObject(reply)) return legacy;
|
|
173
|
+
}
|
|
174
|
+
// A field this merge would extend must already have the type it extends.
|
|
175
|
+
const hsoIn = reply["hookSpecificOutput"];
|
|
176
|
+
if (hsoIn !== undefined && !isObject(hsoIn)) return legacy;
|
|
177
|
+
if (![reply["systemMessage"], hsoIn?.["additionalContext"]].every((v) => v === undefined || typeof v === "string")) return legacy;
|
|
178
|
+
const join = (a: unknown, b: string, sep: string): string => (typeof a === "string" && a ? `${a}${sep}${b}` : b);
|
|
179
|
+
if (context.trim()) {
|
|
180
|
+
const hso = hsoIn ?? { hookEventName: nativeEventName(platform, event) };
|
|
181
|
+
reply["hookSpecificOutput"] = { ...hso, additionalContext: join(hso["additionalContext"], context.trim(), "\n\n") };
|
|
182
|
+
}
|
|
183
|
+
reply["systemMessage"] = join(reply["systemMessage"], text, "\n");
|
|
184
|
+
return `${JSON.stringify(reply)}\n`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Write the dispatcher's whole stdout — `withUserNotice` over its parts — and
|
|
189
|
+
* only then a receipt for each notice that is actually in what was written: a
|
|
190
|
+
* write that throws leaves no receipt, and a dropped notice leaves none, so its
|
|
191
|
+
* concern says it again instead of counting it as shown. The notice rides only
|
|
192
|
+
* on exit 0, because the host parses stdout only then.
|
|
193
|
+
*/
|
|
194
|
+
export function writeStdout(
|
|
195
|
+
platform: string,
|
|
196
|
+
event: string,
|
|
197
|
+
context: string,
|
|
198
|
+
emission: Emission,
|
|
199
|
+
notices: NoticeBatch,
|
|
200
|
+
sessionId: string,
|
|
201
|
+
write: (text: string) => unknown,
|
|
202
|
+
): void {
|
|
203
|
+
const out = withUserNotice(platform, event, context, emission.stdout, emission.exit === 0 ? notices.map((n) => n.text).join("\n") : "");
|
|
204
|
+
if (out) write(out);
|
|
205
|
+
if (notices.length > 0 && out !== context + emission.stdout) writeNoticeReceipts(notices, sessionId);
|
|
206
|
+
}
|
|
207
|
+
|
|
136
208
|
/**
|
|
137
209
|
* A host permission verdict for a `pre_tool_use` call.
|
|
138
210
|
*
|
|
@@ -72,6 +72,14 @@ export const LAZY_CONCERNS: Readonly<Record<string, { readonly chunk: string; re
|
|
|
72
72
|
chunk: 'instructions_observer.chunk.cjs',
|
|
73
73
|
source: 'instructions_observer_hook',
|
|
74
74
|
}),
|
|
75
|
+
'src/scripts/hooks/code_graph_refresh_hook.ts': Object.freeze({
|
|
76
|
+
chunk: 'code_graph_refresh.chunk.cjs',
|
|
77
|
+
source: 'code_graph_refresh_hook',
|
|
78
|
+
}),
|
|
79
|
+
'src/scripts/update_notice_hook.ts': Object.freeze({
|
|
80
|
+
chunk: 'update_notice.chunk.cjs',
|
|
81
|
+
source: path.join('..', 'update_notice_hook'),
|
|
82
|
+
}),
|
|
75
83
|
});
|
|
76
84
|
|
|
77
85
|
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user-addressed channel: text a concern hands the dispatcher for the HUMAN,
|
|
3
|
+
* which the host shows in its own UI and never adds to model context.
|
|
4
|
+
*
|
|
5
|
+
* WHY IT EXISTS. Every channel the dispatcher had addressed the model
|
|
6
|
+
* (`additional_context`, `context`) or went to stderr, and every host runs the
|
|
7
|
+
* dispatcher on pipes — so a concern with something to tell the user had no
|
|
8
|
+
* path that reached them. `update-notice` was refused registration for exactly
|
|
9
|
+
* that: bound, it would have said nothing anywhere.
|
|
10
|
+
*
|
|
11
|
+
* Claude Code documents a `systemMessage` field on every hook event, "Display a
|
|
12
|
+
* message to the user", read off the 2.1.310 binary on 2026-10-24. It is the
|
|
13
|
+
* only host this package has verified a user-addressed field on, so the set
|
|
14
|
+
* below is one host wide; an unlisted host gets nothing rather than a guess.
|
|
15
|
+
*
|
|
16
|
+
* loss_class: ephemeral-lossy
|
|
17
|
+
*
|
|
18
|
+
* A notice longer than USER_NOTICE_MAX_CHARS loses its tail, deliberately: the
|
|
19
|
+
* text is for the user's UI and never reaches the model, and a notice is a few
|
|
20
|
+
* lines by contract, so nothing recovers the cut part.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
|
|
25
|
+
import { write_atomic } from '../_lib/fs_atomic.js';
|
|
26
|
+
import { write_target } from '../_lib/user_global_paths.js';
|
|
27
|
+
|
|
28
|
+
/** The concern reply key a notification concern puts its user-facing text under. */
|
|
29
|
+
export const USER_NOTICE_KEY = 'user_notice';
|
|
30
|
+
|
|
31
|
+
/** The reply key naming this one notice, echoed into its receipt and never shown. */
|
|
32
|
+
export const USER_NOTICE_ID_KEY = 'notice_id';
|
|
33
|
+
|
|
34
|
+
/** Longest notice the dispatcher forwards, in Unicode code points; a notice is a few lines, never a document. */
|
|
35
|
+
export const USER_NOTICE_MAX_CHARS = 1000;
|
|
36
|
+
|
|
37
|
+
/** Hosts with a verified field that shows text to the user and not to the model. */
|
|
38
|
+
export const USER_NOTICE_HOSTS: ReadonlySet<string> = new Set(['claude']);
|
|
39
|
+
|
|
40
|
+
export function hostShowsUserNotice(platform: unknown): boolean {
|
|
41
|
+
return typeof platform === 'string' && USER_NOTICE_HOSTS.has(platform);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The notices one dispatch kept, each with the concern it came from, in
|
|
46
|
+
* manifest order. One entry carries both, so the text that is emitted and the
|
|
47
|
+
* concern that gets a receipt can never come apart.
|
|
48
|
+
*/
|
|
49
|
+
export type NoticeBatch = Array<{ readonly concern: string; readonly text: string; readonly id: string }>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Only a `notification` concern may address the user: no other effect was
|
|
53
|
+
* reviewed for it. The text is cleaned, then capped by code point so a pair is
|
|
54
|
+
* never split, and kept only if something is left to show.
|
|
55
|
+
*/
|
|
56
|
+
export function collectUserNotice(
|
|
57
|
+
batch: NoticeBatch,
|
|
58
|
+
concern: Record<string, unknown>,
|
|
59
|
+
reply: Record<string, unknown>,
|
|
60
|
+
clean: (text: string) => string,
|
|
61
|
+
): void {
|
|
62
|
+
const notice = reply[USER_NOTICE_KEY];
|
|
63
|
+
if (concern['effect'] !== 'notification' || typeof notice !== 'string') return;
|
|
64
|
+
const text = Array.from(clean(notice.trim())).slice(0, USER_NOTICE_MAX_CHARS).join('').trim();
|
|
65
|
+
const id = reply[USER_NOTICE_ID_KEY];
|
|
66
|
+
if (text) batch.push({ concern: String(concern['name']), text, id: typeof id === 'string' ? id.slice(0, 64) : '' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A delivery receipt: the dispatcher writes one per concern whose notice it
|
|
71
|
+
* actually put into host output, naming the session and the notice's own id.
|
|
72
|
+
* A concern that must not count a notice as shown until it was handed to the
|
|
73
|
+
* host reads it on its next run and matches the id it handed over; anything the
|
|
74
|
+
* dispatcher dropped leaves no receipt for that id, so the concern says it again.
|
|
75
|
+
*/
|
|
76
|
+
export interface NoticeReceipt {
|
|
77
|
+
readonly session_id: string;
|
|
78
|
+
readonly notice_id: string;
|
|
79
|
+
readonly at: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function receiptPath(concern: string): string {
|
|
83
|
+
return write_target(`user-notice-${concern}.delivered`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function writeNoticeReceipts(batch: NoticeBatch, sessionId: string): void {
|
|
87
|
+
for (const { concern, id } of batch) {
|
|
88
|
+
try {
|
|
89
|
+
write_atomic(receiptPath(concern), JSON.stringify({ session_id: sessionId, notice_id: id, at: new Date().toISOString() }));
|
|
90
|
+
} catch {
|
|
91
|
+
// no receipt → the concern repeats its notice, which is the safe direction
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function readNoticeReceipt(concern: string): NoticeReceipt | null {
|
|
97
|
+
try {
|
|
98
|
+
const o = JSON.parse(fs.readFileSync(receiptPath(concern), 'utf-8')) as Record<string, unknown>;
|
|
99
|
+
return typeof o['session_id'] === 'string' && typeof o['notice_id'] === 'string' && typeof o['at'] === 'string'
|
|
100
|
+
? { session_id: o['session_id'], notice_id: o['notice_id'], at: o['at'] }
|
|
101
|
+
: null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Install git hooks for this repository.
|
|
3
3
|
# Run once: bash src/scripts/install-hooks.sh
|
|
4
|
+
#
|
|
5
|
+
# TRUST MODEL. The hooks this writes execute code FROM THE CHECKOUT —
|
|
6
|
+
# `./agent-config`, `./scripts-run`, `task`, `npm run build:cli` — on commit,
|
|
7
|
+
# checkout, merge, rebase and push. That is acceptable in exactly one place:
|
|
8
|
+
# this package's own development tree, whose code the developer already runs
|
|
9
|
+
# (tests, `prepare`, the CLI itself). A globally installed `agent-config` would
|
|
10
|
+
# be an older build of the same code, so the checkout's own is the right one
|
|
11
|
+
# HERE. It is wrong anywhere else: run from a consumer's
|
|
12
|
+
# `node_modules/@event4u/agent-config/`, this script would resolve the
|
|
13
|
+
# CONSUMER's git directory and install these hooks into a repository whose
|
|
14
|
+
# code nobody vetted for them. The guard below enforces the boundary — the
|
|
15
|
+
# project root must be the git top-level AND its package.json must name
|
|
16
|
+
# @event4u/agent-config — and consumer freshness travels through the
|
|
17
|
+
# `code-graph-refresh` session concern instead, which never runs repository
|
|
18
|
+
# code (road-to-code-graph-refresh-follow-ups 3.1 / 3.2).
|
|
4
19
|
|
|
5
20
|
set -euo pipefail
|
|
6
21
|
|
|
@@ -8,6 +23,15 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
8
23
|
# This installer lives at src/scripts/install-hooks.sh, so the repo root
|
|
9
24
|
# (which owns .git/) is two levels up.
|
|
10
25
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
26
|
+
|
|
27
|
+
TOPLEVEL="$(git -C "$PROJECT_ROOT" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
28
|
+
if [ -z "$TOPLEVEL" ] \
|
|
29
|
+
|| [ "$(cd "$TOPLEVEL" && pwd -P)" != "$(cd "$PROJECT_ROOT" && pwd -P)" ] \
|
|
30
|
+
|| ! grep -Eq '"name"[[:space:]]*:[[:space:]]*"@event4u/agent-config"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
|
|
31
|
+
echo "❌ refusing to install the development git hooks: $PROJECT_ROOT is not the top level of an" >&2
|
|
32
|
+
echo " @event4u/agent-config checkout. These hooks run code from the checkout (TRUST MODEL above)." >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
11
35
|
# Resolve the SHARED hooks dir via git so this also works from a linked worktree
|
|
12
36
|
# (where $PROJECT_ROOT/.git is a file, not a dir, and a literal .git/hooks path
|
|
13
37
|
# would break under `set -e`). --git-common-dir points at the main .git even from
|
|
@@ -619,18 +643,26 @@ echo "✅ Auto-sync block appended to post-merge / post-checkout hooks." >&2
|
|
|
619
643
|
#
|
|
620
644
|
# road-to-a-graph-that-is-shipped 1.3. A code graph goes stale the moment a
|
|
621
645
|
# commit lands, and the only thing that knew were the reader — after it had
|
|
622
|
-
# already answered from a stale index. These
|
|
646
|
+
# already answered from a stale index. These hooks close that without a
|
|
623
647
|
# resident process: git tells us HEAD moved, and a detached child rebuilds.
|
|
624
648
|
#
|
|
649
|
+
# Four hooks, because HEAD moves through four doors (measured with a probe
|
|
650
|
+
# repo): a commit fires post-commit, a branch switch post-checkout, a
|
|
651
|
+
# fast-forward `git pull` ONLY post-merge, and a rebase post-checkout at its
|
|
652
|
+
# START and post-rewrite at its end. Without the last two, `git pull` left the
|
|
653
|
+
# graph stale and a rebase refreshed against an intermediate HEAD.
|
|
654
|
+
#
|
|
625
655
|
# Four properties, each load-bearing:
|
|
626
656
|
#
|
|
627
657
|
# · BACKGROUND. The refresh is `&`-detached with stdout/stderr closed, so a
|
|
628
658
|
# commit never waits on a parse. Git's own runtime is unchanged.
|
|
629
|
-
# · SINGLE-FLIGHT. `
|
|
630
|
-
#
|
|
631
|
-
#
|
|
632
|
-
# rebuilds
|
|
633
|
-
#
|
|
659
|
+
# · SINGLE-FLIGHT. `refresh --single-flight` takes the engine's own mkdir
|
|
660
|
+
# lock (code_graph/refresh_lock.ts), the one the session-start concern and
|
|
661
|
+
# a manual `map` take too, so of two concurrent commits exactly one
|
|
662
|
+
# rebuilds. This is Risk 3 in the roadmap's register — "git hooks pile up
|
|
663
|
+
# rebuilds". The hook used to hold its own shell lock; a ratification round
|
|
664
|
+
# of 2026-10-23 found two lock protocols racing on one directory, so there
|
|
665
|
+
# is now one, and its stale-lock reclaim is mutex-guarded and re-checked.
|
|
634
666
|
# · BUDGETED. `--budget-seconds` is the engine's own wall-clock ceiling; a
|
|
635
667
|
# child that exceeds it is SIGKILLed and the OLD cache is left untouched
|
|
636
668
|
# (cli.ts's refresh contract). Kill register K7 — never block on a stale
|
|
@@ -639,9 +671,8 @@ echo "✅ Auto-sync block appended to post-merge / post-checkout hooks." >&2
|
|
|
639
671
|
# exists. Building a graph for someone who never asked for one is the pure
|
|
640
672
|
# cost Risk 1 names; refreshing one they built is the service.
|
|
641
673
|
#
|
|
642
|
-
#
|
|
643
|
-
#
|
|
644
|
-
# blocking forever.
|
|
674
|
+
# `--map` redraws agents/map/ under the same lock, only for a project that has
|
|
675
|
+
# one (ADR-297).
|
|
645
676
|
|
|
646
677
|
append_code_graph_refresh_block() {
|
|
647
678
|
local name="$1"
|
|
@@ -657,30 +688,18 @@ if [ "${3:-1}" = "0" ]; then
|
|
|
657
688
|
fi
|
|
658
689
|
EOF
|
|
659
690
|
fi
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
if [
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
if [ -z "$(find "$cg_lock" -maxdepth 0 -mmin -5 2>/dev/null)" ]; then
|
|
668
|
-
rmdir "$cg_lock" 2>/dev/null || true
|
|
669
|
-
fi
|
|
670
|
-
fi
|
|
671
|
-
# Atomic: exactly one of N concurrent hooks wins this, and the losers do
|
|
672
|
-
# nothing rather than queueing a second rebuild behind the first.
|
|
673
|
-
if mkdir "$cg_lock" 2>/dev/null; then
|
|
674
|
-
(
|
|
675
|
-
trap 'rmdir "$cg_lock" 2>/dev/null || true' EXIT
|
|
676
|
-
./agent-config code-graph refresh --budget-seconds 60 >/dev/null 2>&1 || true
|
|
677
|
-
# The map follows the index it is drawn from, but only for a
|
|
678
|
-
# project that asked for one: no agents/map/, no map (ADR-297).
|
|
679
|
-
if [ -d agents/map ]; then
|
|
680
|
-
./agent-config code-graph map --no-verify --no-build >/dev/null 2>&1 || true
|
|
681
|
-
fi
|
|
682
|
-
) >/dev/null 2>&1 &
|
|
691
|
+
if [ "$name" = "post-rewrite" ]; then
|
|
692
|
+
cat >> "$HOOKS_DIR/$name" << 'EOF'
|
|
693
|
+
# An amend already fired post-commit; only a finished rebase is news here.
|
|
694
|
+
if [ "${1:-}" != "rebase" ]; then
|
|
695
|
+
exit 0
|
|
696
|
+
fi
|
|
697
|
+
EOF
|
|
683
698
|
fi
|
|
699
|
+
cat >> "$HOOKS_DIR/$name" << 'EOF'
|
|
700
|
+
if [ -f agents/runtime/state/code-graph-v1.json ] && [ -x ./agent-config ]; then
|
|
701
|
+
./agent-config code-graph refresh --single-flight --map --budget-seconds 60 \
|
|
702
|
+
</dev/null >/dev/null 2>&1 &
|
|
684
703
|
fi
|
|
685
704
|
exit 0
|
|
686
705
|
EOF
|
|
@@ -688,4 +707,6 @@ EOF
|
|
|
688
707
|
|
|
689
708
|
append_code_graph_refresh_block "post-commit"
|
|
690
709
|
append_code_graph_refresh_block "post-checkout"
|
|
691
|
-
|
|
710
|
+
append_code_graph_refresh_block "post-merge"
|
|
711
|
+
append_code_graph_refresh_block "post-rewrite"
|
|
712
|
+
echo "✅ Code-graph refresh block appended to post-commit / post-checkout / post-merge / post-rewrite hooks." >&2
|
package/src/scripts/install.ts
CHANGED
|
@@ -125,7 +125,7 @@ import { installerThinsHost } from './_lib/lean_projection_mode.js';
|
|
|
125
125
|
import { GLOBAL_DEPLOY_SOURCES, RULE_SOURCE_REL, dryRunTargetLines } from '../install/wizard-plan.js';
|
|
126
126
|
import { flattenSurface, computeSurfaceDelta, type SettingsSurface } from '../shared/settingsSurface.js';
|
|
127
127
|
import { settingsSchema } from '../server/schemas/settings.js';
|
|
128
|
-
import {
|
|
128
|
+
import { toJsonSchema7 } from '../shared/zodJsonSchema.js';
|
|
129
129
|
import { detect_module_roots } from './_lib/module_detection.js';
|
|
130
130
|
import {
|
|
131
131
|
TIER_TO_CLAUDE_MODEL,
|
|
@@ -3671,11 +3671,7 @@ const SETTINGS_SURFACE_REL = path.join('state', 'settings-surface.json');
|
|
|
3671
3671
|
const SETTINGS_DELTA_REL = path.join('state', 'settings-delta.json');
|
|
3672
3672
|
|
|
3673
3673
|
function _current_settings_surface(version: string): SettingsSurface {
|
|
3674
|
-
const jsonSchema =
|
|
3675
|
-
name: 'AgentSettings',
|
|
3676
|
-
$refStrategy: 'none',
|
|
3677
|
-
target: 'jsonSchema7',
|
|
3678
|
-
}) as Parameters<typeof flattenSurface>[0];
|
|
3674
|
+
const jsonSchema = toJsonSchema7(settingsSchema, 'AgentSettings') as Parameters<typeof flattenSurface>[0];
|
|
3679
3675
|
return flattenSurface(jsonSchema, version);
|
|
3680
3676
|
}
|
|
3681
3677
|
|
|
@@ -30,7 +30,13 @@
|
|
|
30
30
|
* 4. Pinning — `source_ref` carries a revision or is explicitly opaque; a
|
|
31
31
|
* bare URL is rejected because it cannot be re-verified later.
|
|
32
32
|
* 5. Orphan citations — every `<!-- harvest:<id> -->` marker under the
|
|
33
|
-
* scanned roots resolves to a ledger
|
|
33
|
+
* scanned roots resolves to a row in either ledger.
|
|
34
|
+
* 6. Mechanism decisions — `provenance/mechanisms.jsonl`, the source-free
|
|
35
|
+
* decision ledger: a closed shape with no source id, pin or locator
|
|
36
|
+
* (council O5), adopt|adapt carrying existing `realised_in` paths, reject
|
|
37
|
+
* carrying a reason, gap|deferred carrying a roadmap or `open`. A
|
|
38
|
+
* mechanism may repeat — the ledger is append-only and a later row
|
|
39
|
+
* supersedes an earlier one.
|
|
34
40
|
*
|
|
35
41
|
* An EMPTY ledger with zero markers is a legitimate pass and is reported as
|
|
36
42
|
* such on stdout. A gate that scans nothing and exits green silently is
|
|
@@ -57,6 +63,7 @@ const _HERE = path.dirname(_FILE);
|
|
|
57
63
|
// src/scripts/lint_harvest_provenance.ts → two levels up is the repo root.
|
|
58
64
|
const REPO = path.resolve(_HERE, '..', '..');
|
|
59
65
|
const LEDGER_REL = 'provenance/harvests.jsonl';
|
|
66
|
+
const MECHANISMS_REL = 'provenance/mechanisms.jsonl';
|
|
60
67
|
|
|
61
68
|
/** Roots scanned for citation markers. Markdown-bearing authored surfaces. */
|
|
62
69
|
export const CITATION_SCAN_ROOTS: readonly string[] = [
|
|
@@ -102,11 +109,23 @@ export interface HarvestRecord {
|
|
|
102
109
|
readonly verdict: 'adopt' | 'adapt';
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
export interface MechanismRecord {
|
|
113
|
+
readonly mechanism_id: string;
|
|
114
|
+
readonly verdict: 'adopt' | 'adapt' | 'reject' | 'already' | 'gap' | 'deferred';
|
|
115
|
+
readonly realised_in?: readonly string[];
|
|
116
|
+
readonly roadmap?: string;
|
|
117
|
+
readonly decided_at: string;
|
|
118
|
+
readonly reason?: string;
|
|
119
|
+
readonly reopen_if?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
105
122
|
export interface Finding {
|
|
106
123
|
/** Ledger line number, or 0 for findings that belong to no single row. */
|
|
107
124
|
readonly line: number;
|
|
108
|
-
readonly rule: 'schema' | 'uniqueness' | 'dead-row' | 'pinning' | 'orphan-citation';
|
|
125
|
+
readonly rule: 'schema' | 'uniqueness' | 'dead-row' | 'pinning' | 'orphan-citation' | 'verdict-requirement';
|
|
109
126
|
readonly message: string;
|
|
127
|
+
/** The ledger the line number refers to; absent means the harvest ledger. */
|
|
128
|
+
readonly ledger?: string;
|
|
110
129
|
}
|
|
111
130
|
|
|
112
131
|
function isValidCalendarDate(s: string): boolean {
|
|
@@ -227,6 +246,100 @@ export function validateRecord(raw: unknown, line: number, repoRoot: string): Fi
|
|
|
227
246
|
return findings;
|
|
228
247
|
}
|
|
229
248
|
|
|
249
|
+
const MECHANISM_FIELDS = ['mechanism_id', 'verdict', 'realised_in', 'roadmap', 'decided_at', 'reason', 'reopen_if'] as const;
|
|
250
|
+
const MECHANISM_REQUIRED = ['mechanism_id', 'verdict', 'decided_at'] as const;
|
|
251
|
+
const MECHANISM_VERDICTS: ReadonlySet<string> = new Set(['adopt', 'adapt', 'reject', 'already', 'gap', 'deferred']);
|
|
252
|
+
|
|
253
|
+
/** `rel` (with an optional `:<line>`) is a repo-relative path that exists; else the finding. */
|
|
254
|
+
function repoPathFinding(rel: string, field: string, line: number, repoRoot: string): Finding | null {
|
|
255
|
+
const bare = stripLineSuffix(rel);
|
|
256
|
+
const resolved = path.resolve(repoRoot, bare);
|
|
257
|
+
if (bare.trim() === '' || path.isAbsolute(bare) || !(resolved === repoRoot || resolved.startsWith(repoRoot + path.sep))) {
|
|
258
|
+
return { line, rule: 'schema', message: `${field} '${rel}' must be a repo-relative path inside the repo`, ledger: MECHANISMS_REL };
|
|
259
|
+
}
|
|
260
|
+
if (!fs.existsSync(resolved)) {
|
|
261
|
+
return { line, rule: 'dead-row', message: `${field} '${bare}' does not exist — the decision points at nothing`, ledger: MECHANISMS_REL };
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Validate one `mechanisms.jsonl` row: closed and source-free, then the per-verdict requirement. */
|
|
267
|
+
export function validateMechanismRecord(raw: unknown, line: number, repoRoot: string): Finding[] {
|
|
268
|
+
const at = (rule: Finding['rule'], message: string): Finding => ({ line, rule, message, ledger: MECHANISMS_REL });
|
|
269
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return [at('schema', 'record is not a JSON object')];
|
|
270
|
+
const rec = raw as Record<string, unknown>;
|
|
271
|
+
const findings: Finding[] = [];
|
|
272
|
+
|
|
273
|
+
for (const k of Object.keys(rec)) {
|
|
274
|
+
if ((MECHANISM_FIELDS as readonly string[]).includes(k)) continue;
|
|
275
|
+
findings.push(
|
|
276
|
+
at(
|
|
277
|
+
'schema',
|
|
278
|
+
/source|pin|locator/.test(k)
|
|
279
|
+
? `field '${k}' links a source to a mechanism — the tracked decision ledger is source-free (council O5); it belongs in the local lineage ledger`
|
|
280
|
+
: `unexpected field '${k}' — the ledger schema is closed to [${MECHANISM_FIELDS.join(', ')}]`,
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
for (const k of MECHANISM_REQUIRED) if (!(k in rec)) findings.push(at('schema', `missing required field '${k}'`));
|
|
285
|
+
|
|
286
|
+
if ('mechanism_id' in rec && (typeof rec.mechanism_id !== 'string' || !HARVEST_ID_RE.test(rec.mechanism_id))) {
|
|
287
|
+
findings.push(at('schema', `mechanism_id must be a kebab-case slug, got ${JSON.stringify(rec.mechanism_id)}`));
|
|
288
|
+
}
|
|
289
|
+
if ('decided_at' in rec && !(typeof rec.decided_at === 'string' && isValidCalendarDate(rec.decided_at))) {
|
|
290
|
+
findings.push(at('schema', `decided_at must be an ISO-8601 date (YYYY-MM-DD), got ${JSON.stringify(rec.decided_at)}`));
|
|
291
|
+
}
|
|
292
|
+
for (const k of ['reason', 'reopen_if'] as const) {
|
|
293
|
+
if (k in rec && (typeof rec[k] !== 'string' || (rec[k] as string).trim().length < EVIDENCE_MIN_LENGTH)) {
|
|
294
|
+
findings.push(at('schema', `${k} must be at least ${EVIDENCE_MIN_LENGTH} characters`));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const realised = rec.realised_in;
|
|
298
|
+
if (realised !== undefined && (!Array.isArray(realised) || realised.length === 0 || realised.some((r) => typeof r !== 'string'))) {
|
|
299
|
+
findings.push(at('schema', 'realised_in must be a non-empty list of repo-relative paths'));
|
|
300
|
+
} else if (Array.isArray(realised)) {
|
|
301
|
+
for (const r of realised as string[]) {
|
|
302
|
+
const f = repoPathFinding(r, 'realised_in', line, repoRoot);
|
|
303
|
+
if (f) findings.push(f);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if ('roadmap' in rec) {
|
|
307
|
+
if (typeof rec.roadmap !== 'string') findings.push(at('schema', "roadmap must be a repo-relative path or 'open'"));
|
|
308
|
+
else if (rec.roadmap !== 'open') {
|
|
309
|
+
const f = repoPathFinding(rec.roadmap, 'roadmap', line, repoRoot);
|
|
310
|
+
if (f) findings.push(f);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const verdict = rec.verdict;
|
|
315
|
+
if ('verdict' in rec && (typeof verdict !== 'string' || !MECHANISM_VERDICTS.has(verdict))) {
|
|
316
|
+
findings.push(at('schema', `verdict must be one of [${[...MECHANISM_VERDICTS].join(', ')}], got ${JSON.stringify(verdict)}`));
|
|
317
|
+
} else if ((verdict === 'adopt' || verdict === 'adapt') && realised === undefined) {
|
|
318
|
+
findings.push(at('verdict-requirement', `verdict '${verdict}' needs realised_in — an adoption that lives nowhere is not one`));
|
|
319
|
+
} else if (verdict === 'reject' && !('reason' in rec)) {
|
|
320
|
+
findings.push(at('verdict-requirement', "verdict 'reject' needs a reason — a rejection without one is re-litigated on every round"));
|
|
321
|
+
} else if ((verdict === 'gap' || verdict === 'deferred') && !('roadmap' in rec)) {
|
|
322
|
+
findings.push(at('verdict-requirement', `verdict '${verdict}' needs a roadmap that owns it, or 'open'`));
|
|
323
|
+
}
|
|
324
|
+
return findings;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Parse + validate every mechanism row. Repeats are allowed: a later row supersedes. */
|
|
328
|
+
export function lintMechanismsText(
|
|
329
|
+
text: string,
|
|
330
|
+
repoRoot: string,
|
|
331
|
+
): { readonly records: MechanismRecord[]; readonly findings: Finding[] } {
|
|
332
|
+
const { parsed, findings: parseFindings } = parseLedgerText(text);
|
|
333
|
+
const findings: Finding[] = parseFindings.map((f) => ({ ...f, ledger: MECHANISMS_REL }));
|
|
334
|
+
const records: MechanismRecord[] = [];
|
|
335
|
+
for (const { line, value } of parsed) {
|
|
336
|
+
const f = validateMechanismRecord(value, line, repoRoot);
|
|
337
|
+
findings.push(...f);
|
|
338
|
+
if (f.length === 0) records.push(value as MechanismRecord);
|
|
339
|
+
}
|
|
340
|
+
return { records, findings };
|
|
341
|
+
}
|
|
342
|
+
|
|
230
343
|
// ─── ledger parsing ──────────────────────────────────────────────────────────
|
|
231
344
|
|
|
232
345
|
export interface ParsedLine {
|
|
@@ -371,18 +484,19 @@ export function collectPersonaSources(repoRoot: string, personaRoot = 'src/agent
|
|
|
371
484
|
return citations;
|
|
372
485
|
}
|
|
373
486
|
|
|
374
|
-
/** Every citation must resolve to a ledger
|
|
487
|
+
/** Every citation must resolve to a row in either ledger. */
|
|
375
488
|
export function findOrphanCitations(
|
|
376
489
|
citations: readonly Citation[],
|
|
377
490
|
records: readonly HarvestRecord[],
|
|
491
|
+
mechanisms: readonly MechanismRecord[] = [],
|
|
378
492
|
): Finding[] {
|
|
379
|
-
const known = new Set(records.map((r) => r.harvest_id));
|
|
493
|
+
const known = new Set([...records.map((r) => r.harvest_id), ...mechanisms.map((m) => m.mechanism_id)]);
|
|
380
494
|
return citations
|
|
381
495
|
.filter((c) => !known.has(c.id))
|
|
382
496
|
.map((c) => ({
|
|
383
497
|
line: 0,
|
|
384
498
|
rule: 'orphan-citation' as const,
|
|
385
|
-
message: `${c.file} cites harvest '${c.id}', which has no row in ${LEDGER_REL}`,
|
|
499
|
+
message: `${c.file} cites harvest '${c.id}', which has no row in ${LEDGER_REL} or ${MECHANISMS_REL}`,
|
|
386
500
|
}));
|
|
387
501
|
}
|
|
388
502
|
|
|
@@ -422,6 +536,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
422
536
|
// the run would report a clean green over a file that no longer exists.
|
|
423
537
|
try {
|
|
424
538
|
assertWatchlistResolves({ gate: 'lint_harvest_provenance', candidates: [LEDGER_REL], repoRoot: REPO });
|
|
539
|
+
assertWatchlistResolves({ gate: 'lint_harvest_provenance', candidates: [MECHANISMS_REL], repoRoot: REPO });
|
|
425
540
|
} catch (exc) {
|
|
426
541
|
if (exc instanceof DeadScopeError) {
|
|
427
542
|
process.stderr.write(`❌ ${exc.message}\n`);
|
|
@@ -432,9 +547,11 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
432
547
|
|
|
433
548
|
const text = fs.readFileSync(ledgerPath, 'utf-8');
|
|
434
549
|
const { records, findings } = lintLedgerText(text, REPO);
|
|
550
|
+
const mechText = fs.readFileSync(path.join(REPO, MECHANISMS_REL), 'utf-8');
|
|
551
|
+
const { records: mechanisms, findings: mechFindings } = lintMechanismsText(mechText, REPO);
|
|
435
552
|
const citations = [...collectCitations(REPO), ...collectPersonaSources(REPO)];
|
|
436
|
-
const orphans = findOrphanCitations(citations, records);
|
|
437
|
-
const allFindings = [...findings, ...orphans];
|
|
553
|
+
const orphans = findOrphanCitations(citations, records, mechanisms);
|
|
554
|
+
const allFindings = [...findings, ...mechFindings, ...orphans];
|
|
438
555
|
|
|
439
556
|
// Per-target completeness accounting over the two populations this gate
|
|
440
557
|
// reasons about: the ledger's own rows, and the citations that point at
|
|
@@ -444,7 +561,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
444
561
|
// failure the success message above already tries to talk its way out of in
|
|
445
562
|
// prose. The ledger states it structurally instead.
|
|
446
563
|
const ledger = new GateLedger('lint_harvest_provenance');
|
|
447
|
-
const known = new Set(records.map((r) => r.harvest_id));
|
|
564
|
+
const known = new Set([...records.map((r) => r.harvest_id), ...mechanisms.map((m) => m.mechanism_id)]);
|
|
448
565
|
// The row population is every PARSED ledger line, not `records`: a row that
|
|
449
566
|
// fails validation or uniqueness is `continue`d and never reaches `records`,
|
|
450
567
|
// so planning `records` would plan exactly the rows that passed and account
|
|
@@ -452,8 +569,11 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
452
569
|
// 100 %. Lines are the stable key; a Finding already carries one.
|
|
453
570
|
const { parsed } = parseLedgerText(text);
|
|
454
571
|
const badLines = new Set(findings.filter((f) => f.line > 0).map((f) => f.line));
|
|
572
|
+
const { parsed: mechParsed } = parseLedgerText(mechText);
|
|
573
|
+
const badMechLines = new Set(mechFindings.filter((f) => f.line > 0).map((f) => f.line));
|
|
455
574
|
ledger.plan([
|
|
456
575
|
...parsed.map((r) => `row:${String(r.line)}`),
|
|
576
|
+
...mechParsed.map((r) => `mechanism:${String(r.line)}`),
|
|
457
577
|
...citations.map((c) => `citation:${c.id}@${c.file}`),
|
|
458
578
|
]);
|
|
459
579
|
for (const r of parsed) {
|
|
@@ -461,6 +581,11 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
461
581
|
if (badLines.has(r.line)) ledger.fail(id, 'ledger-row finding');
|
|
462
582
|
else ledger.complete(id);
|
|
463
583
|
}
|
|
584
|
+
for (const r of mechParsed) {
|
|
585
|
+
const id = `mechanism:${String(r.line)}`;
|
|
586
|
+
if (badMechLines.has(r.line)) ledger.fail(id, 'mechanism-row finding');
|
|
587
|
+
else ledger.complete(id);
|
|
588
|
+
}
|
|
464
589
|
for (const c of citations) {
|
|
465
590
|
const id = `citation:${c.id}@${c.file}`;
|
|
466
591
|
if (known.has(c.id)) ledger.complete(id);
|
|
@@ -470,7 +595,8 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
470
595
|
|
|
471
596
|
if (allFindings.length > 0) {
|
|
472
597
|
for (const f of allFindings) {
|
|
473
|
-
const
|
|
598
|
+
const file = f.ledger ?? LEDGER_REL;
|
|
599
|
+
const at = f.line > 0 ? `${file}:${f.line}` : file;
|
|
474
600
|
process.stderr.write(`❌ ${at}: [${f.rule}] ${f.message}\n`);
|
|
475
601
|
}
|
|
476
602
|
process.stderr.write(
|
|
@@ -484,7 +610,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
484
610
|
// that does not distinguish "nothing to check" from "everything checked"
|
|
485
611
|
// is the failure mode this repo has already paid for once.
|
|
486
612
|
process.stdout.write(
|
|
487
|
-
`✅ lint_harvest_provenance: ${records.length} ledger row(s) OK · ` +
|
|
613
|
+
`✅ lint_harvest_provenance: ${records.length} ledger row(s) OK · ${mechanisms.length} mechanism row(s) OK · ` +
|
|
488
614
|
`${citations.length} citation(s) resolved across ${CITATION_SCAN_ROOTS.length} scanned root(s)` +
|
|
489
615
|
`${records.length === 0 && citations.length === 0 ? ' — ledger is empty and nothing cites it, which is a legitimate pass' : ''}\n`,
|
|
490
616
|
);
|
|
@@ -514,4 +640,4 @@ if (_isCliEntry()) {
|
|
|
514
640
|
}
|
|
515
641
|
}
|
|
516
642
|
|
|
517
|
-
export { REPO, LEDGER_REL, ExitCode };
|
|
643
|
+
export { REPO, LEDGER_REL, MECHANISMS_REL, ExitCode };
|
|
@@ -69,6 +69,7 @@ const DEFAULT_HOST_LOWERING = path.join(HOOKS_DIR, "host_lowering.yaml");
|
|
|
69
69
|
* - `memory` reads or writes the memory layer
|
|
70
70
|
* - `notification` tells the human something and changes nothing else
|
|
71
71
|
* - `formatting` reshapes a command or a generated file
|
|
72
|
+
* - `execution` starts a process that outlives the event it fired on
|
|
72
73
|
*/
|
|
73
74
|
export const CONCERN_EFFECTS: ReadonlySet<string> = new Set([
|
|
74
75
|
"permission",
|
|
@@ -78,6 +79,7 @@ export const CONCERN_EFFECTS: ReadonlySet<string> = new Set([
|
|
|
78
79
|
"memory",
|
|
79
80
|
"notification",
|
|
80
81
|
"formatting",
|
|
82
|
+
"execution",
|
|
81
83
|
]);
|
|
82
84
|
|
|
83
85
|
// Canonical event vocabulary — keep in lock-step with
|