@event4u/agent-config 17.3.0 → 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.
Files changed (137) 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/CHANGELOG.md +164 -342
  5. package/CREDITS.md +2 -2
  6. package/README.md +1 -1
  7. package/dist/agent-src/skills/code-intelligence/SKILL.md +3 -0
  8. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  9. package/dist/cli/registry.js +2 -2
  10. package/dist/cli-delegate/{chunk-DHSSP7V3.js → chunk-5AYJGUA3.js} +0 -2
  11. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  12. package/dist/cli-delegate/{chunk-MT2WOOCY.js → chunk-WY5ZZPGL.js} +2 -2
  13. package/dist/cli-delegate/{chunk-ORJPBER2.js → chunk-X6LZ7W5A.js} +73 -10
  14. package/dist/cli-delegate/cmd_conformance.js +175 -183
  15. package/dist/cli-delegate/cmd_doctor.js +5 -5
  16. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  17. package/dist/cli-delegate/cmd_explain.js +1 -1
  18. package/dist/cli-delegate/cmd_handoff.js +2 -2
  19. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  20. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  21. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  22. package/dist/cli-delegate/cmd_update.js +24 -13
  23. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  24. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  25. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  26. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  27. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +3 -3
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +1 -1
  32. package/dist/discovery/dormancy-report.md +1 -1
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +2 -2
  35. package/dist/discovery/trust-report.md +1 -1
  36. package/dist/discovery/workspaces.json +2 -2
  37. package/dist/hooks/code_graph_refresh.chunk.cjs +56 -27
  38. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -1
  39. package/dist/hooks/dispatch.js +982 -929
  40. package/dist/hooks/dispatch.sha256 +1 -1
  41. package/dist/hooks/egress_engine.chunk.cjs +56 -6
  42. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  43. package/dist/hooks/update_notice.chunk.cjs +914 -0
  44. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  45. package/dist/mcp/registry-manifest.json +1 -1
  46. package/dist/mcp/server.json +2 -2
  47. package/dist/mcp/server.mjs +71259 -66437
  48. package/dist/scripts/condense.js +9 -6
  49. package/package.json +4 -1
  50. package/src/config/evaluator-budgets.json +9 -8
  51. package/src/config/gate-violation-baselines.json +2 -2
  52. package/src/config/hook-bundle-budget.json +31 -6
  53. package/src/config/packed-binary-manifest.json +9 -0
  54. package/src/config/sigstore/npm_keys.json +50 -0
  55. package/src/config/sigstore/trust-material.json +9 -0
  56. package/src/config/sigstore/trusted_root.json +126 -0
  57. package/src/config/skill-body-budget.json +7 -1
  58. package/src/scripts/_cli/cmd_update.ts +48 -20
  59. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  60. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  61. package/src/scripts/_dispatch.bash +3 -2
  62. package/src/scripts/_lib/confusables.ts +0 -19
  63. package/src/scripts/_lib/egress_normalize.ts +53 -23
  64. package/src/scripts/_lib/egress_text.ts +5 -3
  65. package/src/scripts/_lib/egress_token.ts +7 -5
  66. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  67. package/src/scripts/_lib/git_common_dir.ts +10 -0
  68. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  69. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  70. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  71. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  72. package/src/scripts/_lib/lineage_map.ts +55 -0
  73. package/src/scripts/_lib/lineage_query.ts +140 -0
  74. package/src/scripts/_lib/lineage_seed.ts +116 -0
  75. package/src/scripts/_lib/lineage_watch.ts +249 -0
  76. package/src/scripts/_lib/link_crypto.ts +8 -4
  77. package/src/scripts/_lib/production_reach.ts +3 -3
  78. package/src/scripts/_lib/project_registry.ts +227 -0
  79. package/src/scripts/_lib/provenance_verify.ts +14 -242
  80. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  81. package/src/scripts/_lib/source_id.ts +48 -0
  82. package/src/scripts/_lib/source_shape.ts +4 -0
  83. package/src/scripts/_lib/unattended_gates.ts +6 -1
  84. package/src/scripts/advisories.ts +26 -10
  85. package/src/scripts/ai_council/council_egress.ts +5 -6
  86. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  87. package/src/scripts/check_dependency_floors.ts +7 -0
  88. package/src/scripts/check_egress_mutations.ts +1 -0
  89. package/src/scripts/code_graph/build.ts +67 -3
  90. package/src/scripts/code_graph/cli.ts +131 -28
  91. package/src/scripts/code_graph/conventions_default.ts +20 -1
  92. package/src/scripts/code_graph/detect.ts +5 -5
  93. package/src/scripts/code_graph/estate_cli.ts +135 -0
  94. package/src/scripts/code_graph/estate_index.ts +394 -0
  95. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  96. package/src/scripts/code_graph/estate_map.ts +141 -0
  97. package/src/scripts/code_graph/extract.ts +4 -1
  98. package/src/scripts/code_graph/extract_more.ts +137 -2
  99. package/src/scripts/code_graph/graph_budget.ts +158 -0
  100. package/src/scripts/code_graph/graph_home.ts +80 -0
  101. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  102. package/src/scripts/code_graph/map.ts +22 -13
  103. package/src/scripts/code_graph/map_model.ts +67 -12
  104. package/src/scripts/code_graph/map_verify.ts +68 -5
  105. package/src/scripts/code_graph/map_viewer/viewer.css +12 -0
  106. package/src/scripts/code_graph/map_viewer/viewer.js +45 -15
  107. package/src/scripts/code_graph/project_ref.ts +25 -0
  108. package/src/scripts/code_graph/projects_cli.ts +254 -0
  109. package/src/scripts/code_graph/providers/catalog.ts +14 -5
  110. package/src/scripts/code_graph/providers/django.ts +52 -9
  111. package/src/scripts/code_graph/providers/index.ts +2 -0
  112. package/src/scripts/code_graph/providers/rails.ts +204 -0
  113. package/src/scripts/code_graph/providers/spring.ts +38 -1
  114. package/src/scripts/code_graph/query.ts +9 -9
  115. package/src/scripts/code_graph/refresh_lock.ts +12 -8
  116. package/src/scripts/code_graph/repo_identity.ts +169 -0
  117. package/src/scripts/code_graph/sqlite_store.ts +22 -25
  118. package/src/scripts/code_graph/types.ts +6 -2
  119. package/src/scripts/condense.ts +6 -6
  120. package/src/scripts/hook_chunks.mjs +7 -0
  121. package/src/scripts/hook_manifest.json +1 -1
  122. package/src/scripts/hook_manifest.yaml +20 -17
  123. package/src/scripts/hooks/code_graph_refresh_hook.ts +10 -7
  124. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  125. package/src/scripts/hooks/host_semantics.ts +72 -0
  126. package/src/scripts/hooks/lazy_concern.ts +4 -0
  127. package/src/scripts/hooks/user_notice.ts +105 -0
  128. package/src/scripts/lint_harvest_provenance.ts +137 -11
  129. package/src/scripts/mcp_server/consumer_tool_catalog.json +69 -15
  130. package/src/scripts/mcp_server/graph_tools.ts +161 -41
  131. package/src/scripts/network_sources.ts +24 -6
  132. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  133. package/src/scripts/update_notice_hook.ts +75 -7
  134. package/src/vendor/grammars/README.md +7 -6
  135. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  136. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  137. /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
