@vegastack/skills 0.2.0 → 0.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 (40) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +9 -9
  3. package/package.json +2 -2
  4. package/skill/arch-guardian/SKILL.md +27 -39
  5. package/skill/arch-guardian/agents/openai.yaml +2 -2
  6. package/skill/arch-guardian/assets/adr-template.md +10 -25
  7. package/skill/arch-guardian/assets/answers-example.json +8 -18
  8. package/skill/arch-guardian/assets/architecture-profile.json +10 -22
  9. package/skill/arch-guardian/assets/architecture-profile.schema.json +13 -195
  10. package/skill/arch-guardian/references/advisory-report.md +65 -0
  11. package/skill/arch-guardian/references/architecture/ai-cost.md +2 -0
  12. package/skill/arch-guardian/references/architecture/ai-data-boundaries.md +2 -0
  13. package/skill/arch-guardian/references/architecture/ai-evals.md +2 -0
  14. package/skill/arch-guardian/references/architecture/connectors-sandbox.md +2 -2
  15. package/skill/arch-guardian/references/architecture/delivery-operations.md +5 -5
  16. package/skill/arch-guardian/references/architecture/durable-execution.md +3 -1
  17. package/skill/arch-guardian/references/architecture/flutter.md +1 -1
  18. package/skill/arch-guardian/references/architecture/foundation.md +17 -19
  19. package/skill/arch-guardian/references/architecture/hosting-reliability.md +1 -1
  20. package/skill/arch-guardian/references/architecture/identity-tenancy.md +7 -7
  21. package/skill/arch-guardian/references/architecture/model-lifecycle.md +5 -3
  22. package/skill/arch-guardian/references/architecture/models-observability.md +1 -1
  23. package/skill/arch-guardian/references/architecture/security-privacy.md +9 -5
  24. package/skill/arch-guardian/references/architecture/topology-monorepo.md +1 -1
  25. package/skill/arch-guardian/references/architecture/web.md +1 -1
  26. package/skill/arch-guardian/references/foundation-compatibility.json +1 -1
  27. package/skill/arch-guardian/references/profile-governance.md +25 -39
  28. package/skill/arch-guardian/references/rule-model.json +2 -2
  29. package/skill/arch-guardian/references/workflows.md +15 -12
  30. package/skill/arch-guardian/refresh/REFRESH.md +9 -2
  31. package/skill/arch-guardian/refresh/sources.json +26 -14
  32. package/skill/arch-guardian/scripts/lib.mjs +1 -10
  33. package/skill/arch-guardian/scripts/profile-tool.mjs +43 -49
  34. package/skill/arch-guardian/scripts/refresh-evidence.mjs +43 -5
  35. package/skill/arch-guardian/scripts/validate-profile.mjs +20 -196
  36. package/skill/arch-guardian/scripts/verify-corpus.mjs +1 -13
  37. package/skill/skill-maintainer/refresh/REFRESH.md +1 -1
  38. package/skill-integrity.json +34 -35
  39. package/skill/arch-guardian/references/control-catalog.json +0 -55
  40. package/skill/arch-guardian/scripts/architecture-check.mjs +0 -323
@@ -153,7 +153,8 @@
153
153
  "changelog": "https://github.com/vercel/next.js/releases",
154
154
  "package": "https://www.npmjs.com/package/next/v/16.3.0",
155
155
  "repository": "https://github.com/vercel/next.js/tree/d73f5622e226358dcef8cf7a8a373333ff265ae7",
156
- "security": "https://github.com/vercel/next.js/security"
156
+ "security": "https://github.com/vercel/next.js/security",
157
+ "llms": "https://nextjs.org/docs/llms.txt"
157
158
  },
158
159
  "versionDetection": {
159
160
  "type": "npm",
@@ -227,7 +228,8 @@
227
228
  "docsIndex": "https://developers.cloudflare.com/workers/",
228
229
  "api": "https://developers.cloudflare.com/api/",
229
230
  "changelog": "https://developers.cloudflare.com/changelog/",
230
- "security": "https://www.cloudflare.com/trust-hub/"
231
+ "security": "https://www.cloudflare.com/trust-hub/",
232
+ "llms": "https://developers.cloudflare.com/llms.txt"
231
233
  },
232
234
  "versionDetection": {
233
235
  "type": "http-hash-and-changelog"
@@ -265,7 +267,8 @@
265
267
  "changelog": "https://developers.cloudflare.com/changelog/",
266
268
  "package": "https://www.npmjs.com/package/@cloudflare/sandbox/v/0.12.4",
267
269
  "repository": "https://github.com/cloudflare/sandbox-sdk/tree/798afcf33ead69fdbfb6598ca345f396deb4d6b2",
268
- "security": "https://github.com/cloudflare/sandbox-sdk/security"
270
+ "security": "https://github.com/cloudflare/sandbox-sdk/security",
271
+ "llms": "https://developers.cloudflare.com/llms.txt"
269
272
  },
