@event4u/agent-config 17.2.1 → 17.3.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.
Files changed (152) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/skills/install-agent-config/SKILL.md +9 -9
  5. package/CHANGELOG.md +152 -0
  6. package/dist/agent-src/skills/code-intelligence/SKILL.md +13 -12
  7. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  8. package/dist/agent-src/templates/scripts/check_memory.ts +1 -1
  9. package/dist/cli/commands/recordTriggerEval.js +1 -1
  10. package/dist/cli-delegate/{chunk-KR5ZIFPO.js → chunk-DHSSP7V3.js} +1 -1
  11. package/dist/cli-delegate/chunk-E7ACTJTJ.js +6792 -0
  12. package/dist/cli-delegate/{chunk-BZJ4Y3CI.js → chunk-MT2WOOCY.js} +1 -1
  13. package/dist/cli-delegate/{chunk-CMZEKSNN.js → chunk-ORJPBER2.js} +2 -2
  14. package/dist/cli-delegate/{chunk-VYZGJLCA.js → chunk-RKNT4LIK.js} +22 -1286
  15. package/dist/cli-delegate/cmd_conformance.js +20 -14
  16. package/dist/cli-delegate/cmd_doctor.js +3 -3
  17. package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
  18. package/dist/cli-delegate/cmd_explain.js +1 -1
  19. package/dist/cli-delegate/cmd_export.js +2 -2
  20. package/dist/cli-delegate/cmd_prune.js +2 -2
  21. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  22. package/dist/cli-delegate/cmd_settings_set.js +8 -4
  23. package/dist/cli-delegate/cmd_sync.js +2 -2
  24. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  25. package/dist/cli-delegate/cmd_validate.js +2 -2
  26. package/dist/discovery/deprecation-report.md +1 -1
  27. package/dist/discovery/discovery-manifest.json +3 -3
  28. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  29. package/dist/discovery/discovery-manifest.summary.md +1 -1
  30. package/dist/discovery/dormancy-report.md +1 -1
  31. package/dist/discovery/orphan-report.md +1 -1
  32. package/dist/discovery/packs.json +2 -2
  33. package/dist/discovery/trust-report.md +1 -1
  34. package/dist/discovery/workspaces.json +2 -2
  35. package/dist/hooks/code_graph_refresh.chunk.cjs +414 -0
  36. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -0
  37. package/dist/hooks/dispatch.js +21 -11
  38. package/dist/hooks/dispatch.sha256 +1 -1
  39. package/dist/hooks/egress_engine.chunk.cjs +1 -1
  40. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  41. package/dist/install/install.mjs +6974 -6037
  42. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -0
  43. package/dist/mcp/registry-manifest.json +3 -2
  44. package/dist/mcp/server.json +13 -4
  45. package/dist/mcp/server.mjs +5561 -2463
  46. package/dist/scripts/_lib/egress_rules.js +8 -2
  47. package/dist/scripts/condense.js +9 -9
  48. package/dist/server/routes/discovery.js +1 -1
  49. package/dist/server/routes/install.js +1 -1
  50. package/dist/server/routes/ping.js +1 -1
  51. package/dist/server/routes/schema.js +4 -4
  52. package/dist/server/routes/settings.js +2 -6
  53. package/dist/server/routes/wizard.js +6 -6
  54. package/dist/server/schemas/settings.js +42 -42
  55. package/dist/shared/settingsSurface.js +1 -1
  56. package/dist/shared/userMd/schema.js +1 -1
  57. package/dist/shared/zodJsonSchema.js +47 -0
  58. package/dist/ui/assets/index-B0bcIgi0.js +77 -0
  59. package/dist/ui/index.html +1 -1
  60. package/package.json +3 -3
  61. package/src/agent-src/templates/scripts/check_memory.ts +1 -1
  62. package/src/config/ci-local-parity.yml +11 -0
  63. package/src/config/continuity-surface.json +9 -2
  64. package/src/config/gate-violation-baselines.json +3 -3
  65. package/src/config/gitignore-block.txt +6 -5
  66. package/src/config/hook-bundle-budget.json +16 -1
  67. package/src/config/pack-size-budget.json +12 -1
  68. package/src/config/packed-binary-manifest.json +9 -0
  69. package/src/scripts/_cli/cmd_settings_set.ts +3 -2
  70. package/src/scripts/_dispatch.bash +1 -1
  71. package/src/scripts/_lib/egress_rules.ts +8 -2
  72. package/src/scripts/_lib/listing_manifests.ts +85 -0
  73. package/src/scripts/_lib/neighbour_census.ts +1 -0
  74. package/src/scripts/_lib/release_settings_surface.ts +2 -6
  75. package/src/scripts/adoption_status.ts +231 -27
  76. package/src/scripts/build_mcp_registry_manifest.ts +23 -12
  77. package/src/scripts/check_dependency_floors.ts +7 -0
  78. package/src/scripts/check_release_manifest_content.ts +17 -2
  79. package/src/scripts/check_release_pr_shape.ts +5 -0
  80. package/src/scripts/code_graph/build.ts +80 -44
  81. package/src/scripts/code_graph/cli.ts +260 -16
  82. package/src/scripts/code_graph/conventions_default.ts +228 -0
  83. package/src/scripts/code_graph/extract.ts +5 -2
  84. package/src/scripts/code_graph/extract_more.ts +105 -1
  85. package/src/scripts/code_graph/loader.ts +10 -2
  86. package/src/scripts/code_graph/map.ts +174 -41
  87. package/src/scripts/code_graph/map_model.ts +53 -15
  88. package/src/scripts/code_graph/map_verify.ts +12 -0
  89. package/src/scripts/code_graph/map_viewer/viewer.css +6 -0
  90. package/src/scripts/code_graph/map_viewer/viewer.js +114 -13
  91. package/src/scripts/code_graph/passes.ts +173 -0
  92. package/src/scripts/code_graph/providers/angular.ts +55 -0
  93. package/src/scripts/code_graph/providers/aspnetcore.ts +182 -0
  94. package/src/scripts/code_graph/providers/catalog.ts +290 -0
  95. package/src/scripts/code_graph/providers/django.ts +163 -0
  96. package/src/scripts/code_graph/providers/express.ts +25 -0
  97. package/src/scripts/code_graph/providers/fastapi.ts +49 -0
  98. package/src/scripts/code_graph/providers/flask.ts +99 -0
  99. package/src/scripts/code_graph/providers/go_http.ts +121 -0
  100. package/src/scripts/code_graph/providers/index.ts +101 -0
  101. package/src/scripts/code_graph/{laravel_overlay.ts → providers/laravel.ts} +113 -7
  102. package/src/scripts/code_graph/providers/nestjs.ts +34 -0
  103. package/src/scripts/code_graph/providers/nextjs.ts +91 -0
  104. package/src/scripts/code_graph/providers/nuxt.ts +55 -0
  105. package/src/scripts/code_graph/providers/react-router.ts +58 -0
  106. package/src/scripts/code_graph/providers/shared/detect.ts +78 -0
  107. package/src/scripts/code_graph/providers/shared/extractors.ts +249 -0
  108. package/src/scripts/code_graph/providers/shared/js.ts +252 -0
  109. package/src/scripts/code_graph/providers/shared/kit.ts +210 -0
  110. package/src/scripts/code_graph/providers/shared/php.ts +64 -0
  111. package/src/scripts/code_graph/providers/shared/python.ts +223 -0
  112. package/src/scripts/code_graph/providers/spring.ts +138 -0
  113. package/src/scripts/code_graph/providers/sqlalchemy.ts +62 -0
  114. package/src/scripts/code_graph/providers/sveltekit.ts +59 -0
  115. package/src/scripts/code_graph/providers/symfony.ts +142 -0
  116. package/src/scripts/code_graph/providers/typeorm.ts +58 -0
  117. package/src/scripts/code_graph/providers/types.ts +56 -0
  118. package/src/scripts/code_graph/providers/vue-router.ts +21 -0
  119. package/src/scripts/code_graph/providers/wordpress.ts +128 -0
  120. package/src/scripts/code_graph/refresh_cli.ts +81 -0
  121. package/src/scripts/code_graph/refresh_lock.ts +193 -0
  122. package/src/scripts/code_graph/resolution_tiers.ts +19 -7
  123. package/src/scripts/code_graph/scip_reader.ts +2 -2
  124. package/src/scripts/code_graph/sqlite_store.ts +10 -4
  125. package/src/scripts/code_graph/types.ts +75 -41
  126. package/src/scripts/code_graph/validate.ts +6 -8
  127. package/src/scripts/condense.ts +9 -9
  128. package/src/scripts/generate_settings_reference.ts +2 -6
  129. package/src/scripts/hook_chunks.mjs +4 -0
  130. package/src/scripts/hook_manifest.json +1 -1
  131. package/src/scripts/hook_manifest.yaml +24 -7
  132. package/src/scripts/hooks/code_graph_refresh_hook.ts +160 -0
  133. package/src/scripts/hooks/lazy_concern.ts +4 -0
  134. package/src/scripts/install-hooks.sh +54 -33
  135. package/src/scripts/install.ts +2 -6
  136. package/src/scripts/lint_hook_manifest.ts +2 -0
  137. package/src/scripts/lint_marketplace.ts +33 -0
  138. package/src/scripts/lint_mcp_registry_manifest.ts +36 -0
  139. package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -0
  140. package/src/scripts/mcp_server/graph_tools.ts +5 -1
  141. package/src/scripts/release.ts +2 -5
  142. package/src/scripts/release_drill.ts +4 -1
  143. package/src/scripts/release_env.ts +17 -0
  144. package/src/scripts/release_mix_taxonomy.json +3 -0
  145. package/src/server/schemas/settings.ts +42 -42
  146. package/src/shared/settingsSurface.ts +1 -1
  147. package/src/shared/userMd/schema.ts +1 -1
  148. package/src/shared/zodJsonSchema.ts +41 -0
  149. package/src/vendor/grammars/README.md +6 -5
  150. package/src/vendor/grammars/tree-sitter-c_sharp.wasm +0 -0
  151. package/dist/cli-delegate/chunk-XIAPY44N.js +0 -4587
  152. package/dist/ui/assets/index-w70Ado09.js +0 -23
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The listing manifests outside the npm tarball that start the MCP server
3
+ * through `npx`: the GitHub Copilot discovery plugin and the Gemini CLI
4
+ * extension. Both carry the package version twice — in `version` and pinned
5
+ * inside the `npx` argument — and a crawler installs whatever the default
6
+ * branch says, so both copies follow `package.json` through the release.
7
+ *
8
+ * One module because three readers must agree on the same two facts: the
9
+ * release writes them (`release_env`), the release-PR content gate admits a
10
+ * change to them and nothing else (`check_release_manifest_content`), and the
11
+ * marketplace lint fails a tree where they drifted (`lint_marketplace`).
12
+ */
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+
16
+ export const LISTING_MANIFESTS = ['.github/plugin/plugin.json', 'gemini-extension.json'] as const;
17
+
18
+ export const PACKAGE_NAME = '@event4u/agent-config';
19
+
20
+ /** An `npx` argument pinning this package to an exact release, prereleases included. */
21
+ export const PINNED_PACKAGE_ARG_RE = /^@event4u\/agent-config@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
22
+
23
+ export function pinnedPackageArg(version: string): string {
24
+ return `${PACKAGE_NAME}@${version}`;
25
+ }
26
+
27
+ type Obj = Record<string, unknown>;
28
+
29
+ function _isObject(v: unknown): v is Obj {
30
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
31
+ }
32
+
33
+ /** Every `[serverName, args]` pair under `mcpServers`, args left `unknown` for the caller to judge. */
34
+ export function mcpServerArgs(manifest: unknown): Array<[string, unknown]> {
35
+ if (!_isObject(manifest) || !_isObject(manifest['mcpServers'])) return [];
36
+ return Object.entries(manifest['mcpServers']).map(([name, server]) => [
37
+ name,
38
+ _isObject(server) ? server['args'] : undefined,
39
+ ]);
40
+ }
41
+
42
+ /**
43
+ * Every way `manifest` disagrees with `version`: the `version` field, and each
44
+ * server whose `npx` arguments do not pin exactly this release. An unpinned
45
+ * `@event4u/agent-config` argument is a finding too — it resolves to whatever
46
+ * is latest on the day a user installs, which is the drift being prevented.
47
+ */
48
+ export function listingVersionFindings(label: string, manifest: unknown, version: string): string[] {
49
+ const out: string[] = [];
50
+ const declared = _isObject(manifest) ? manifest['version'] : undefined;
51
+ if (declared !== version) {
52
+ out.push(`${label}: version \`${String(declared)}\` does not match package.json version \`${version}\``);
53
+ }
54
+ const servers = mcpServerArgs(manifest);
55
+ if (servers.length === 0) out.push(`${label}: declares no mcpServers entry`);
56
+ for (const [name, args] of servers) {
57
+ const own = Array.isArray(args)
58
+ ? args.filter((a): a is string => typeof a === 'string' && a.startsWith(PACKAGE_NAME))
59
+ : [];
60
+ if (own.length !== 1 || own[0] !== pinnedPackageArg(version)) {
61
+ out.push(
62
+ `${label}: mcpServers.${name}.args must pin \`${pinnedPackageArg(version)}\` ` +
63
+ `(found ${own.length === 0 ? 'none' : own.map((a) => `\`${a}\``).join(', ')})`,
64
+ );
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+
70
+ /** Rewrite `version` and every pinned package argument; `jsonDump` is the release's own serializer. */
71
+ export function setListingManifestVersion(p: string, version: string, jsonDump: (v: unknown) => string): void {
72
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8')) as unknown;
73
+ if (_isObject(data) && 'version' in data) data['version'] = version;
74
+ for (const [, args] of mcpServerArgs(data)) {
75
+ if (!Array.isArray(args)) continue;
76
+ args.forEach((a, i) => {
77
+ if (typeof a === 'string' && PINNED_PACKAGE_ARG_RE.test(a)) args[i] = pinnedPackageArg(version);
78
+ });
79
+ }
80
+ fs.writeFileSync(p, jsonDump(data) + '\n', 'utf-8');
81
+ }
82
+
83
+ export function listingManifestPaths(root: string): string[] {
84
+ return LISTING_MANIFESTS.map((rel) => path.join(root, rel));
85
+ }
@@ -86,6 +86,7 @@ export type Effect =
86
86
  | 'memory'
87
87
  | 'notification'
88
88
  | 'formatting'
89
+ | 'execution'
89
90
  | 'unknown';
90
91
 
91
92
  export interface NeighbourEntry {
@@ -33,7 +33,7 @@
33
33
  import * as fs from 'node:fs';
34
34
  import * as path from 'node:path';
35
35
 
36
- import { zodToJsonSchema } from 'zod-to-json-schema';
36
+ import { toJsonSchema7 } from '../../shared/zodJsonSchema.js';
37
37
 
38
38
  import { settingsSchema } from '../../server/schemas/settings.js';
39
39
  import { computeSurfaceDelta, flattenSurface, type SettingsSurface, type SurfaceEntry } from '../../shared/settingsSurface.js';
@@ -50,11 +50,7 @@ import { RENAMED_KEYS, SETTINGS_REMOVAL_RULE } from './settings_renamed_keys.js'
50
50
  * "keep a preview fast" rule was written about.
51
51
  */
52
52
  function _jsonSchema(): Parameters<typeof flattenSurface>[0] {
53
- return zodToJsonSchema(settingsSchema, {
54
- name: 'AgentSettings',
55
- $refStrategy: 'none',
56
- target: 'jsonSchema7',
57
- }) as Parameters<typeof flattenSurface>[0];
53
+ return toJsonSchema7(settingsSchema, 'AgentSettings') as Parameters<typeof flattenSurface>[0];
58
54
  }
59
55
 
60
56
  /** The one committed copy of the last release's surface. */
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * CLI:
24
24
  *
25
- * ./scripts-run src/scripts/adoption_status [--json] [--branch main] [--check-upstream]
25
+ * ./scripts-run src/scripts/adoption_status [--json] [--branch main] [--check-upstream] [--fail-on-action]
26
26
  *
27
27
  * `--check-upstream` reads each ledger row's upstream (`gh pr view` for a pull
28
28
  * request, one `curl` for a listing page or registry query) and appends what it
@@ -30,10 +30,19 @@
30
30
  * report exactly as before, and stderr names that mode plus every row whose
31
31
  * `Last read` date is missing or older than the sheet's re-read threshold.
32
32
  *
33
+ * With `--check-upstream`, a row whose sheet and upstream disagree in a way that
34
+ * needs a person also carries an action (`sheet-behind`, `needs-response`,
35
+ * `listing-lost`, `listing-stale`) next to its unchanged verdict, and the report
36
+ * names the next act. A submission is done when its entry is live, not when it
37
+ * was sent, so `--fail-on-action` turns any action into a non-zero exit for the
38
+ * daily watcher. Without `--check-upstream` there is nothing to judge and the
39
+ * flag is reported on stderr and ignored.
40
+ *
33
41
  * Exit codes:
34
42
  *
35
43
  * 0 — printed successfully (status itself does not gate the exit).
36
44
  * 1 — IO error reading the registry-submissions sheet.
45
+ * 2 — `--check-upstream --fail-on-action` and at least one row needs an action.
37
46
  */
38
47
 
39
48
  import { spawnSync } from 'node:child_process';
@@ -243,25 +252,29 @@ interface Args {
243
252
  json: boolean;
244
253
  branch: string;
245
254
  checkUpstream: boolean;
255
+ failOnAction: boolean;
246
256
  }
247
257
 
248
258
  export function parse_args(argv: string[]): Args {
249
259
  let json = false;
250
260
  let branch = 'main';
251
261
  let checkUpstream = false;
262
+ let failOnAction = false;
252
263
  for (let i = 0; i < argv.length; i++) {
253
264
  const a = argv[i];
254
265
  if (a === '--json') {
255
266
  json = true;
256
267
  } else if (a === '--check-upstream') {
257
268
  checkUpstream = true;
269
+ } else if (a === '--fail-on-action') {
270
+ failOnAction = true;
258
271
  } else if (a === '--branch') {
259
272
  branch = argv[++i] ?? '';
260
273
  } else if (a !== undefined && a.startsWith('--branch=')) {
261
274
  branch = a.slice('--branch='.length);
262
275
  }
263
276
  }
264
- return { json, branch, checkUpstream };
277
+ return { json, branch, checkUpstream, failOnAction };
265
278
  }
266
279
 
267
280
  export function main(argv: string[] = process.argv.slice(2)): number {
@@ -293,10 +306,21 @@ export function main(argv: string[] = process.argv.slice(2)): number {
293
306
  for (const u of unread) {
294
307
  process.stderr.write(` row ${u.id} ${u.registry}: ${u.label}\n`);
295
308
  }
309
+ if (args.failOnAction) {
310
+ process.stderr.write(
311
+ '--fail-on-action ignored: actions are judged only with --check-upstream\n',
312
+ );
313
+ }
296
314
  return 0;
297
315
  }
298
316
 
299
- const verdicts = judge_upstream(rows, live_upstream_reader, today);
317
+ const verdicts = judge_upstream(
318
+ rows,
319
+ live_upstream_reader,
320
+ today,
321
+ _releasedServer(),
322
+ parse_reread_threshold(registryText),
323
+ );
300
324
  if (args.json) {
301
325
  const base = JSON.parse(render_json(registryCounts, reports, ci, args.branch)) as Record<
302
326
  string,
@@ -308,7 +332,32 @@ export function main(argv: string[] = process.argv.slice(2)): number {
308
332
  process.stdout.write(render_text(registryCounts, reports, ci, args.branch) + '\n');
309
333
  process.stdout.write(render_upstream_text(verdicts, unread) + '\n');
310
334
  }
311
- return 0;
335
+ const code = upstream_exit_code(verdicts, args.failOnAction);
336
+ if (code !== 0) {
337
+ // On a runner the `::error::` prefix turns each line into an annotation
338
+ // naming the row, so the red job says what to do without opening the log.
339
+ const prefix = process.env.GITHUB_ACTIONS === 'true' ? '::error title=adoption_status::' : 'action: ';
340
+ for (const line of action_lines(verdicts)) {
341
+ process.stderr.write(prefix + line + '\n');
342
+ }
343
+ }
344
+ return code;
345
+ }
346
+
347
+ /** `name` and `version` the registry should carry, from package.json. */
348
+ function _releasedServer(): ReleasedServer | undefined {
349
+ try {
350
+ const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf-8')) as {
351
+ mcpName?: unknown;
352
+ version?: unknown;
353
+ };
354
+ if (typeof pkg.mcpName === 'string' && typeof pkg.version === 'string') {
355
+ return { name: pkg.mcpName, version: pkg.version };
356
+ }
357
+ } catch {
358
+ // Without a readable package.json there is nothing to compare against.
359
+ }
360
+ return undefined;
312
361
  }
313
362
 
314
363
  // --- upstream reading --------------------------------------------------------
@@ -325,7 +374,15 @@ export interface LedgerRow {
325
374
  }
326
375
 
327
376
  export type UpstreamReading =
328
- | { ok: true; state: 'open' | 'closed' | 'merged' | 'listed' | 'not-listed'; at?: string }
377
+ | {
378
+ ok: true;
379
+ state: 'open' | 'closed' | 'merged' | 'listed' | 'not-listed';
380
+ at?: string;
381
+ /** `gh pr view` reviewDecision, e.g. `CHANGES_REQUESTED`; pull requests only. */
382
+ reviewDecision?: string;
383
+ /** Server name → the version the registry marks latest; registry queries only. */
384
+ latestVersions?: Record<string, string>;
385
+ }
329
386
  | { ok: false; reason: string };
330
387
 
331
388
  export type UpstreamReader = (row: LedgerRow) => UpstreamReading;
@@ -340,11 +397,25 @@ export type UpstreamVerdict =
340
397
  | 'unreachable'
341
398
  | 'no-upstream';
342
399
 
400
+ /**
401
+ * Reported next to the verdict, never instead of it: the verdict says what the
402
+ * upstream reads, the action says the sheet and that reading disagree in a way a
403
+ * person has to resolve.
404
+ */
405
+ export type ActionVerdict = 'sheet-behind' | 'needs-response' | 'listing-lost' | 'listing-stale' | 'read-overdue';
406
+
343
407
  export interface RowVerdict {
344
408
  id: string;
345
409
  registry: string;
346
410
  verdict: UpstreamVerdict;
347
411
  detail: string;
412
+ action?: ActionVerdict;
413
+ next?: string;
414
+ }
415
+
416
+ export interface ReleasedServer {
417
+ name: string;
418
+ version: string;
348
419
  }
349
420
 
350
421
  export interface UnreadFinding {
@@ -451,6 +522,8 @@ export function judge_upstream(
451
522
  rows: LedgerRow[],
452
523
  reader: UpstreamReader,
453
524
  today: string,
525
+ released?: ReleasedServer,
526
+ thresholdDays: number = DEFAULT_REREAD_THRESHOLD_DAYS,
454
527
  ): RowVerdict[] {
455
528
  return rows.map((row): RowVerdict => {
456
529
  const base = { id: row.id, registry: row.registry };
@@ -459,27 +532,116 @@ export function judge_upstream(
459
532
  }
460
533
  const r = reader(row);
461
534
  if (!r.ok) {
462
- return { ...base, verdict: 'unreachable', detail: r.reason };
463
- }
464
- switch (r.state) {
465
- case 'closed':
466
- return { ...base, verdict: 'answered-closed', detail: `closed ${r.at ?? 'date unknown'}` };
467
- case 'merged':
468
- return { ...base, verdict: 'answered-merged', detail: `merged ${r.at ?? 'date unknown'}` };
469
- case 'listed':
470
- case 'not-listed':
471
- return { ...base, verdict: r.state, detail: row.ref };
472
- case 'open': {
473
- const age = row.date === '' ? null : _daysBetween(row.date, today);
474
- if (row.status === 'submitted' && age !== null && age >= STALLED_AFTER_DAYS) {
475
- return { ...base, verdict: 'stalled', detail: `open ${age} days, no answer` };
476
- }
477
- return { ...base, verdict: 'open', detail: age === null ? 'open' : `open ${age} days` };
535
+ const out = { ...base, verdict: 'unreachable' as const, detail: r.reason };
536
+ // One outage never reds the watcher; a listing nobody has been able to read
537
+ // for longer than the sheet's own threshold does, or it could vanish unseen.
538
+ const age = row.lastRead === '' ? null : _daysBetween(row.lastRead, today);
539
+ const live = row.status === 'accepted' || row.status === 'submitted';
540
+ if (live && (age === null || age > thresholdDays)) {
541
+ return {
542
+ ...out,
543
+ action: 'read-overdue',
544
+ next: `read the upstream another way (a headless browser) and set Last read; unread ${age === null ? 'ever' : `for ${age} days`}`,
545
+ };
478
546
  }
547
+ return out;
479
548
  }
549
+ const out = { ...base, ..._verdictOf(row, r, today) };
550
+ const action = _actionFor(row, r, released);
551
+ // Spread only when present: the JSON writer prints an undefined value as `null`.
552
+ return action === null ? out : { ...out, ...action };
480
553
  });
481
554
  }
482
555
 
556
+ function _verdictOf(
557
+ row: LedgerRow,
558
+ r: Extract<UpstreamReading, { ok: true }>,
559
+ today: string,
560
+ ): { verdict: UpstreamVerdict; detail: string } {
561
+ switch (r.state) {
562
+ case 'closed':
563
+ return { verdict: 'answered-closed', detail: `closed ${r.at ?? 'date unknown'}` };
564
+ case 'merged':
565
+ return { verdict: 'answered-merged', detail: `merged ${r.at ?? 'date unknown'}` };
566
+ case 'listed':
567
+ case 'not-listed':
568
+ return { verdict: r.state, detail: row.ref };
569
+ case 'open': {
570
+ const age = row.date === '' ? null : _daysBetween(row.date, today);
571
+ if (row.status === 'submitted' && age !== null && age >= STALLED_AFTER_DAYS) {
572
+ return { verdict: 'stalled', detail: `open ${age} days, no answer` };
573
+ }
574
+ return { verdict: 'open', detail: age === null ? 'open' : `open ${age} days` };
575
+ }
576
+ }
577
+ }
578
+
579
+ /**
580
+ * The cases where the sheet and the live upstream disagree in a way that needs a
581
+ * person. `rejected` and `stalled` rows are left alone: the sheet already says
582
+ * the attempt ended, and an unreadable upstream is never an action, so a network
583
+ * outage cannot red the watcher.
584
+ */
585
+ function _actionFor(
586
+ row: LedgerRow,
587
+ r: Extract<UpstreamReading, { ok: true }>,
588
+ released: ReleasedServer | undefined,
589
+ ): { action: ActionVerdict; next: string } | null {
590
+ const awaiting = row.status === 'submitted' || row.status === 'pending';
591
+ if (awaiting && (r.state === 'closed' || r.state === 'merged')) {
592
+ return {
593
+ action: 'sheet-behind',
594
+ next: `update the ledger row: the upstream ${r.state} ${r.at ?? 'on an unknown date'} while the sheet says ${row.status}`,
595
+ };
596
+ }
597
+ // Row 4's path after its first CI publish: the entry went live, the sheet still waits.
598
+ if (awaiting && r.state === 'listed') {
599
+ return {
600
+ action: 'sheet-behind',
601
+ next: `update the ledger row: the listing reads listed while the sheet says ${row.status}`,
602
+ };
603
+ }
604
+ if (r.state === 'open' && r.reviewDecision === 'CHANGES_REQUESTED') {
605
+ return { action: 'needs-response', next: `answer the review on ${row.ref}` };
606
+ }
607
+ if (row.status === 'accepted' && r.state === 'not-listed') {
608
+ return { action: 'listing-lost', next: `re-check or re-submit the listing at ${row.ref}` };
609
+ }
610
+ if (
611
+ row.status === 'accepted' &&
612
+ row.kind === 'registry-query' &&
613
+ r.state === 'listed' &&
614
+ released !== undefined &&
615
+ r.latestVersions !== undefined
616
+ ) {
617
+ const live = r.latestVersions[released.name];
618
+ if (live === undefined) {
619
+ return {
620
+ action: 'listing-lost',
621
+ next: `re-check or re-submit the listing: the registry answer carries no ${released.name}`,
622
+ };
623
+ }
624
+ if (live !== released.version) {
625
+ return {
626
+ action: 'listing-stale',
627
+ next: `publish ${released.version} to the registry: it carries ${live} for ${released.name}`,
628
+ };
629
+ }
630
+ }
631
+ return null;
632
+ }
633
+
634
+ /** The rows `--fail-on-action` reds on, one line each: row, action, next act. */
635
+ export function action_lines(verdicts: RowVerdict[]): string[] {
636
+ return verdicts
637
+ .filter((v) => v.action !== undefined)
638
+ .map((v) => `row ${v.id} ${v.action} (${v.registry}) — ${v.next ?? ''}`);
639
+ }
640
+
641
+ export function upstream_exit_code(verdicts: RowVerdict[], failOnAction: boolean): number {
642
+ return failOnAction && verdicts.some((v) => v.action !== undefined) ? 2 : 0;
643
+ }
644
+
483
645
  export function upstream_mode_note(checked: boolean): string {
484
646
  return checked
485
647
  ? 'upstream: read live (--check-upstream)'
@@ -508,6 +670,14 @@ export function render_upstream_text(verdicts: RowVerdict[], unread: UnreadFindi
508
670
  for (const u of unread) {
509
671
  lines.push(` row ${u.id.padEnd(3)} ${u.label} ${u.registry}`);
510
672
  }
673
+ const actions = action_lines(verdicts);
674
+ if (actions.length > 0) {
675
+ lines.push('');
676
+ lines.push('Actions needed (a submission is done only once it is live):');
677
+ for (const a of actions) {
678
+ lines.push(` ${a}`);
679
+ }
680
+ }
511
681
  return lines.join('\n');
512
682
  }
513
683
 
@@ -521,14 +691,14 @@ function _readPullRequest(ref: string): UpstreamReading {
521
691
  }
522
692
  const proc = spawnSync(
523
693
  'gh',
524
- ['pr', 'view', m[2] as string, '--repo', m[1] as string, '--json', 'state,closedAt,mergedAt'],
694
+ ['pr', 'view', m[2] as string, '--repo', m[1] as string, '--json', 'state,closedAt,mergedAt,reviewDecision'],
525
695
  { encoding: 'utf8', timeout: 20_000 },
526
696
  );
527
697
  if (proc.status !== 0) {
528
698
  const snip = sanitize_text(_pyStrip(proc.stderr ?? '')).slice(0, 80);
529
699
  return { ok: false, reason: `gh exit=${proc.status}: ${snip}` };
530
700
  }
531
- let d: { state?: string; closedAt?: string; mergedAt?: string };
701
+ let d: { state?: string; closedAt?: string; mergedAt?: string; reviewDecision?: string };
532
702
  try {
533
703
  d = JSON.parse(proc.stdout) as typeof d;
534
704
  } catch {
@@ -536,7 +706,10 @@ function _readPullRequest(ref: string): UpstreamReading {
536
706
  }
537
707
  if (d.state === 'MERGED') return { ok: true, state: 'merged', at: sanitize_text(d.mergedAt ?? '').slice(0, 10) };
538
708
  if (d.state === 'CLOSED') return { ok: true, state: 'closed', at: sanitize_text(d.closedAt ?? '').slice(0, 10) };
539
- if (d.state === 'OPEN') return { ok: true, state: 'open' };
709
+ if (d.state === 'OPEN') {
710
+ const review = sanitize_text(d.reviewDecision ?? '').slice(0, 40);
711
+ return review === '' ? { ok: true, state: 'open' } : { ok: true, state: 'open', reviewDecision: review };
712
+ }
540
713
  return { ok: false, reason: `unknown pull-request state ${sanitize_text(String(d.state)).slice(0, 40)}` };
541
714
  }
542
715
 
@@ -574,9 +747,21 @@ export function read_url(
574
747
  if (code === '404') return { ok: true, state: 'not-listed' };
575
748
  return { ok: false, reason: `HTTP ${code} from ${ref}` };
576
749
  }
577
- let body: { metadata?: { count?: unknown } };
750
+ return read_registry_answer(out.slice(0, cut), ref);
751
+ }
752
+
753
+ const _REGISTRY_META_KEY = 'io.modelcontextprotocol.registry/official';
754
+
755
+ /**
756
+ * Shape read live from `GET /v0/servers?search=` on 2026-10-24:
757
+ * `{servers: [{server: {name, version}, _meta: {"<key>": {isLatest}}}], metadata: {count}}`.
758
+ * A search returns every published version, so only the one flagged `isLatest`
759
+ * names what a client installs.
760
+ */
761
+ export function read_registry_answer(text: string, ref: string): UpstreamReading {
762
+ let body: { metadata?: { count?: unknown }; servers?: unknown };
578
763
  try {
579
- body = JSON.parse(out.slice(0, cut)) as typeof body;
764
+ body = JSON.parse(text) as typeof body;
580
765
  } catch {
581
766
  return { ok: false, reason: `registry answer from ${ref} not parseable as JSON` };
582
767
  }
@@ -584,7 +769,26 @@ export function read_url(
584
769
  if (typeof n !== 'number') {
585
770
  return { ok: false, reason: 'registry answer has no metadata.count' };
586
771
  }
587
- return { ok: true, state: n > 0 ? 'listed' : 'not-listed' };
772
+ if (n === 0) {
773
+ return { ok: true, state: 'not-listed' };
774
+ }
775
+ const latestVersions: Record<string, string> = {};
776
+ let flagged = false;
777
+ for (const entry of Array.isArray(body.servers) ? body.servers : []) {
778
+ const e = entry as {
779
+ server?: { name?: unknown; version?: unknown };
780
+ _meta?: Record<string, { isLatest?: unknown } | undefined>;
781
+ };
782
+ const name = e.server?.name;
783
+ const version = e.server?.version;
784
+ if (typeof name !== 'string' || typeof version !== 'string') continue;
785
+ if (e._meta?.[_REGISTRY_META_KEY]?.isLatest !== true) continue;
786
+ flagged = true;
787
+ latestVersions[sanitize_text(name).slice(0, 200)] = sanitize_text(version).slice(0, 40);
788
+ }
789
+ // No `isLatest` flag anywhere means the answer is not the shape read above;
790
+ // comparing against an empty map would report a live listing as lost.
791
+ return flagged ? { ok: true, state: 'listed', latestVersions } : { ok: true, state: 'listed' };
588
792
  }
589
793
 
590
794
  /** Reads the real upstream: `gh` for a pull request, `curl` otherwise. */
@@ -208,31 +208,40 @@ function _render_cf_json(m: JsonObject): string {
208
208
  return pyJsonDumps(payload, { indent: 2, sortKeys: true }) + '\n';
209
209
  }
210
210
 
211
+ /** The registry schema the payload is validated against; its `description` caps at 100 characters. */
212
+ export const SERVER_SCHEMA_URL = 'https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json';
213
+ export const REGISTRY_DESCRIPTION_MAX = 100;
214
+
211
215
  /**
212
216
  * Official MCP Registry server.json (registry.modelcontextprotocol.io).
213
- * Minimal valid shape per the registry's server.schema.json: reverse-DNS
214
- * name (matches package.json `mcpName` + the README `mcp-name:` marker),
215
- * the npm package pointer, and the stdio transport. The version is asserted
216
- * equal to package.json.version by lint_mcp_registry_manifest (CI).
217
+ * The name matches package.json `mcpName` + the README `mcp-name:` marker; the
218
+ * description is `agentConfig.registryDescription`, because the npm description
219
+ * is longer than the registry accepts. The positional `mcp-server` argument is
220
+ * what makes a registry client start the server instead of the bare CLI.
221
+ * Version- and length-equality are asserted by lint_mcp_registry_manifest (CI).
217
222
  */
218
- function _render_server_json(m: JsonObject): string {
219
- const pkg = m['package'] as JsonObject;
223
+ export function renderServerJson(pkg: JsonObject): string {
220
224
  const version = pkg['version'] as string;
225
+ const agentConfig = (pkg['agentConfig'] as JsonObject | undefined) ?? {};
221
226
  const payload: JsonObject = {
222
- $schema: 'https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json',
223
- name: 'io.github.event4u-app/agent-config',
224
- description: pkg['description'],
227
+ $schema: SERVER_SCHEMA_URL,
228
+ name: pkg['mcpName'] as string,
229
+ title: 'agent-config',
230
+ description: (agentConfig['registryDescription'] as string | undefined) ?? '',
231
+ websiteUrl: pkg['homepage'] as string,
225
232
  repository: {
226
- url: pkg['repository'],
233
+ url: _repo_url(pkg),
227
234
  source: 'github',
228
235
  },
229
236
  version,
230
237
  packages: [
231
238
  {
232
239
  registryType: 'npm',
233
- identifier: '@event4u/agent-config',
240
+ identifier: pkg['name'] as string,
234
241
  version,
242
+ runtimeHint: 'npx',
235
243
  transport: { type: 'stdio' },
244
+ packageArguments: [{ type: 'positional', value: 'mcp-server' }],
236
245
  },
237
246
  ],
238
247
  };
@@ -280,7 +289,9 @@ export function main(argv: readonly string[]): number {
280
289
  const manifest_text = pyJsonDumps(manifest, { indent: 2, sortKeys: true }) + '\n';
281
290
  const row_md = _render_row_md(manifest);
282
291
  const cf_json = _render_cf_json(manifest);
283
- const server_json = _render_server_json(manifest);
292
+ const server_json = renderServerJson(
293
+ JSON.parse(fs.readFileSync(_config.PKG_FILE, 'utf-8')) as JsonObject,
294
+ );
284
295
 
285
296
  const outputs: Array<[string, string]> = [
286
297
  [_config.OUT_MANIFEST, manifest_text],
@@ -75,6 +75,13 @@ export const SECURITY_FLOOR_EXCEPTIONS: Readonly<Record<string, { required: stri
75
75
  'high-severity route-guard bypass via path traversal (CVSS 7.5) affects <=10.1.1; ' +
76
76
  '10.1.2 is the fix. Verified with npm audit on a clean 10.1.0 install, 2026-07-31.',
77
77
  },
78
+ zod: {
79
+ required: '^4.6.6',
80
+ reason:
81
+ 'high-severity memory exhaustion while validating oversized constrained collections ' +
82
+ '(GHSA-mrhx-c7xc-m3w5) affects <4.6.6; 4.6.6 is the fix. Verified with npm audit on ' +
83
+ 'clean 4.6.5 (1 high) and 4.6.6 (0) installs, 2026-10-24.',
84
+ },
78
85
  };
79
86
 
80
87
  /** A settled caret floor: `^X.Y.0`. */
@@ -4,7 +4,7 @@
4
4
  * `check_release_pr_shape`.
5
5
  *
6
6
  * The shape check lets a release PR carry `package.json`, `package-lock.json`
7
- * and the three plugin manifests, and it reads only their NAMES. Smoke is
7
+ * and the plugin and extension manifests, and it reads only their NAMES. Smoke is
8
8
  * skipped on release heads (`docs/contracts/release-pr-gating.md`), so an added
9
9
  * script, a new dependency or a changed `bin` path could ride a release PR with
10
10
  * no gate reading it. This compares each manifest against the base and fails on
@@ -27,6 +27,7 @@ import * as path from 'node:path';
27
27
  import { fileURLToPath, pathToFileURL } from 'node:url';
28
28
 
29
29
  import { CONSUMER_IMPACT_VALUES } from './_lib/consumer_impact.js';
30
+ import { PINNED_PACKAGE_ARG_RE } from './_lib/listing_manifests.js';
30
31
  import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
31
32
 
32
33
  const _HERE = fileURLToPath(import.meta.url);
@@ -43,8 +44,17 @@ export const VERSION_FIELDS: Readonly<Record<string, readonly Pattern[]>> = {
43
44
  '.claude-plugin/marketplace.json': [['version'], ['metadata', 'version'], ['plugins', '*', 'version']],
44
45
  '.augment-plugin/plugin.json': [['version']],
45
46
  '.augment-plugin/marketplace.json': [['version'], ['metadata', 'version'], ['plugins', '*', 'version']],
47
+ '.github/plugin/plugin.json': [['version'], ['mcpServers', 'agent-config', 'args', '*']],
48
+ 'gemini-extension.json': [['version'], ['mcpServers', 'agent-config', 'args', '*']],
46
49
  };
47
50
 
51
+ /**
52
+ * Patterns admitted only as a re-pin: an `npx` argument may move from one
53
+ * `@event4u/agent-config@X.Y.Z` to another, never to any other string — the
54
+ * arguments around it are the command a crawler runs.
55
+ */
56
+ const PINNED_ARG_PATTERN: Pattern = ['mcpServers', 'agent-config', 'args', '*'];
57
+
48
58
  function _matches(p: readonly Segment[], pat: Pattern): boolean {
49
59
  if (p.length !== pat.length) return false;
50
60
  return pat.every((seg, i) => seg === p[i] || (seg === '*' && /^\d+$/u.test(p[i] ?? '')));
@@ -137,7 +147,12 @@ export function manifestDifferences(file: string, base: string | null, head: str
137
147
  before = (before as Record<string, unknown> | undefined)?.[s];
138
148
  after = (after as Record<string, unknown> | undefined)?.[s];
139
149
  }
140
- if (atVersion && typeof before === 'string' && typeof after === 'string') continue;
150
+ const pinOnly = _matches(p, PINNED_ARG_PATTERN);
151
+ const inShape =
152
+ typeof before === 'string' &&
153
+ typeof after === 'string' &&
154
+ (!pinOnly || (PINNED_PACKAGE_ARG_RE.test(before) && PINNED_PACKAGE_ARG_RE.test(after)));
155
+ if (atVersion && inShape) continue;
141
156
  out.push(`${file}: ${_render(p)} differs outside the version fields`);
142
157
  }
143
158
  return out;
@@ -36,6 +36,11 @@ const ALLOWLIST_GLOBS = [
36
36
  // the tarball (publish-surface.json roots), so a release PR must carry them.
37
37
  '.augment-plugin/plugin.json',
38
38
  '.augment-plugin/marketplace.json',
39
+ // Listing manifests — version and the `npx` pin rewritten by release.ts
40
+ // set_plugin_manifest_versions; neither ships in the tarball, but a crawler
41
+ // reads them from the default branch.
42
+ '.github/plugin/plugin.json',
43
+ 'gemini-extension.json',
39
44
  'src/packs/*/pack.yaml',
40
45
  'src/packs/*/README.md',
41
46
  'src/domains/*/pack.yaml',