@@ -0,0 +1,50 @@
1
+ {
2
+ "keys": [
3
+ {
4
+ "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
5
+ "keyUsage": "npm:signatures",
6
+ "publicKey": {
7
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==",
8
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
9
+ "validFor": {
10
+ "start": "1999-01-01T00:00:00.000Z",
11
+ "end": "2025-01-29T00:00:00.000Z"
12
+ }
13
+ }
14
+ },
15
+ {
16
+ "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
17
+ "keyUsage": "npm:attestations",
18
+ "publicKey": {
19
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==",
20
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
21
+ "validFor": {
22
+ "start": "2022-12-01T00:00:00.000Z",
23
+ "end": "2025-01-29T00:00:00.000Z"
24
+ }
25
+ }
26
+ },
27
+ {
28
+ "keyId": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U",
29
+ "keyUsage": "npm:signatures",
30
+ "publicKey": {
31
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g==",
32
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
33
+ "validFor": {
34
+ "start": "2025-01-13T00:00:00.000Z"
35
+ }
36
+ }
37
+ },
38
+ {
39
+ "keyId": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U",
40
+ "keyUsage": "npm:attestations",
41
+ "publicKey": {
42
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+HOKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g==",
43
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
44
+ "validFor": {
45
+ "start": "2025-01-13T00:00:00.000Z"
46
+ }
47
+ }
48
+ }
49
+ ]
50
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "Trust material the unattended upgrade verifies release provenance against (road-to-an-update-that-installs-itself-only-when-it-is-safe D15). Pinned, never refreshed at run time: a candidate release can never choose the roots it is checked against. Read by src/scripts/_lib/sigstore_provenance.ts, which refuses when a file's sha256 differs from the one recorded here. Refresh by re-extracting both targets from a newer @sigstore/tuf seeds.json and updating source and digests together; until a release carries new material, an unattended run refuses anything signed under a rotated root and `agent-config upgrade` by hand still works.",
3
+ "source": "@sigstore/tuf@5.0.0 seeds.json, mirror https://tuf-repo-cdn.sigstore.dev, extracted 2026-10-24",
4
+ "files": {
5
+ "trusted_root.json": "6494e21ea73fa7ee769f85f57d5a3e6a08725eae1e38c755fc3517c9e6bc0b66",
6
+ "npm_keys.json": "160677eb6e1c7083c89b166b20f8fe4e837fb71181506aff1991b80b89184f7d"
7
+ },
8
+ "npm_keys_live_check": "Equal on 2026-10-24 to https://registry.npmjs.org/-/npm/v1/keys: SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA (expired 2025-01-29) and SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U (no expiry)."
9
+ }
@@ -0,0 +1,126 @@
1
+ {
2
+ "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1",
3
+ "tlogs": [
4
+ {
5
+ "baseUrl": "https://rekor.sigstore.dev",
6
+ "hashAlgorithm": "SHA2_256",
7
+ "publicKey": {
8
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==",
9
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
10
+ "validFor": {
11
+ "start": "2021-01-12T11:53:27Z"
12
+ }
13
+ },
14
+ "logId": {
15
+ "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
16
+ }
17
+ },
18
+ {
19
+ "baseUrl": "https://log2025-1.rekor.sigstore.dev",
20
+ "hashAlgorithm": "SHA2_256",
21
+ "publicKey": {
22
+ "rawBytes": "MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=",
23
+ "keyDetails": "PKIX_ED25519",
24
+ "validFor": {
25
+ "start": "2025-09-23T00:00:00Z"
26
+ }
27
+ },
28
+ "logId": {
29
+ "keyId": "zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A="
30
+ }
31
+ }
32
+ ],
33
+ "certificateAuthorities": [
34
+ {
35
+ "subject": {
36
+ "organization": "sigstore.dev",
37
+ "commonName": "sigstore"
38
+ },
39
+ "uri": "https://fulcio.sigstore.dev",
40
+ "certChain": {
41
+ "certificates": [
42
+ {
43
+ "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ=="
44
+ }
45
+ ]
46
+ },
47
+ "validFor": {
48
+ "start": "2021-03-07T03:20:29Z",
49
+ "end": "2022-12-31T23:59:59.999Z"
50
+ }
51
+ },
52
+ {
53
+ "subject": {
54
+ "organization": "sigstore.dev",
55
+ "commonName": "sigstore"
56
+ },
57
+ "uri": "https://fulcio.sigstore.dev",
58
+ "certChain": {
59
+ "certificates": [
60
+ {
61
+ "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="
62
+ },
63
+ {
64
+ "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"
65
+ }
66
+ ]
67
+ },
68
+ "validFor": {
69
+ "start": "2022-04-13T20:06:15Z"
70
+ }
71
+ }
72
+ ],
73
+ "ctlogs": [
74
+ {
75
+ "baseUrl": "https://ctfe.sigstore.dev/test",
76
+ "hashAlgorithm": "SHA2_256",
77
+ "publicKey": {
78
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==",
79
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
80
+ "validFor": {
81
+ "start": "2021-03-14T00:00:00Z",
82
+ "end": "2022-10-31T23:59:59.999Z"
83
+ }
84
+ },
85
+ "logId": {
86
+ "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I="
87
+ }
88
+ },
89
+ {
90
+ "baseUrl": "https://ctfe.sigstore.dev/2022",
91
+ "hashAlgorithm": "SHA2_256",
92
+ "publicKey": {
93
+ "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==",
94
+ "keyDetails": "PKIX_ECDSA_P256_SHA_256",
95
+ "validFor": {
96
+ "start": "2022-10-20T00:00:00Z"
97
+ }
98
+ },
99
+ "logId": {
100
+ "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4="
101
+ }
102
+ }
103
+ ],
104
+ "timestampAuthorities": [
105
+ {
106
+ "subject": {
107
+ "organization": "sigstore.dev",
108
+ "commonName": "sigstore-tsa-selfsigned"
109
+ },
110
+ "uri": "https://timestamp.sigstore.dev/api/v1/timestamp",
111
+ "certChain": {
112
+ "certificates": [
113
+ {
114
+ "rawBytes": "MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw=="
115
+ },
116
+ {
117
+ "rawBytes": "MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E"
118
+ }
119
+ ]
120
+ },
121
+ "validFor": {
122
+ "start": "2025-07-04T00:00:00Z"
123
+ }
124
+ }
125
+ ]
126
+ }
@@ -38,7 +38,7 @@
38
38
  "character-consistency": 8555,