270
273
  "versionDetection": {
271
274
  "type": "npm",
@@ -302,7 +305,8 @@
302
305
  "primary": "https://developers.cloudflare.com/hyperdrive/concepts/connection-pooling/",
303
306
  "docsIndex": "https://developers.cloudflare.com/hyperdrive/",
304
307
  "api": "https://developers.cloudflare.com/api/resources/hyperdrive/",
305
- "changelog": "https://developers.cloudflare.com/changelog/"
308
+ "changelog": "https://developers.cloudflare.com/changelog/",
309
+ "llms": "https://developers.cloudflare.com/llms.txt"
306
310
  },
307
311
  "versionDetection": {
308
312
  "type": "http-hash-and-changelog"
@@ -337,7 +341,8 @@
337
341
  "docsIndex": "https://developers.cloudflare.com/ai-gateway/",
338
342
  "api": "https://developers.cloudflare.com/api/resources/ai-gateway/",
339
343
  "changelog": "https://developers.cloudflare.com/changelog/",
340
- "security": "https://www.cloudflare.com/trust-hub/"
344
+ "security": "https://www.cloudflare.com/trust-hub/",
345
+ "llms": "https://developers.cloudflare.com/llms.txt"
341
346
  },
342
347
  "versionDetection": {
343
348
  "type": "http-hash-and-changelog"
@@ -372,7 +377,8 @@
372
377
  "changelog": "https://modal.com/docs/reference/changelog",
373
378
  "package": "https://pypi.org/project/modal/1.5.3/",
374
379
  "repository": "https://github.com/modal-labs/modal-client/tree/e8454377aa7c94e562d525c700a95a23b3d8838a",
375
- "security": "https://modal.com/security"
380
+ "security": "https://modal.com/security",
381
+ "llms": "https://modal.com/llms.txt"
376
382
  },
377
383
  "versionDetection": {
378
384
  "type": "pypi",
@@ -415,7 +421,8 @@
415
421
  "changelog": "https://better-auth.com/changelog",
416
422
  "package": "https://www.npmjs.com/package/better-auth/v/1.6.26",
417
423
  "repository": "https://github.com/better-auth/better-auth/tree/a16b30e8437927c08350194000178073b06af8cf",
418
- "security": "https://github.com/better-auth/better-auth/blob/a16b30e8437927c08350194000178073b06af8cf/SECURITY.md"
424
+ "security": "https://github.com/better-auth/better-auth/blob/a16b30e8437927c08350194000178073b06af8cf/SECURITY.md",
425
+ "llms": "https://better-auth.com/llms.txt"
419
426
  },
420
427
  "versionDetection": {
421
428
  "type": "npm-suite",
@@ -649,7 +656,8 @@
649
656
  "changelog": "https://github.com/vercel/turborepo/releases",
650
657
  "package": "https://www.npmjs.com/package/turbo",
651
658
  "repository": "https://github.com/vercel/turborepo",
652
- "security": "https://github.com/vercel/turborepo/security"
659
+ "security": "https://github.com/vercel/turborepo/security",
660
+ "llms": "https://turborepo.dev/llms.txt"
653
661
  },
654
662
  "versionDetection": {
655
663
  "type": "npm",
@@ -683,7 +691,8 @@
683
691
  "api": "https://api.flutter.dev/",
684
692
  "changelog": "https://docs.flutter.dev/release/release-notes",
685
693
  "repository": "https://github.com/flutter/flutter",
686
- "security": "https://github.com/flutter/flutter/security"
694
+ "security": "https://github.com/flutter/flutter/security",
695
+ "llms": "https://docs.flutter.dev/llms.txt"
687
696
  },