39
39
  "check-refs": 4188,
40
40
  "churn-prevention": 8014,
41
- "code-intelligence": 16998,
41
+ "code-intelligence": 17182,
42
42
  "code-refactoring": 13717,
43
43
  "code-review": 16261,
44
44
  "command-routing": 6040,
@@ -687,6 +687,12 @@
687
687
  "old": 16492,
688
688
  "new": 16998,
689
689
  "reason": "road-to-a-map-that-draws-itself (ADR-297): step 0 sends an orientation question to agents/map/MAP.md before any graph query, and the MCP table gains rows for graph_map_status and graph_area. Both are the skill's routing surface for the new map; trimming them would hide the tools the roadmap added."
690
+ },
691
+ {
692
+ "skill": "code-intelligence",
693
+ "old": 16998,
694
+ "new": 17182,
695
+ "reason": "road-to-a-graph-that-follows-you (ADR-299): the MCP table gains a row for graph_estate and one line naming the new project parameter on every other graph tool. The ceiling sat 3 bytes above the body, so documenting the tool at all needs a raise; the wording was compressed first, from 262 to 184 bytes."
690
696
  }
691
697
  ]
692
698
  }
@@ -21,6 +21,12 @@
21
21
  * Downgrades are allowed; the pin is a project decision.
22
22
  * - (no flag) — pin to the registry's `latest` tag.
23
23
  *
24
+ * The global install moves first. Only the CLI follows the pin (`pin_resolver`
25
+ * re-execs through npx); hooks, rules and skills run from the global install. So
26
+ * when `installed.lock` records an older version than the target, this runs
27
+ * `agent-config upgrade` to exactly that version and writes the pin only once it
28
+ * succeeded — offline, where it cannot, the pin is refused instead.
29
+ *
24
30
  * Write target: the **deepest** `.agent-settings.yml` in the project cascade
25
31
  * that already carries the `agent_config_version` key. When no file carries
26
32
  * it, the repo-root file is created/edited. Comments and key ordering are
@@ -405,6 +411,10 @@ interface MainOptions {
405
411
  state_path?: string | null;
406
412
  out?: OutSink;
407
413
  err?: OutSink;
414
+ /** The version the global install recorded in `installed.lock`, or `null` when there is none. */
415
+ global_version?: () => string | null;
416
+ /** Install exactly this version globally; returns the exit code. */
417
+ upgrader?: (version: string, out: OutSink, err: OutSink) => Promise<number>;
408
418
  }
409
419
 
410
420
  interface Args {
@@ -526,6 +536,29 @@ export async function main(argv: string[] | null = null, options: MainOptions =
526
536
  return 0;
527
537
  }
528
538
 
539
+ // The pin only steers the CLI (`pin_resolver` re-execs through npx); hooks,
540
+ // rules and skills run from the global install. A pin ahead of it would
541
+ // leave the project naming one version while every session runs another,
542
+ // so the global install moves first, and the pin only once it has.
543
+ const global = (options.global_version ?? _global_install_version)();
544
+ if (global !== null && update_check._is_newer(latest, global)) {
545
+ if (offline) {
546
+ _print(
547
+ err,
548
+ `❌ agent-config: the global install is ${global} and cannot be upgraded to ${latest} offline; ` +
549
+ 'the pin is unchanged. Run `agent-config update` again with registry access.',
550
+ );
551
+ return 1;
552
+ }
553
+ _print(out, `→ The global install is ${global}; installing ${latest} first so the pin matches what runs.`);
554
+ const upgrader = options.upgrader ?? _upgrade_global;
555
+ const rc = await upgrader(latest, out, err);
556
+ if (rc !== 0) {
557
+ _print(err, `❌ agent-config: the global upgrade to ${latest} failed (exit ${rc}); the pin is unchanged.`);
558
+ return 1;
559
+ }
560
+ }
561
+
529
562
  const pin_file = _find_pin_file(cwd);
530
563
  const changed = _write_pin(pin_file, latest);
531
564
  const rel = _relativeToOr(pin_file, cwd);
@@ -542,7 +575,6 @@ export async function main(argv: string[] | null = null, options: MainOptions =
542
575
  cache_warmer(latest);
543
576
  }
544
577
  _refresh_state(latest, latest, state_path);
545
- _report_global_install(latest, out);
546
578
  return 0;
547
579
  }
548
580
 
@@ -556,28 +588,24 @@ function _relativeToOr(target: string, base: string): string {
556
588
  }
557
589
 
558
590
  /**
559
- * Name `agent-config upgrade` when the global install is older than the pin.
591
+ * The version the global install recorded, or `null` when there is no global
592
+ * install to keep in step with.
560
593
  *
561
- * Read-only by design: `installed.lock` records what the global install
562
- * deployed, and `upgrade` (plus its `--check` and unattended paths) reads it as
563
- * the installed version. This command used to rewrite it to the pin, so after
564
- * `update` the lock claimed a version no `npm install -g` had installed and
565
- * `upgrade --check` reported "up to date" over the old binary.
594
+ * Read-only by design: `installed.lock` is written by the global install itself.
595
+ * This command once rewrote it to the pin, so the lock claimed a version no
596
+ * `npm install -g` had installed and `upgrade --check` reported "up to date"
597
+ * over the old binary.
566
598
  */