688
697
  "versionDetection": {
689
698
  "type": "release-index"
@@ -777,8 +786,8 @@
777
786
  "versionDetection": {
778
787
  "type": "http-hash-and-changelog"
779
788
  },
780
- "retrievedAt": "2026-08-08T07:53:01.687Z",
781
- "checksum": "9d428c76f42eafa6cd895e29bc54cd7b2ba0fa067c63eef457d6cf1a519b1b71",
789
+ "retrievedAt": "2026-08-08T10:20:35.016Z",
790
+ "checksum": "3e1d7ed893bbc41eff69d7009bd1269c811ea773a9edfb2a05e9e3c68af91343",
782
791
  "previousRetrievalChecksum": "d8117a391c5875a7b4d352a3f8b8b89ad12e6617a91038228f7279c732d0cd98",
783
792
  "topics": [
784
793
  "notifications",
@@ -994,7 +1003,8 @@
994
1003
  "api": "https://bun.sh/docs/api",
995
1004
  "changelog": "https://bun.sh/blog",
996
1005
  "repository": "https://github.com/oven-sh/bun",
997
- "security": "https://github.com/oven-sh/bun/security"
1006
+ "security": "https://github.com/oven-sh/bun/security",
1007
+ "llms": "https://bun.sh/llms.txt"
998
1008
  },
999
1009
  "versionDetection": {
1000
1010
  "type": "runtime-and-github-release"
@@ -1026,7 +1036,8 @@
1026
1036
  "docsIndex": "https://modelcontextprotocol.io/docs",
1027
1037
  "changelog": "https://modelcontextprotocol.io/specification/versioning",
1028
1038
  "repository": "https://github.com/modelcontextprotocol/modelcontextprotocol",
1029
- "security": "https://modelcontextprotocol.io/specification/2026-07-28/basic/security_best_practices"
1039
+ "security": "https://modelcontextprotocol.io/specification/2026-07-28/basic/security_best_practices",
1040
+ "llms": "https://modelcontextprotocol.io/llms.txt"
1030
1041
  },
1031
1042
  "versionDetection": {
1032
1043
  "type": "spec-version"
@@ -1058,7 +1069,8 @@
1058
1069
  "changelog": "https://github.com/vercel/ai/releases",
1059
1070
  "package": "https://www.npmjs.com/package/ai",
1060
1071
  "repository": "https://github.com/vercel/ai",
1061
- "security": "https://github.com/vercel/ai/security"
1072
+ "security": "https://github.com/vercel/ai/security",
1073
+ "llms": "https://ai-sdk.dev/llms.txt"
1062
1074
  },
1063
1075
  "versionDetection": {
1064
1076
  "type": "npm",
@@ -1,8 +1,7 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { lstat, readFile, readdir } from 'node:fs/promises'
3
- import { join, relative, sep } from 'node:path'
3
+ import { join, relative } from 'node:path'
4
4
 
5
- export const LABELS = ['OBSERVED', 'DOCUMENTED', 'REPRODUCED', 'INFERRED', 'RECOMMENDED', 'NOT VERIFIED']
6
5
  export const sha256 = body => createHash('sha256').update(body).digest('hex')
7
6
 
8
7
  // Canonical profile basename first; legacy name accepted with a deprecation notice at call sites.
@@ -47,11 +46,3 @@ export async function listFiles(root, predicate = () => true) {
47
46
  return output.sort((a, b) => relative(root, a).localeCompare(relative(root, b)))
48
47
  }
49
48
 
50
- export function slashRelative(root, path) {
51
- return relative(root, path).split(sep).join('/')
52
- }
53
-
54
- export function issue(status, rule, control, message, evidence, extra = {}) {
55
- const severity = status === 'FAIL' ? 'fail' : status === 'EXCEPTED' ? 'accepted-risk' : status === 'NOT_VERIFIED' ? 'warning' : 'pass'
56
- return { status, severity, rule, control, message, verificationType: extra.verificationType ?? 'static-sentinel', ...(evidence ? { evidence, path: evidence.path } : {}), ...extra }
57
- }
@@ -5,18 +5,21 @@ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep }
5
5
  import { pathToFileURL } from 'node:url'
6
6
  import { pathExists, readJsonYaml } from './lib.mjs'
7
7
 
8
- const capabilityNames = ['webControlPlane', 'flutter', 'agents', 'jobs', 'sandbox', 'connectors', 'knowledge', 'modelRouting', 'enterpriseIdentity', 'realtime', 'notifications', 'secrets']
8
+ const capabilityNames = ['web', 'flutter', 'agents', 'jobs', 'sandbox', 'connectors', 'knowledge', 'models', 'enterprise-identity', 'realtime', 'notifications']
9
9
  const observedListCap = 25
10
+ // Observation patterns are heuristic evidence, never proof. `secrets` is observed as advisory
11
+ // evidence but is not a declarable capability — secret custody guidance activates whenever
12
+ // production secrets exist.
10
13
  const patterns = {
11
- webControlPlane: /(?:next|@opennextjs\/cloudflare)/i,
14
+ web: /(?:next|@opennextjs\/cloudflare)/i,
12
15
  flutter: /(?:pubspec\.yaml|package:flutter)/i,
13
16
  agents: /(?:\beve\b|@workflow\/world|AgentRun)/i,
14
17
  jobs: /(?:pg-boss|PgBoss)/i,
15
18
  sandbox: /(?:@cloudflare\/sandbox|SandboxProvider|enableInternet)/i,
16
19
  connectors: /(?:\bMCP\b|webhook|connector)/i,
17
20
  knowledge: /(?:pgvector|embedding|\bknowledge\b)/i,
18
- modelRouting: /(?:ai-gateway|generateText|streamText|BYOK)/i,
19
- enterpriseIdentity: /(?:\bSCIM\b|\bSSO\b|saml)/i,
21
+ models: /(?:ai-gateway|generateText|streamText|BYOK)/i,
22
+ 'enterprise-identity': /(?:\bSCIM\b|\bSSO\b|saml)/i,
20
23
  realtime: /(?:EventSource|text\/event-stream|WebSocket)/i,
21
24
  notifications: /(?:firebase_messaging|APNs|notification)/i,
22
25
  secrets: /(?:OPENBAO|VAULT_ADDR|CLIENT_SECRET|DATABASE_URL)/i
@@ -40,18 +43,17 @@ async function inspectableFiles(root) {
40
43
 
41
44
  function baseDraft() {
42
45
  return {
43
- schemaVersion: 3,
44
- profileStatus: 'draft',
45
- foundation: { version: '0.3.0', baseline: 'vs-2026-08-07', adoption: 'supported' },
46
- project: { name: 'REQUIRED-CONFIRMED-PROJECT-NAME', kind: 'REQUIRED-CONFIRMED-PROJECT-KIND', lifecycle: 'brownfield', access: 'REQUIRED-CONFIRMED-ACCESS', tenancy: 'REQUIRED-CONFIRMED-TENANCY' },
47
- environments: { production: { hosting: 'REQUIRED-CONFIRMED-PRODUCTION-TARGET' }, localDevelopment: { trusted: true, allowances: [] } },
48
- capabilities: Object.fromEntries(capabilityNames.map(name => [name, { status: 'disabled', ownership: 'not-applicable' }])),
49
- exceptions: []
46
+ schemaVersion: 4,
47
+ foundationVersion: '0.4.0',
48
+ project: { name: 'REQUIRED-CONFIRMED-PROJECT-NAME', kind: 'REQUIRED-CONFIRMED-KIND', tier: 'REQUIRED-CONFIRMED-TIER', tenancy: 'REQUIRED-CONFIRMED-TENANCY' },
49
+ hosting: 'REQUIRED-CONFIRMED-HOSTING',
50
+ capabilities: [],
51
+ notes: []
50
52
  }
51
53
  }
52
54
 
53
55
  // Only literally exact versions are recorded as facts; ranges ("^1.2.3") are not laundered into
54
- // exact pins — the draft records them as unconfirmed instead.
56
+ // exact pins — they stay observational.
55
57
  function exact(value) {
56
58
  const normalized = String(value ?? '').trim().replace(/^v/, '')
57
59
  return /^\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(normalized) ? normalized : null
@@ -69,7 +71,7 @@ async function inspect(root) {
69
71
  for (const [name, value] of Object.entries({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) })) if (exact(value)) packageVersions[name] = exact(value)
70
72
  } catch { /* malformed manifests remain observational unknowns */ }
71
73
  }
72
- const observed = Object.fromEntries(capabilityNames.map(name => [name, []]))
74
+ const observed = Object.fromEntries(Object.keys(patterns).map(name => [name, []]))
73
75
  for (const path of files.filter(path => !/[\\/]\.vegastack[\\/]/.test(path) && (['.ts', '.tsx', '.js', '.mjs', '.cjs', '.json', '.yaml', '.yml', '.toml', '.sql', '.dart'].includes(extname(path)) || basename(path) === 'pubspec.yaml'))) {
74
76
  const rel = relative(root, path).split(sep).join('/')
75
77
  let body = ''
@@ -77,27 +79,14 @@ async function inspect(root) {
77
79
  for (const [name, pattern] of Object.entries(patterns)) if (pattern.test(`${rel}\n${body}`)) observed[name].push(rel)
78
80
  }
79
81
  const profile = baseDraft()
80
- for (const [name, evidence] of Object.entries(observed)) if (evidence.length) {
81
- const sourceRoot = evidence[0].includes('/') ? evidence[0].split('/').slice(0, -1).join('/') : '.'
82
- const versions = {}
83
- const candidates = {
84
- webControlPlane: [['next', 'next'], ['openNext', '@opennextjs/cloudflare']],
85
- agents: [['eve', 'eve'], ['workflowPostgres', '@workflow/world-postgres']],
86
- jobs: [['pgBoss', 'pg-boss']],
87
- sandbox: [['cloudflareSandbox', '@cloudflare/sandbox']]
88
- }[name] ?? []
89
- for (const [key, pkg] of candidates) if (packageVersions[pkg]) versions[key] = packageVersions[pkg]
90
- profile.capabilities[name] = { status: 'enabled', ownership: 'REQUIRED-CONFIRMED-OWNERSHIP', versions: Object.keys(versions).length ? versions : { unconfirmed: 'REQUIRED-EXACT-VERSION' }, placement: 'REQUIRED-CONFIRMED-PLACEMENT', sourceRoots: [sourceRoot], controls: { observedSourceRoots: evidence } }
91
- }
82
+ profile.capabilities = capabilityNames.filter(name => observed[name].length)
92
83
  const artifacts = ['.vegastack/architecture.json']
93
- if (Object.values(profile.capabilities).some(item => item.status === 'enabled')) artifacts.push('docs/architecture/service-design.md')
94
- if (profile.environments.production.hosting !== 'none') artifacts.push('docs/architecture/deployment-review.md')
95
- if (['connectors', 'sandbox', 'agents'].some(name => profile.capabilities[name].status === 'enabled')) artifacts.push('docs/architecture/threat-model.md')
84
+ if (['connectors', 'sandbox', 'agents'].some(name => observed[name].length)) artifacts.push('docs/architecture/threat-model.md')
96
85
  const after = await inspectableFiles(root)
97
86
  let changed = after.length !== before.size
98
87
  for (const path of after) if (!before.has(path) || before.get(path) !== (await lstat(path)).mtimeMs) changed = true
99
88
  if (changed) throw new Error('inspection mutation guard detected a changed file inventory')
100
- return { mode: 'brownfield-observed', mutated: false, observed, packageVersions, profileDraft: profile, relevantArtifacts: artifacts, caveats: ['Detection is heuristic and does not prove absence, ownership, placement, compliance, runtime behavior, or project intent. Every REQUIRED field must be confirmed before use.'] }
89
+ return { mode: 'brownfield-observed', mutated: false, observed, packageVersions, profileDraft: profile, relevantArtifacts: artifacts, caveats: ['Detection is heuristic and does not prove absence, placement, compliance, runtime behavior, or project intent. Every REQUIRED field must be confirmed before use.'] }
101
90
  }
102
91
 
103
92
  // Compact context-friendly view for interactive agent use; --json prints the full result.
@@ -117,26 +106,31 @@ function capObserved(result) {
117
106
  }
118
107
 
119
108
  function fromAnswers(answers) {
120
- if (answers.schemaVersion === 3) return answers
109
+ if (answers.schemaVersion === 4) return answers
121
110
  const profile = baseDraft()
122
- for (const key of ['profileStatus', 'project', 'environments', 'capabilities', 'data', 'objectives', 'exceptions']) if (answers[key] !== undefined) profile[key] = answers[key]
111
+ for (const key of ['project', 'hosting', 'capabilities', 'notes', 'foundationVersion']) if (answers[key] !== undefined) profile[key] = answers[key]
123
112
  return profile
124
113
  }
125
114
 
126
- function migrateV2(old) {
127
- if (old.schemaVersion !== 2) throw new Error('migrate-v2 requires a schemaVersion 2 profile')
115
+ // Deterministic v3 -> v4 migration: exceptions are dropped (the advisor records deliberate
116
+ // deviations in notes/ADR documents, never as suppression machinery), capability detail collapses
117
+ // to the enabled list, and versions return to lockfiles.
118
+ const v3CapabilityMap = { webControlPlane: 'web', flutter: 'flutter', agents: 'agents', jobs: 'jobs', sandbox: 'sandbox', connectors: 'connectors', knowledge: 'knowledge', modelRouting: 'models', enterpriseIdentity: 'enterprise-identity', realtime: 'realtime', notifications: 'notifications' }
119
+ const v3KindMap = { 'saas-product': 'saas', 'internal-product': 'internal-tool', 'public-product': 'saas', 'platform-service': 'api', 'shared-package': 'package' }
120
+
121
+ function migrateV3(old) {
122
+ if (old.schemaVersion !== 3) throw new Error('migrate requires a schemaVersion 3 profile; older versions need a fresh v4 profile from answers')
128
123
  const profile = baseDraft()
129
- profile.project = { name: old.project ?? 'REQUIRED-CONFIRMED-PROJECT-NAME', kind: 'saas-product', lifecycle: 'migration', access: 'authenticated', tenancy: old.tenancy?.storage === 'shared-schema-composite-keys' ? 'multi-tenant-shared-schema' : 'REQUIRED-CONFIRMED-TENANCY' }
130
- profile.environments.production.hosting = old.hostingProfile ?? 'REQUIRED-CONFIRMED-HOSTING'
131
- const owned = (versions, placement, sourceRoots, controls = {}) => ({ status: 'enabled', ownership: 'owned', versions, placement, sourceRoots, controls })
132
- profile.capabilities.webControlPlane = owned({ bun: old.versions?.bun, node: old.versions?.node, next: old.versions?.next, ...(old.versions?.openNext !== 'not-applicable' ? { openNext: old.versions?.openNext } : {}), betterAuth: old.versions?.betterAuth }, old.runtimePlacement?.next, old.sourceRoots?.next, { canonicalApi: old.capabilities?.api?.canonical, openapiGenerated: old.capabilities?.api?.openapiGenerated, secureCookies: true })
133
- profile.capabilities.flutter = owned({ flutter: 'REQUIRED-EXACT-VERSION' }, old.runtimePlacement?.flutter, ['REQUIRED-CONFIRMED-FLUTTER-ROOT'], { delegatedOAuthPkce: old.identity?.delegated === 'oauth2.1-oidc-code-pkce', generatedRestClient: old.capabilities?.api?.openapiGenerated === true })
134
- profile.capabilities.agents = owned({ eve: old.versions?.eve, workflowWorldContract: old.versions?.workflowWorldContract, workflowLocal: old.versions?.workflowLocal, workflowPostgres: old.versions?.workflowPostgres, node: old.versions?.node }, old.runtimePlacement?.eve, old.sourceRoots?.eve, { workflowWorld: 'postgres', agentRun: true, admission: 'pg-boss' })
135
- profile.capabilities.jobs = owned({ pgBoss: old.versions?.pgBoss, postgres: old.versions?.postgres }, old.runtimePlacement?.jobs, old.sourceRoots?.jobs, { roles: old.capabilities?.jobs?.roles ?? [] })
136
- profile.capabilities.sandbox = owned({ provider: 'REQUIRED-EXACT-VERSION' }, old.runtimePlacement?.sandboxBroker, ['REQUIRED-CONFIRMED-SANDBOX-ROOT'], { provider: old.capabilities?.sandbox?.provider, egress: old.capabilities?.sandbox?.egress, databaseCredentials: old.capabilities?.sandbox?.databaseCredentials, trustedBroker: true })
137
- if (old.capabilities?.secrets === 'openbao') profile.capabilities.secrets = owned({ openbao: 'REQUIRED-EXACT-VERSION' }, 'external-service', ['REQUIRED-CONFIRMED-SECRETS-CONFIG-ROOT'], { provider: 'openbao' })
138
- profile.exceptions = []
139
- return { profile, guidance: ['The v2 format implied a full platform, so all implied capabilities are enabled in this draft.', 'Confirm project kind/access/tenancy, Flutter and sandbox roots/versions, controls, capability ownership, data/objectives, and migrate each exception to exact v3 rule/control/path scope.', 'No source file or old profile was changed.'] }
124
+ profile.project = {
125
+ name: old.project?.name ?? 'REQUIRED-CONFIRMED-PROJECT-NAME',
126
+ kind: v3KindMap[old.project?.kind] ?? 'REQUIRED-CONFIRMED-KIND',
127
+ tier: 'REQUIRED-CONFIRMED-TIER',
128
+ tenancy: old.project?.tenancy ?? 'REQUIRED-CONFIRMED-TENANCY'
129
+ }
130
+ profile.hosting = old.environments?.production?.hosting ?? 'REQUIRED-CONFIRMED-HOSTING'
131
+ profile.capabilities = Object.entries(old.capabilities ?? {}).filter(([, value]) => value?.status === 'enabled').map(([name]) => v3CapabilityMap[name]).filter(Boolean)
132
+ profile.notes = (old.exceptions ?? []).map(exception => `migrated deviation (was exception ${exception.id} on ${exception.ruleId}): ${exception.decision ?? exception.rationale ?? 'see original ADR'}`)
133
+ return { profile, guidance: ['Confirm the tier deliberately — it decides which concerns apply, not which tools.', 'v3 exceptions were converted to notes; the advisor has no suppression machinery.', 'Versions now come from lockfiles at advice time.', 'No source file or old profile was changed.'] }
140
134
  }
141
135
 
142
136
  async function assertNoSymlink(path) {
@@ -199,18 +193,18 @@ function confinedOutput(dir, output) {
199
193
 
200
194
  async function main() {
201
195
  const options = parse(process.argv.slice(2))
202
- if (options.command === 'help') return console.log('Usage: profile-tool.mjs inspect [DIR] [--json] | scaffold ANSWERS.json --dir DIR [--write] [--force] [--output PATH] | migrate-v2 PROFILE --dir DIR [--write] [--force] [--output PATH]\n\ninspect prints a compact summary by default; --json prints the full draft and evidence.\nscaffold answers format: see assets/answers-example.json next to this skill.\nWrites are atomic, refuse symlinks, stay inside --dir, and require --force to replace differing content.')
196
+ if (options.command === 'help') return console.log('Usage: profile-tool.mjs inspect [DIR] [--json] | scaffold ANSWERS.json --dir DIR [--write] [--force] [--output PATH] | migrate PROFILE --dir DIR [--write] [--force] [--output PATH]\n\ninspect prints a compact summary by default; --json prints the full draft and evidence.\nscaffold answers format: see assets/answers-example.json next to this skill.\nmigrate converts a v3 profile to a v4 draft (exceptions become notes).\nWrites are atomic, refuse symlinks, stay inside --dir, and require --force to replace differing content.')
203
197
  if (options.command === 'inspect') {
204
198
  const root = resolve(options.input ?? options.dir)
205
199
  const result = await inspect(root)
206
200
  return console.log(JSON.stringify(options.json ? capObserved(result) : summarizeInspection(result), null, 2))
207
201
  }
208
- if (!['scaffold', 'migrate-v2'].includes(options.command) || !options.input) throw new Error('A supported command and input file are required')
202
+ if (!['scaffold', 'migrate'].includes(options.command) || !options.input) throw new Error('A supported command and input file are required')
209
203
  const source = resolve(options.input)
210
204
  const parsed = await readJsonYaml(source)
211
- const result = options.command === 'migrate-v2' ? migrateV2(parsed) : { profile: fromAnswers(parsed), guidance: ['Generated only from supplied answers; confirm draft facts before CI.'] }
205
+ const result = options.command === 'migrate' ? migrateV3(parsed) : { profile: fromAnswers(parsed), guidance: ['Generated only from supplied answers; confirm draft facts before relying on them.'] }
212
206
  const body = `${JSON.stringify(result.profile, null, 2)}\n`
213
- const defaultOutput = options.command === 'migrate-v2' ? '.vegastack/architecture.v3-draft.json' : '.vegastack/architecture.json'
207
+ const defaultOutput = options.command === 'migrate' ? '.vegastack/architecture.v4-draft.json' : '.vegastack/architecture.json'
214
208
  const output = confinedOutput(options.dir, options.output ?? defaultOutput)
215
209
  const payload = { mode: options.write ? 'authorized-write' : 'dry-run', output, profile: result.profile, guidance: result.guidance }
216
210
  if (!options.write) return console.log(JSON.stringify(payload, null, 2))
@@ -220,4 +214,4 @@ async function main() {
220
214
 
221
215
  if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) main().catch(error => { console.error(`error: ${error.message}`); process.exitCode = 1 })
222
216
 
223
- export { inspect, migrateV2, fromAnswers, atomicWrite }
217
+ export { inspect, migrateV3, fromAnswers, atomicWrite }
@@ -13,7 +13,7 @@ const defaultRegistry = join(skillRoot, 'refresh', 'sources.json')
13
13
  const maximumBytes = 5 * 1024 * 1024
14
14
  // Every host referenced by refresh/sources.json must appear here; loadRegistry enforces the
15
15
  // consistency so the allowlist cannot silently drift from the registry.
16
- const approvedHosts = new Set(['agentskills.io', 'ai-sdk.dev', 'ai.google.dev', 'api.flutter.dev', 'aws.amazon.com', 'better-auth.com', 'bun.sh', 'code.claude.com', 'developer.apple.com', 'developers.cloudflare.com', 'developers.openai.com', 'docs.aws.amazon.com', 'docs.flutter.dev', 'firebase.google.com', 'git.postgresql.org', 'github.com', 'hermes-agent.nousresearch.com', 'learn.chatgpt.com', 'modal.com', 'modelcontextprotocol.io', 'nextjs.org', 'openbao.org', 'openid.github.io', 'opennext.js.org', 'opentelemetry.io', 'platform.claude.com', 'pub.dev', 'pypi.org', 'raw.githubusercontent.com', 'registry.npmjs.org', 'riverpod.dev', 'turborepo.dev', 'workflow-sdk.dev', 'www.cloudflare.com', 'www.npmjs.com', 'www.postgresql.org'])
16
+ const approvedHosts = new Set(['agentskills.io', 'ai-sdk.dev', 'ai.google.dev', 'api.flutter.dev', 'api.osv.dev', 'aws.amazon.com', 'better-auth.com', 'bun.sh', 'code.claude.com', 'developer.apple.com', 'developers.cloudflare.com', 'developers.openai.com', 'docs.aws.amazon.com', 'docs.flutter.dev', 'firebase.google.com', 'git.postgresql.org', 'github.com', 'hermes-agent.nousresearch.com', 'learn.chatgpt.com', 'modal.com', 'modelcontextprotocol.io', 'nextjs.org', 'openbao.org', 'openid.github.io', 'opennext.js.org', 'opentelemetry.io', 'platform.claude.com', 'pub.dev', 'pypi.org', 'raw.githubusercontent.com', 'registry.npmjs.org', 'riverpod.dev', 'turborepo.dev', 'workflow-sdk.dev', 'www.cloudflare.com', 'www.npmjs.com', 'www.postgresql.org'])
17
17
 
18
18
  function flagValue(argv, flag) {
19
19
  const value = argv.shift()
@@ -22,7 +22,7 @@ function flagValue(argv, flag) {
22
22
  }
23
23
 
24
24
  function args(argv) {
25
- const result = { registry: defaultRegistry, cache: '.vegastack/evidence-cache.json', report: '.vegastack/evidence-drift.json', topics: [], offline: false, acceptBaselines: false, now: new Date().toISOString() }
25
+ const result = { registry: defaultRegistry, cache: '.vegastack/evidence-cache.json', report: '.vegastack/evidence-drift.json', topics: [], offline: false, acceptBaselines: false, now: new Date().toISOString(), compatibility: join(skillRoot, 'references', 'foundation-compatibility.json') }
26
26
  while (argv.length) {
27
27
  const flag = argv.shift()
28
28
  if (flag === '--registry') result.registry = resolve(flagValue(argv, flag))
@@ -33,11 +33,31 @@ function args(argv) {
33
33
  else if (flag === '--accept-baselines') result.acceptBaselines = true
34
34
  else if (flag === '--verify-baselines') { /* explicit alias for the default online verification run */ }
35
35
  else if (flag === '--now') result.now = flagValue(argv, flag)
36
+ else if (flag === '--compatibility') result.compatibility = resolve(flagValue(argv, flag))
36
37
  else throw new Error(`Unknown option: ${flag}`)
37
38
  }
38
39
  return result
39
40
  }
40
41
 
42
+ // Security-advisory watch: query OSV.dev for every pinned npm/PyPI package. Advisories against a
43
+ // pinned version are the highest-value freshness signal — they surface in the weekly report and
44
+ // fail closed for critical sources so a vulnerable pin is never silently kept.
45
+ async function osvAdvisories(source, allowHttpLocalhost) {
46
+ const detection = source.versionDetection ?? {}
47
+ const ecosystem = { npm: 'npm', 'npm-suite': 'npm', pypi: 'PyPI' }[detection.type]
48
+ const version = String(source.pinnedVersion ?? '')
49
+ if (!ecosystem || !/^\d/.test(version)) return []
50
+ const packages = detection.type === 'npm-suite' ? detection.packages : [detection.package]
51
+ const findings = []
52
+ for (const name of packages) {
53
+ const response = await safeFetch('https://api.osv.dev/v1/query', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ package: { name, ecosystem }, version }) }, allowHttpLocalhost)
54
+ if (!response.ok) throw new Error(`OSV HTTP ${response.status} for ${name}`)
55
+ const data = JSON.parse(new TextDecoder().decode(await readBounded(response)))
56
+ for (const vuln of data.vulns ?? []) findings.push({ package: name, version, id: vuln.id, summary: vuln.summary ?? null })
57
+ }
58
+ return findings
59
+ }
60
+
41
61
  function privateAddress(address) {
42
62
  if (address === '::1' || address === '::' || address.startsWith('fc') || address.startsWith('fd') || address.startsWith('fe80:')) return true
43
63
  if (!isIP(address)) return true
@@ -240,7 +260,17 @@ export async function refreshEvidence(options) {
240
260
  const now = new Date(options.now)
241
261
  if (Number.isNaN(now.getTime())) throw new Error(`Invalid --now timestamp: ${options.now}`)
242
262
  const selected = registry.sources.filter(source => !options.topics.length || source.topics.some(topic => options.topics.includes(topic)))
243
- const report = { schemaVersion: 1, generatedAt: now.toISOString(), offline: options.offline, acceptBaselines: options.acceptBaselines, selected: selected.map(source => source.id), drift: [], versionDrift: [], stale: [], unavailable: [], unaffected: [], manualVersionReview: [], acceptedBaselines: [] }
263
+ const report = { schemaVersion: 1, generatedAt: now.toISOString(), offline: options.offline, acceptBaselines: options.acceptBaselines, selected: selected.map(source => source.id), drift: [], versionDrift: [], stale: [], unavailable: [], unaffected: [], manualVersionReview: [], acceptedBaselines: [], advisories: [], advisoryCheckFailed: [], reviewOverdue: [] }
264
+ // Baseline-adoption nags: a reviewBy date that passed without a human decision, or a critical
265
+ // pin lagging a known-newer current version, is how pins rot politely — surface both on every
266
+ // run (warning, never fail-closed).
267
+ if (options.compatibility && await pathExists(options.compatibility)) {
268
+ const compatibility = JSON.parse(await readFile(options.compatibility, 'utf8'))
269
+ for (const [name, baseline] of Object.entries(compatibility.baselines ?? {})) {
270
+ if (baseline.reviewBy && Date.parse(`${baseline.reviewBy}T23:59:59Z`) < now.getTime()) report.reviewOverdue.push({ baseline: name, reviewBy: baseline.reviewBy, state: baseline.state })
271
+ }
272
+ }
273
+ report.pinLag = registry.sources.filter(source => source.critical && /^\d/.test(String(source.pinnedVersion ?? '')) && /^\d/.test(String(source.currentVersion ?? '')) && source.pinnedVersion !== source.currentVersion).map(source => ({ id: source.id, pinned: source.pinnedVersion, current: source.currentVersion }))
244
274
  const baselineUpdates = new Map()
245
275
  for (const source of selected) {
246
276
  const prior = cache.sources[source.id]
@@ -261,6 +291,11 @@ export async function refreshEvidence(options) {
261
291
  else report.unaffected.push(source.id)
262
292
  continue
263
293
  }
294
+ try {
295
+ for (const advisory of await osvAdvisories(source, options.allowHttpLocalhost)) report.advisories.push(item(source, advisory))
296
+ } catch (error) {
297
+ report.advisoryCheckFailed.push(item(source, { error: error.message }))
298
+ }
264
299
  const detectedVersion = await detectVersion(source, options.allowHttpLocalhost)
265
300
  if (detectedVersion && detectedVersion !== source.currentVersion) {
266
301
  // Under --accept-baselines a verified version change is accepted, not re-reported as drift —
@@ -316,13 +351,16 @@ export async function refreshEvidence(options) {
316
351
  }
317
352
  await atomicJson(options.cache, cache)
318
353
  await atomicJson(options.report, report)
319
- const failClosed = [...report.stale, ...report.unavailable, ...report.drift, ...report.versionDrift, ...report.manualVersionReview.filter(entry => entry.due)].some(entry => entry.critical || entry.integrityFailure)
354
+ const failClosed = [...report.stale, ...report.unavailable, ...report.drift, ...report.versionDrift, ...report.advisories, ...report.manualVersionReview.filter(entry => entry.due)].some(entry => entry.critical || entry.integrityFailure)
320
355
  return { report, failClosed }
321
356
  }
322
357
 
323
358
  if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
324
359
  const options = args(process.argv.slice(2))
325
360
  const { report, failClosed } = await refreshEvidence(options)
326
- console.log(`refresh-evidence: selected=${report.selected.length} drift=${report.drift.length} version-drift=${report.versionDrift.length} stale=${report.stale.length} unavailable=${report.unavailable.length}${report.acceptedBaselines.length ? ` accepted=${report.acceptedBaselines.length}` : ''}`)
361
+ console.log(`refresh-evidence: selected=${report.selected.length} drift=${report.drift.length} version-drift=${report.versionDrift.length} stale=${report.stale.length} unavailable=${report.unavailable.length} advisories=${report.advisories.length}${report.reviewOverdue.length ? ` review-overdue=${report.reviewOverdue.length}` : ''}${report.acceptedBaselines.length ? ` accepted=${report.acceptedBaselines.length}` : ''}`)
362
+ for (const overdue of report.reviewOverdue) console.error(`warning: baseline ${overdue.baseline} (${overdue.state}) passed its reviewBy ${overdue.reviewBy} — a human adoption decision is overdue`)
363
+ for (const lag of report.pinLag ?? []) console.error(`warning: critical source ${lag.id} pin ${lag.pinned} lags current ${lag.current} — review adoption or record the deliberate hold`)
364
+ for (const advisory of report.advisories) console.error(`advisory: ${advisory.package}@${advisory.version} — ${advisory.id}${advisory.summary ? `: ${advisory.summary}` : ''}`)
327
365
  if (failClosed) process.exitCode = 1
328
366
  }