567
- function _report_global_install(version: string, out: OutSink): void {
599
+ function _global_install_version(): string | null {
568
600
  const existing = installed_lock.read_lockfile(installed_lock.lockfile_path());
569
- if (existing === null) {
570
- return;
571
- }
572
- const recorded = (existing as unknown as Record<string, unknown>)['agent_config_version'];
573
- if (typeof recorded !== 'string' || !update_check._is_newer(version, _normalize(recorded))) {
574
- return;
575
- }
576
- _print(
577
- out,
578
- `ℹ️ The global install is ${_normalize(recorded)}; this pin does not change it. ` +
579
- `Run \`agent-config upgrade\` to install ${version} globally.`,
580
- );
601
+ const recorded = (existing as unknown as Record<string, unknown> | null)?.['agent_config_version'];
602
+ return typeof recorded === 'string' && recorded.trim() !== '' ? _normalize(recorded) : null;
603
+ }
604
+
605
+ /** `agent-config upgrade`, in process, to exactly `version`. */
606
+ async function _upgrade_global(version: string, out: OutSink, err: OutSink): Promise<number> {
607
+ const upgrade = await import('./cmd_upgrade.js');
608
+ return upgrade.main([], { version, out, err });
581
609
  }
582
610
 
583
611
  /** Read `version` from the package's own `package.json`. */
@@ -505,6 +505,12 @@ interface MainOptions {
505
505
  project_root?: string | null;
506
506
  out?: OutSink;
507
507
  err?: OutSink;
508
+ /**
509
+ * Install this exact version instead of `latest`. Programmatic only — set by
510
+ * `agent-config update`, which must install the version it is about to pin;
511
+ * the command line keeps `update --to` as the one way to name a version.
512
+ */
513
+ version?: string;
508
514
  }
509
515
 
510
516
  export async function main(argv: string[] | null = null, options: MainOptions = {}): Promise<number> {
@@ -549,7 +555,7 @@ export async function main(argv: string[] | null = null, options: MainOptions =
549
555
  return 0;
550
556
  }
551
557
 
552
- const target = `${PACKAGE_NAME}@latest`;
558
+ const target = `${PACKAGE_NAME}@${options.version ?? 'latest'}`;
553
559
  const npm_cmd: string[] = ['npm', 'install', '-g', target];
554
560
  // --no-ui: the setup wizard is an onboarding surface, not an upgrade
555
561
  // step — its foreground server used to block the upgrade until Ctrl-C.
@@ -22,17 +22,14 @@
22
22
  * registry, a live session) · `1` a step failed and the previous version was
23
23
  * restored · `4` a step failed and the restore did not reach the previous version.
24
24
  */
25
+ import * as crypto from 'node:crypto';
25
26
  import * as fs from 'node:fs';
27
+ import * as os from 'node:os';
26
28
  import * as path from 'node:path';
27
29
  import { spawnSync } from 'node:child_process';
28
30
  import process from 'node:process';
29
31
 
30
- import {
31
- EXPECTED_WORKFLOW_REF,
32
- attestation_workflow_ref,
33
- provenance_matches_publish,
34
- verify_attestation_signature,
35
- } from '../_lib/provenance_verify.js';
32
+ import { type ReleaseToVerify, verify_release_provenance } from '../_lib/sigstore_provenance.js';
36
33
  import { read_candidate, unattended_decision, type RegistryCandidate } from '../_lib/unattended_gates.js';
37
34
  import type { InstallOrigin } from '../_lib/classify_update.js';
38
35
  import * as installed_lock from '../_lib/installed_lock.js';
@@ -97,10 +94,13 @@ export interface StepResult {
97
94
 
98
95
  export type Packument = { readonly ok: true; readonly doc: unknown } | { readonly ok: false; readonly reason: string };
99
96
 
97
+ /** A verified tarball on disk, the only thing the install step may install — or why there is none. */
98
+ export type Provenance = { readonly ok: true; readonly tarball: string } | { readonly ok: false; readonly reason: string };
99
+
100
100
  export interface UnattendedDeps {
101
101
  readonly fetchPackument: () => Promise<Packument>;
102
- /** Refusal reason when the attestation does not tie to this package's publish workflow, else `null`. */
103
- readonly verifyProvenance: (candidate: RegistryCandidate) => Promise<string | null>;
102
+ /** Download the candidate, verify its attestations against it, and hand back the verified file. */
103
+ readonly verifyProvenance: (candidate: RegistryCandidate) => Promise<Provenance>;
104
104
  readonly run: (cmd: string[]) => StepResult;
105
105
  /** The version recorded in `installed.lock`; `''` when unknown. */
106
106
  readonly readInstalled: () => string;
@@ -243,9 +243,10 @@ function release_lock(deps: Pick<UnattendedDeps, 'lockPath' | 'pid'>): void {
243
243
 
244
244
  // --- the run -----------------------------------------------------------------
245
245
 
246
- function install_steps(version: string, deps: UnattendedDeps): string[][] {
246
+ /** `spec` is the verified tarball for an upgrade, and `<package>@<version>` only for a restore. */
247
+ function install_steps(spec: string, deps: UnattendedDeps): string[][] {
247
248
  return [
248
- ['npm', 'install', '-g', `${PACKAGE_NAME}@${version}`],
249
+ ['npm', 'install', '-g', spec],
249
250
  [deps.agentConfigBin, 'global', '--no-ui'],
250
251
  ...deps.globalSettings.map((target) => [deps.agentConfigBin, 'settings:sync', '--path', target]),
251
252
  ];
@@ -263,7 +264,7 @@ function run_steps(steps: string[][], deps: UnattendedDeps, honourSignal = true)
263
264
 
264
265
  /** Restored means both the binary and `installed.lock` read the previous version again. */
265
266
  function restore(previous: string, deps: UnattendedDeps): number {
266
- const back = run_steps(install_steps(previous, deps).slice(0, 2), deps, false);
267
+ const back = run_steps(install_steps(`${PACKAGE_NAME}@${previous}`, deps).slice(0, 2), deps, false);
267
268
  const lock = deps.readInstalled();
268
269
  const binary = deps.readBinary();
269
270
  if (back.ok && lock === previous && binary === previous) {
@@ -279,7 +280,10 @@ function restore(previous: string, deps: UnattendedDeps): number {
279
280
  return EXIT_RESTORE_FAILED;
280
281
  }
281
282
 
282
- async function decide(installed: string, deps: UnattendedDeps): Promise<{ candidate: RegistryCandidate } | { refused: string[] }> {
283
+ async function decide(
284
+ installed: string,
285
+ deps: UnattendedDeps,
286
+ ): Promise<{ candidate: RegistryCandidate; tarball: string } | { refused: string[] }> {
283
287
  const fetched = await deps.fetchPackument();
284
288
  if (!fetched.ok) return { refused: [`registry unavailable: ${fetched.reason}`] };
285
289
  const candidate = read_candidate(fetched.doc);
@@ -292,85 +296,89 @@ async function decide(installed: string, deps: UnattendedDeps): Promise<{ candid
292
296
  env: { ...deps.env, now: deps.now().toISOString() },
293
297
  });
294
298
  if (!decision.install) return { refused: [...decision.reasons] };
295
- // 2.1's open half. `decision` accepted an attestation that EXISTS and
296
- // carries an SLSA predicate; that document is producible by any workflow
297
- // in any repository with `id-token: write`, so it is checked here against
298
- // the one workflow this package publishes from before anything installs.
299
+ // `decision` accepted an attestation that EXISTS and carries an SLSA
300
+ // predicate, which any workflow with `id-token: write` can produce; the
301
+ // tarball is verified here against pinned roots before anything installs.
299
302
  const provenance = await deps.verifyProvenance(candidate);
300
- if (provenance !== null) return { refused: [provenance] };
301
- return { candidate };
303
+ if (!provenance.ok) return { refused: [provenance.reason] };
304
+ return { candidate, tarball: provenance.tarball };
302
305
  }
303
306
 
307
+ /** Largest tarball the unattended path downloads; a release is about 16 MB. */
308
+ export const MAX_TARBALL_BYTES = 64 * 1024 * 1024;
309
+
304
310
  /**
305
- * Fetch the attestation bundle, require it to name this package's publish
306
- * workflow, and require its DSSE signature to verify. Returns a refusal
307
- * reason, or `null` when both hold.
311
+ * Download the candidate's tarball, check it against the packument's integrity
312
+ * and against both attestations, and write exactly those bytes to a private
313
+ * temp file — the file the install step installs. Returns that file, or why
314
+ * there is none.
308
315
  *
309
- * **This used to say the signature half was `npm audit signatures` "in the
310
- * install step". It was not, and never had been** — `install_steps` runs
311
- * `npm install -g`, `agent-config global` and `settings:sync`, and no audit.
312
- * The sentence described a composition nobody wrote, which is the worst shape
313
- * a security note can have, so it is replaced by what the code does rather
314
- * than corrected in place.
316
+ * The bytes verified and the bytes installed are one buffer: installing by
317
+ * `<package>@<version>` would let the registry serve different bytes to the
318
+ * second request than to the first.
315
319
  *
316
- * What runs now: the payload is tied to the workflow, the envelope's signature
317
- * is verified against the certificate the bundle carries, and that
318
- * certificate's SAN is required to name the same workflow. What does NOT run:
319
- * any check of who ISSUED that certificate. The residue is the
320
- * `dsse-trust-root` blocker on the roadmap, and
321
- * `verify_attestation_signature`'s header states it in full.
322
- *
323
- * A bundle that cannot be fetched REFUSES rather than passing. Unlike the live
324
- * session register, there is no second independent gate behind this one: an
325
- * unreachable attestation is exactly what an attacker serving a tarball
326
- * without one produces, so failing open here would hand back the property the
327
- * check exists to establish.
320
+ * Every URL comes from the packument, so each must be on the registry's own
321
+ * origin; a hostile answer would otherwise choose both the host fetched and the
322
+ * document verified. A fetch that fails REFUSES rather than passing: an
323
+ * unreachable attestation is exactly what an attacker serving a tarball without
324
+ * one produces.
328
325
  */
329
326
  export async function verify_provenance_default(
330
327
  candidate: RegistryCandidate,
331
328
  fetch_impl: typeof fetch = fetch,
332
- timeoutMs = 10_000,
333
- ): Promise<string | null> {
334
- if (candidate.attestationsUrl === null) {
335
- return `${candidate.version} carries no attestation bundle to verify`;
329
+ timeoutMs = 60_000,
330
+ verify: (release: ReleaseToVerify) => Promise<string | null> = (r) => verify_release_provenance(r),
331
+ ): Promise<Provenance> {
332
+ const v = candidate.version;
333
+ const registry = new URL(PACKUMENT_URL).origin;
334
+ const refuse = (reason: string): Provenance => ({ ok: false, reason: `${v}: ${reason}` });
335
+ for (const [what, url] of [
336
+ ['attestation bundle', candidate.attestationsUrl],
337
+ ['tarball', candidate.tarballUrl],
338
+ ] as const) {
339
+ if (url === null) return refuse(`the registry names no ${what} to verify`);
340
+ let origin = '';
341
+ try {
342
+ origin = new URL(url).origin;
343
+ } catch {
344
+ // an unparsable URL is refused below like a foreign one
345
+ }
346
+ if (origin !== registry) return refuse(`the ${what} url is not on ${registry}`);
336
347
  }
337
- // The URL comes from the packument, so a hostile registry answer would
338
- // otherwise choose both the host fetched and the document verified.
339
- let origin: string;
348
+ const integrity = /^sha512-([A-Za-z0-9+/=]+)$/.exec(candidate.integrity ?? '');
349
+ if (integrity === null) return refuse('the registry records no sha512 integrity for the tarball');
350
+
351
+ const get = async (url: string): Promise<Response> => {
352
+ const resp = await fetch_impl(url, { signal: AbortSignal.timeout(timeoutMs), redirect: 'error' });
353
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
354
+ return resp;
355
+ };
356
+ let attestations: unknown;
357
+ let bytes: Buffer;
340
358
  try {
341
- origin = new URL(candidate.attestationsUrl).origin;
342
- } catch {
343
- origin = '';
359
+ attestations = await (await get(candidate.attestationsUrl as string)).json();
360
+ const resp = await get(candidate.tarballUrl as string);
361
+ if (Number(resp.headers.get('content-length') ?? 0) > MAX_TARBALL_BYTES) return refuse('the tarball is larger than allowed');
362
+ bytes = Buffer.from(await resp.arrayBuffer());
363
+ } catch (err) {
364
+ return refuse(`download failed: ${err instanceof Error ? err.message : String(err)}`);
344
365
  }
345
- if (origin !== new URL(PACKUMENT_URL).origin) {
346
- return `${candidate.version}: attestation url is not on ${new URL(PACKUMENT_URL).origin}`;
366
+ if (bytes.length > MAX_TARBALL_BYTES) return refuse('the tarball is larger than allowed');
367
+ const digest = crypto.createHash('sha512').update(bytes).digest();
368
+ if (!digest.equals(Buffer.from(integrity[1] as string, 'base64'))) {
369
+ return refuse('the downloaded tarball does not match the integrity the registry records');
347
370
  }
348
- let doc: unknown;
371
+ const reason = await verify({ version: v, tarballSha512Hex: digest.toString('hex'), attestations });
372
+ if (reason !== null) return refuse(reason);
373
+
349
374
  try {
350
- const resp = await fetch_impl(candidate.attestationsUrl, {
351
- signal: AbortSignal.timeout(timeoutMs),
352
- headers: { accept: 'application/json' },
353
- redirect: 'error',
354
- });
355
- if (!resp.ok) return `attestation fetch failed: HTTP ${resp.status}`;
356
- doc = await resp.json();
375
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-config-upgrade-'));
376
+ const tarball = path.join(dir, `agent-config-${v}.tgz`);
377
+ fs.writeFileSync(tarball, bytes, { mode: 0o600, flag: 'wx' });
378
+ return { ok: true, tarball };
357
379
  } catch (err) {
358
- return `attestation fetch failed: ${err instanceof Error ? err.message : String(err)}`;
359
- }
360
- if (!provenance_matches_publish(doc)) {
361
- const named = attestation_workflow_ref(doc);
362
- return named === null
363
- ? `${candidate.version} has no readable provenance workflow in its attestation`
364
- : `${candidate.version} was built by ${named}, not ${EXPECTED_WORKFLOW_REF}`;
380
+ return refuse(`cannot write the verified tarball: ${err instanceof Error ? err.message : String(err)}`);
365
381
  }
366
- // The payload says the right thing; this asks whether the payload is the
367
- // one that was signed, and whether the certificate that signed it carries
368
- // the same identity. It does NOT establish who issued that certificate —
369
- // see `verify_attestation_signature`'s own note and the `dsse-trust-root`
370
- // blocker, which is where the trust root is decided.
371
- const signature = await verify_attestation_signature(doc);
372
- if (signature !== null) return `${candidate.version}: ${signature}`;
373
- return null;
374
382
  }
375
383
 
376
384
  /** Contexts where even the registry fetch and the lock file are side effects nobody asked for. */
@@ -397,14 +405,16 @@ export async function run_unattended(deps: UnattendedDeps): Promise<number> {
397
405
  say(deps.err, `${PREFIX}: not installing — ${locked}.`);
398
406
  return EXIT_REFUSED;
399
407
  }
408
+ let verified: string | null = null;
400
409
  try {
401
410
  const verdict = await decide(previous, deps);
402
411
  if ('refused' in verdict) {
403
412
  say(deps.out, `ℹ️ ${PREFIX}: not installing (installed: ${previous}) — ${verdict.refused.join('; ')}.`);
404
413
  return EXIT_REFUSED;
405
414
  }
415
+ verified = verdict.tarball;
406
416
  const target = verdict.candidate.version;
407
- const steps = install_steps(target, deps);
417
+ const steps = install_steps(verified, deps);
408
418
  if (deps.activationBlocked !== null) {
409
419
  const how = deps.dryRun ? 'dry run: a real run would not install' : 'not installing';
410
420
  say(deps.out, `ℹ️ ${PREFIX}: every gate passed for ${target}, but ${how} — ${deps.activationBlocked}.`);
@@ -422,10 +432,13 @@ export async function run_unattended(deps: UnattendedDeps): Promise<number> {
422
432
  }
423
433
  const what = `${ran.cmd.join(' ')} (exit ${ran.result.rc})`;
424
434
  if (/\bEACCES\b/.test(ran.result.stderr)) {
435
+ // The verified file is removed when this run ends, so the command a
436
+ // person can still run names the version instead.
437
+ const manual = ran.cmd === steps[0] ? ['npm', 'install', '-g', `${PACKAGE_NAME}@${target}`] : ran.cmd;
425
438
  say(
426
439
  deps.err,
427
440
  `❌ ${PREFIX}: permission denied on ${what}. agent-config never runs sudo; ` +
428
- `run this yourself with the right permissions: ${ran.cmd.join(' ')}`,
441
+ `run this yourself with the right permissions: ${manual.join(' ')}`,
429
442
  );
430
443
  // Denied on the very first write, npm changed nothing; any later step leaves a mixed install.
431
444
  const untouched = ran.cmd === steps[0] && deps.readInstalled() === previous && deps.readBinary() === previous;
@@ -435,6 +448,7 @@ export async function run_unattended(deps: UnattendedDeps): Promise<number> {
435
448
  say(deps.err, `❌ ${PREFIX}: step failed: ${what} — restoring ${previous}.`);
436
449
  return restore(previous, deps);
437
450
  } finally {
451
+ if (verified !== null) fs.rmSync(path.dirname(verified), { recursive: true, force: true });
438
452
  release_lock(deps);
439
453
  }
440
454
  }
@@ -104,7 +104,8 @@ EOF
104
104
  cat <<'EOF'
105
105
 
106
106
  Tier 1 — power-user (release shape, audit, migration):
107
- update Update the agent_config_version pin in .agent-settings.yml
107
+ update Pin agent_config_version in .agent-settings.yml; installs that
108
+ version globally first when the global install is older
108
109
  Flags: --check (read-only) | --to <version> (explicit pin)
109
110
  upgrade Install the latest @event4u/agent-config globally
110
111
  (npm i -g) and refresh the global install + plugin.
@@ -1400,7 +1401,7 @@ cmd_use() {
1400
1401
  }
1401
1402
 
1402
1403
  # `agent-config update` — flip the agent_config_version pin in
1403
- # .agent-settings.yml. See scripts/_cli/cmd_update.ts (P3.1 of
1404
+ # .agent-settings.yml, upgrading the global install first when it is older. See scripts/_cli/cmd_update.ts (P3.1 of
1404
1405
  # road-to-portable-runtime-and-update-check.md).
1405
1406
  cmd_update() {
1406
1407
  exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_update.ts" "$@"