@hybridlabor-api/aos 4.12.1 → 4.13.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 (33) hide show
  1. package/.agents/vendor-manifest.json +6 -0
  2. package/.claude/agents/reviewer.md +1 -1
  3. package/.claude/agents/techlead.md +1 -1
  4. package/.opencode/agents/architect.md +1 -1
  5. package/.opencode/agents/reviewer.md +1 -1
  6. package/.opencode/agents/techlead.md +1 -1
  7. package/README.de.md +10 -9
  8. package/README.md +10 -9
  9. package/README.pt.md +10 -9
  10. package/THIRD_PARTY_NOTICES.md +94 -0
  11. package/bin/aos-store.mjs +48 -45
  12. package/installer.js +22 -20
  13. package/lib/scenario-store-index.json +1 -0
  14. package/lib/store-shared.mjs +81 -4
  15. package/lib/store-ui/index.html +14 -5
  16. package/lib/store-ui/server.mjs +44 -21
  17. package/package.json +2 -2
  18. package/scripts/build-plugin-manifest.mjs +33 -10
  19. package/scripts/build-scenario-store-index.mjs +154 -0
  20. package/skills/bdbrainstorm/SKILL.md +1 -0
  21. package/skills/global_config/agent-pipeline/SKILL.md +4 -0
  22. package/skills/global_config/aos-store/SKILL.md +3 -3
  23. package/skills/global_config/design-control-loop/SKILL.md +184 -0
  24. package/skills/global_config/design-control-loop/references/agent-iteration.ts +174 -0
  25. package/skills/global_config/design-control-loop/references/agent-runner-templates.md +157 -0
  26. package/skills/global_config/design-control-loop/references/control-loop-taxonomy.md +75 -0
  27. package/skills/global_config/design-control-loop/references/example-control-loop.md +57 -0
  28. package/skills/global_config/design-control-loop/references/example-skill.md +171 -0
  29. package/skills/global_config/design-control-loop/references/memory-template.md +7 -0
  30. package/skills/global_config/design-control-loop/references/prompt-template.md +58 -0
  31. package/skills/global_config/design-control-loop/references/response-template.md +103 -0
  32. package/skills/global_config/design-control-loop/references/skill-template.md +57 -0
  33. package/skills/global_config/design-control-loop/references/workflow-template.yml +273 -0
package/installer.js CHANGED
@@ -3354,27 +3354,27 @@ function loadPipelineConfig(projectDir = currentDir) {
3354
3354
  function resolveAgentConfig(agentSlug, harness, pipelineConfig = null) {
3355
3355
  const normSlug = agentSlug.toLowerCase().replace(/-/g, '_');
3356
3356
  const roleCfg = pipelineConfig ? (pipelineConfig[normSlug] || pipelineConfig[agentSlug]) : null;
3357
- if (roleCfg) {
3358
- if (roleCfg.model) {
3359
- return {
3360
- model: roleCfg.model,
3361
- tier: roleCfg.tier || 'custom',
3362
- enabled: roleCfg.enabled !== false,
3363
- harness: roleCfg.harness || harness
3364
- };
3365
- }
3366
- if (roleCfg.tier && CANONICAL_TIERS[roleCfg.tier] && CANONICAL_TIERS[roleCfg.tier][harness]) {
3367
- return {
3368
- model: CANONICAL_TIERS[roleCfg.tier][harness],
3369
- tier: roleCfg.tier,
3370
- enabled: roleCfg.enabled !== false,
3371
- harness: roleCfg.harness || harness
3372
- };
3373
- }
3357
+ // A role's model/harness describes where AO runs that role; only valid for that harness.
3358
+ const ownHarness = !!roleCfg && (!roleCfg.harness || roleCfg.harness === harness);
3359
+ if (roleCfg && ownHarness && roleCfg.model) {
3360
+ return {
3361
+ model: roleCfg.model,
3362
+ tier: roleCfg.tier || 'custom',
3363
+ enabled: roleCfg.enabled !== false,
3364
+ harness
3365
+ };
3366
+ }
3367
+ if (roleCfg && roleCfg.tier && CANONICAL_TIERS[roleCfg.tier] && CANONICAL_TIERS[roleCfg.tier][harness]) {
3368
+ return {
3369
+ model: CANONICAL_TIERS[roleCfg.tier][harness],
3370
+ tier: roleCfg.tier,
3371
+ enabled: roleCfg.enabled !== false,
3372
+ harness
3373
+ };
3374
3374
  }
3375
3375
  const defaultTier = (normSlug === 'architect' || normSlug === 'reviewer') ? 'reasoning_max' : 'standard_fast';
3376
3376
  const fallbackModel = (CANONICAL_TIERS[defaultTier] && CANONICAL_TIERS[defaultTier][harness]) || 'inherit';
3377
- return { model: fallbackModel, tier: defaultTier, enabled: true, harness };
3377
+ return { model: fallbackModel, tier: defaultTier, enabled: !(roleCfg && roleCfg.model && roleCfg.enabled === false), harness };
3378
3378
  }
3379
3379
 
3380
3380
  // Generates .claude/agents/<name>.md — Claude Code's native subagent format.
@@ -3385,7 +3385,8 @@ function compileClaudeAgents(agents, targetDir, pipelineConfig = null) {
3385
3385
  for (const a of agents) {
3386
3386
  const slug = a.name.toLowerCase().replace(/_/g, '-');
3387
3387
  const resolved = resolveAgentConfig(slug, 'claude', pipelineConfig);
3388
- const model = (resolved && resolved.model) ? resolved.model.toLowerCase() : (a.model ? a.model.toLowerCase() : 'inherit');
3388
+ let model = (resolved && resolved.model) ? resolved.model.toLowerCase() : (a.model ? a.model.toLowerCase() : 'inherit');
3389
+ if (!/^(opus|sonnet|haiku|inherit|claude-.+)$/.test(model)) model = CANONICAL_TIERS[resolved.tier in CANONICAL_TIERS ? resolved.tier : 'standard_fast'].claude;
3389
3390
  const body = [
3390
3391
  a.role,
3391
3392
  a.skills.length ? `**Primary skills:** ${a.skills.join(', ')}` : null,
@@ -3412,7 +3413,8 @@ function compileOpenCodeAgents(agents, targetDir, pipelineConfig = null) {
3412
3413
  for (const a of agents) {
3413
3414
  const slug = a.name.toLowerCase().replace(/_/g, '-');
3414
3415
  const resolved = resolveAgentConfig(slug, 'opencode', pipelineConfig);
3415
- const model = resolved && resolved.model ? resolved.model : null;
3416
+ let model = resolved && resolved.model ? resolved.model : null;
3417
+ if (model && !model.includes('/')) model = CANONICAL_TIERS[resolved.tier in CANONICAL_TIERS ? resolved.tier : 'standard_fast'].opencode;
3416
3418
  const body = [
3417
3419
  a.role,
3418
3420
  a.skills.length ? `**Primary skills:** ${a.skills.join(', ')}` : null,
@@ -0,0 +1 @@
1
+ {"version":"1.1.0","generated_at":"2026-09-28T16:20:46+02:00","pinned_commit":"5786accd3bb6d79808330230c698898862e8a1d4","source":{"id":"scenario","label":"Scenario","upstream":"https://github.com/scenario-labs/skills","raw_base":"https://raw.githubusercontent.com/scenario-labs/skills","license":"MIT","copyright":"Copyright (c) 2026 Scenario","third_party":true},"harness_support":["claude","antigravity","codex","cursor","roo"],"skills":{"scenario-blender-animation":{"description":"Use when animating characters or objects in Blender through Python, a bouncing ball, walk or run cy…","category":"media-eventtech","tier":"free","origin":"scenario","requires_auth":false,"sha256":"aff7450263b6435bd3d654147d971f38ed8effb37ea6aac86e21a3e574d4fa49","upstream_path":"skills/dcc/blender/scenario-blender-animation/SKILL.md","requires":["scenario-blender-expert"],"software":["Blender"],"hasScripts":true,"scriptCount":1,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"aff7450263b6435bd3d654147d971f38ed8effb37ea6aac86e21a3e574d4fa49","size":20288,"exec":false},{"path":"references/critique.md","sha256":"cb00a869d0847033d0d5dcc31ced3c4b21169f2c40158e8907ff22384d9c71da","size":29916,"exec":false},{"path":"references/expert-notes.md","sha256":"811e83faf618f9edfcc273d1c21d903c76c787dd73e2092c9c0d31cb3fd06454","size":27069,"exec":false},{"path":"references/procedures.md","sha256":"514cf2ce6f40ecb1184cc57ee63c21deb76193111986d2238e4e909e3fa9155c","size":41355,"exec":false},{"path":"references/sources.md","sha256":"1b45bbf9c056ce1bfd266a3bdfd55677803898758d47f4bafe39bdb34043f662","size":9058,"exec":false},{"path":"scripts/bx_anim.py","sha256":"75db8650041f02d881bb5895130571c8ea7e437d2c20fa77154d961ab8c5f005","size":103257,"exec":false}]},"scenario-blender-expert":{"description":"Use when doing any 3D work in Blender through Python or an MCP bridge, headless or in a live sessio…","category":"media-eventtech","tier":"free","origin":"scenario","requires_auth":false,"sha256":"66af9735948dd16ed619c48fdd66b03124a4b521e70543df0f90f77fe6f55376","upstream_path":"skills/dcc/blender/scenario-blender-expert/SKILL.md","software":["Blender"],"hasScripts":true,"scriptCount":3,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"66af9735948dd16ed619c48fdd66b03124a4b521e70543df0f90f77fe6f55376","size":12817,"exec":false},{"path":"references/blender-5.2-deltas.md","sha256":"a3edcef99a5d44c3853901417b9682b17bdda3185bc74f3d1ccada6c7c5f6677","size":118596,"exec":false},{"path":"references/bpy-reliability.md","sha256":"b43625d8ae078dd983fb8b4ddc3d99fa3b71d793142ee1956d6b306b24957566","size":24633,"exec":false},{"path":"scripts/bx_audit.py","sha256":"0ff01ffdc6f9413c42be15016c84fbf8f0696434e1b30cc5ad952813d748a0f8","size":9810,"exec":false},{"path":"scripts/bx_gui.py","sha256":"d5c020f27d2714f7f773da30cfa190017be453a621bb45f1eccececa3a33d60c","size":8951,"exec":false},{"path":"scripts/bx_review.py","sha256":"3862dc1452960f01219206bbc0f24d487810d0f4cda6ab41b97108e522e53986","size":14914,"exec":false}]},"scenario-blender-geometry-nodes":{"description":"Use when building or debugging Geometry Nodes, headless or live: procedural modeling, scattering ob…","category":"media-eventtech","tier":"free","origin":"scenario","requires_auth":false,"sha256":"c7465134d8506ad0975063974d5c462ea3638aadf9f316d74dc81c4b7a522923","upstream_path":"skills/dcc/blender/scenario-blender-geometry-nodes/SKILL.md","requires":["scenario-blender-expert"],"software":[],"hasScripts":true,"scriptCount":1,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"c7465134d8506ad0975063974d5c462ea3638aadf9f316d74dc81c4b7a522923","size":16371,"exec":false},{"path":"references/critique.md","sha256":"88331d11fad74946a8989cdfd1d79703f65fe8efd8565c9be152bdbb85cb9e8b","size":11373,"exec":false},{"path":"references/expert-notes.md","sha256":"49fa43b73f7a423a078e2405bd962545a3f6a2a10fa3e57965914b2eb9320c11","size":18172,"exec":false},{"path":"references/procedures.md","sha256":"8f0b2dc24dcbd01296351cee4177cdd7ce11fa7077402293541c9040f85cb58d","size":55863,"exec":false},{"path":"references/sources.md","sha256":"fe060140ec154d9f0c49e0efbe996a9ebf10c1eb0de0467614cf9aa2116df374","size":7510,"exec":false},{"path":"scripts/bx_gn.py","sha256":"b325317f54e1e944f33f1af91e141c20bd5ebe7d3a3c487738fa70fd725639e0","size":57043,"exec":false}]},"scenario-blender-hard-surface":{"description":"Use when modeling hard-surface objects in Blender (sci-fi panels, props, weapons, vehicles, robots,…","category":"media-eventtech","tier":"free","origin":"scenario","requires_auth":false,"sha256":"f14733f37d3f0ee2654fcac4f9f0bb3c1bc3112854a38848f71736eb155a525d","upstream_path":"skills/dcc/blender/scenario-blender-hard-surface/SKILL.md","requires":["scenario-blender-expert"],"software":["Blender"],"hasScripts":true,"scriptCount":1,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"f14733f37d3f0ee2654fcac4f9f0bb3c1bc3112854a38848f71736eb155a525d","size":18295,"exec":false},{"path":"references/critique.md","sha256":"32fae7dc8e3759556aa4571b868d5fcb783217ce9270c9802aa8b380ca24ee4d","size":14411,"exec":false},{"path":"references/expert-notes.md","sha256":"b9682d071a0fcafa03afa99ae9aecf3831176681d5e74eb7d10c27369386a87b","size":17875,"exec":false},{"path":"references/procedures.md","sha256":"6dbe1b7567a6ff1f21ff5debf4c9235cd7fc4ccea51f736a1b74448bcccca70c","size":25309,"exec":false},{"path":"references/sources.md","sha256":"71d90f1effa9477abdfe4bd77bea4d6b0fb8c21ae2ec449168d9b7d972ba77eb","size":7412,"exec":false},{"path":"scripts/bx_hardsurface.py","sha256":"a7eb60b356c16609573ea8bb0005cd1278e908dcd7593eb7ff1f749ceb7b9999","size":69529,"exec":false}]},"scenario-blender-lighting-rendering":{"description":"Use when lighting, rendering or compositing in Blender: light a character, product or hero shot, in…","category":"media-eventtech","tier":"free","origin":"scenario","requires_auth":false,"sha256":"93a3bea574312104922feba0884afa05332a2a7e12720995b9fabb948e0d297e","upstream_path":"skills/dcc/blender/scenario-blender-lighting-rendering/SKILL.md","requires":["scenario-blender-expert"],"software":["Blender"],"hasScripts":true,"scriptCount":1,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"93a3bea574312104922feba0884afa05332a2a7e12720995b9fabb948e0d297e","size":20126,"exec":false},{"path":"references/critique.md","sha256":"04c4b949a62019d4cfe70cbe536dc96b2411fdd0c82980b750004e0f04ce38db","size":8035,"exec":false},{"path":"references/expert-notes.md","sha256":"fcf07efdf876fa500f96e25ce8477c56bd70c3fe13451ee85b26146c75d70f38","size":19917,"exec":false},{"path":"references/procedures.md","sha256":"774ce5f5c1577d3a74845b73bd716348cb587d0f0d7ea447a5dd64e706ff84a0","size":30203,"exec":false},{"path":"references/sources.md","sha256":"8beafcf59b2822a81ce09f255ef81cc333d12a464f0a58e6f6ac5e37c3679999","size":15667,"exec":false},{"path":"scripts/bx_light.py","sha256":"6f6624765f2afdfebc0efc4b77313fee3cacef9e7d983bcb3a95c88c8fa2f8f2","size":102909,"exec":false}]},"scenario-blender-retopology":{"description":"Use when turning a dense sculpt, scan or AI-generated mesh into clean topology in Blender, for anim…","category":"media-eventtech","tier":"free","origin":"scenario","requires_auth":false,"sha256":"b6f31d60d7cd7d4cd055d0e7ba160eaa58b9d165739055eb3a1c1e381c08036e","upstream_path":"skills/dcc/blender/scenario-blender-retopology/SKILL.md","requires":["scenario-blender-expert"],"software":["Blender"],"hasScripts":true,"scriptCount":1,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"b6f31d60d7cd7d4cd055d0e7ba160eaa58b9d165739055eb3a1c1e381c08036e","size":22026,"exec":false},{"path":"references/critique.md","sha256":"f23b9afc01bc0489d834978277323e1612f576983be98c388ba54e0c0e5ba7d7","size":15592,"exec":false},{"path":"references/expert-notes.md","sha256":"b1faff4c7fdce09eadbe438f4c8d52bceb05930729dd54ad5e6cae9f8406dd58","size":36275,"exec":false},{"path":"references/procedures.md","sha256":"d55d861c34134a6d1681d67577d3c81f4d5be904fdd3c87108e4a6b940790ae5","size":31490,"exec":false},{"path":"references/sources.md","sha256":"d25df1db2d06c8e378349bf188ba19d229ea7f912b46097854afce669d234017","size":17379,"exec":false},{"path":"scripts/bx_retopo.py","sha256":"871005569e63d4d947e5d5a6cd6344f6bee4113302ec5498270b028ad53922dd","size":128751,"exec":false}]},"scenario-blender-texturing-shading":{"description":"Use when texturing, shading or look-developing in Blender: building a PBR material from a texture s…","category":"media-eventtech","tier":"free","origin":"scenario","requires_auth":false,"sha256":"df7f11cc812e5b2dd61bd5770de88017fdb67898951e2957bbdc78717f40c45c","upstream_path":"skills/dcc/blender/scenario-blender-texturing-shading/SKILL.md","requires":["scenario-blender-expert"],"software":["Blender"],"hasScripts":true,"scriptCount":1,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"df7f11cc812e5b2dd61bd5770de88017fdb67898951e2957bbdc78717f40c45c","size":15695,"exec":false},{"path":"references/critique.md","sha256":"8365bb221a2ad681120beb2346ec6ee194a0affca441d98243f35e66540a8d50","size":5828,"exec":false},{"path":"references/expert-notes.md","sha256":"0472e6a6faa26770e603f97a25bd1c34007b9c0220c98967973d937d7a4dbb20","size":28024,"exec":false},{"path":"references/procedures.md","sha256":"703f71d73daab57183ee472b6069e3368b6f79ce72e0a273f8a67ed29a5a61f4","size":31419,"exec":false},{"path":"references/sources.md","sha256":"093bd302b898e3bc3fc438cf0ed0be59cbc6057108c03091ea828d25f31e8503","size":11474,"exec":false},{"path":"scripts/bx_materials.py","sha256":"15877d81bea8d2c95e1393b7c6ce8c96182fdb1a8d26dfa6bfbaf2d88886e72b","size":87244,"exec":false}]},"scenario-blender-uv-baking":{"description":"Use when UV unwrapping, placing seams, straightening or packing UVs, setting texel density, laying …","category":"media-eventtech","tier":"free","origin":"scenario","requires_auth":false,"sha256":"4dba740b856bb7cda4b158c53f1972221d08059ef89bfaaca15a5ac985db9b90","upstream_path":"skills/dcc/blender/scenario-blender-uv-baking/SKILL.md","requires":["scenario-blender-expert"],"software":["Blender"],"hasScripts":true,"scriptCount":1,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"4dba740b856bb7cda4b158c53f1972221d08059ef89bfaaca15a5ac985db9b90","size":17085,"exec":false},{"path":"references/critique.md","sha256":"e9a0bde9c4b2b416cd3d73ceb4dee21f584175fdd7b855a3fa3e04ac434c7a7b","size":17474,"exec":false},{"path":"references/expert-notes.md","sha256":"04fba3c9f09eb7643a5d27329c6eed250da06bb55df9b2374e760a8e0689e52c","size":23242,"exec":false},{"path":"references/procedures.md","sha256":"c8c2f81b18fb66360c2a6ac71c918a2494318b775648e0af2d725d5636df4a94","size":17979,"exec":false},{"path":"references/sources.md","sha256":"fb8a8a3b4d69ab7119e7db3f2f60756bedbe275912e8acbd5e9339cbfef9bb95","size":12550,"exec":false},{"path":"scripts/bx_uvbake.py","sha256":"f07fd3b6c02fdeff9dfaa77e164f24843ece1113e23ba073e61b68002d5c0762","size":107567,"exec":false}]},"scenario-unity-expert":{"description":"Use when an agent drives Unity 6.3 (6000.3) on a Mac for any task: batch mode and -executeMethod jo…","category":"media-eventtech","tier":"free","origin":"scenario","requires_auth":false,"sha256":"61a31dd57792d42514912d864f23aa4ac7511b2c288d78e491987f6747142357","upstream_path":"skills/game-engines/unity/scenario-unity-expert/SKILL.md","software":["Unity 6.3"],"hasScripts":true,"scriptCount":12,"execBridge":true,"execBridgeFiles":[{"path":"scripts/AgentKit/AgentBridge.cs","reason":"invokes a method named in a request + file inbox polled for requests"},{"path":"scripts/ut_live.py","reason":"calls exec()/eval() + file inbox polled for requests"}],"files":[{"path":"SKILL.md","sha256":"61a31dd57792d42514912d864f23aa4ac7511b2c288d78e491987f6747142357","size":20832,"exec":false},{"path":"references/gui-paths.md","sha256":"8571540332e3095ff1855b641ac847dc9a4bf14f64011883f77d1421c6c5f041","size":5161,"exec":false},{"path":"references/live-channels.md","sha256":"37c9bf7a84ee3f81cb003977de6ae7b819bddbc341babf842558ac4a33f10d81","size":10861,"exec":false},{"path":"references/procedures.md","sha256":"d0e79adbed05d073ca9d25f088155f677735210ebe99f27d10670007afed42ad","size":7975,"exec":false},{"path":"references/routing-handoffs.md","sha256":"99fc636babe797bd83f580d11d58cfd7db551528eb946251d09cd97067eb12b8","size":19866,"exec":false},{"path":"references/sources.md","sha256":"49d0bf4a5d0470f6c2d5d5f436cab43789b1f17e0b7b361bed277c397bc218cd","size":28778,"exec":false},{"path":"references/toolkit-map.md","sha256":"db1f8b6d2e1dd685d44398266c8f956d36b862cef867bb8b8cf7e3895a5ab7e3","size":38967,"exec":false},{"path":"references/unity-6.3-traps.md","sha256":"f2c308fdfde091b022077958cd93dac186441cb8fd5dc2ae049697533b4191f3","size":51439,"exec":false},{"path":"references/writing-jobs.md","sha256":"a0f58f7ba6ef31c460c437484305f88b57ee6621a5f5851ecfbc636cc54d8a7e","size":9767,"exec":false},{"path":"scripts/AgentKit/AgentAudit.cs","sha256":"ef3462273115de3e5dfe0728a1eb884a01bfca1368846f563ba3e82acc25c1a4","size":26345,"exec":false},{"path":"scripts/AgentKit/AgentBridge.cs","sha256":"1cd83d4ed4b03d4e4440eab88ca4ca777e6057478d93db2b98fb35f3cd5a0b10","size":12418,"exec":false},{"path":"scripts/AgentKit/AgentBuild.cs","sha256":"9d97386d404e30ba3b286e202e95645b39bcc7b449b791298ce181e9ec7dfa66","size":12631,"exec":false},{"path":"scripts/AgentKit/AgentCapture.cs","sha256":"ebbf7f867607a9f563760161fba53ab0336e91858bc741fb01ffdb183861390a","size":22677,"exec":false},{"path":"scripts/AgentKit/AgentJob.cs","sha256":"a7e56ca70de1a3714509f128d112ea2afaf7817ddfea5c8fd65e9cfd25498328","size":16347,"exec":false},{"path":"scripts/AgentKit/AgentJson.cs","sha256":"9ef0f999966536846a358d647126d863d38140dd5dcf1f2ab51719dc0f4ea6c0","size":12599,"exec":false},{"path":"scripts/AgentKit/AgentProfile.cs","sha256":"61047d48198e42ebd9b9d43d1fa51feab16475232b3ca02837d86850997d3d43","size":13210,"exec":false},{"path":"scripts/ut_env.py","sha256":"0945a5e3d32f0610f73a50a5c186b18cb63301df736ac25be0c8da27f83bc1ec","size":16048,"exec":false},{"path":"scripts/ut_live.py","sha256":"ad4b62c2f4f2551e5edd84a6ac70cb96b5a0956629d1c29034b20b2d92a177c7","size":21945,"exec":false},{"path":"scripts/ut_review.py","sha256":"b698fda1327c7073e1f82593f3f4050278ebd072631342f3a0bc709e0845bb44","size":24732,"exec":false},{"path":"scripts/ut_run.py","sha256":"64e0c0050faaf784c78e5bb3f575b1177d2d55ecc8c30e6dd2f14ae02b92a279","size":26972,"exec":false},{"path":"scripts/ut_stat.py","sha256":"367879ffa58641f67e7e44f190f01bac4b2864128f3ab567df028ac38f18ec9e","size":11680,"exec":false}]},"scenario-unity-performance":{"description":"Use when a Unity 6.3 game is slow or stutters: low fps, frame spikes, hitches when entering areas o…","category":"engineering-method","tier":"free","origin":"scenario","requires_auth":false,"sha256":"c12a5c903f396a72a15e56226d8a0dab553fef6caa13d9c801c54e8125c22333","upstream_path":"skills/game-engines/unity/scenario-unity-performance/SKILL.md","requires":["scenario-unity-expert"],"software":["Unity 6.3"],"hasScripts":true,"scriptCount":11,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"c12a5c903f396a72a15e56226d8a0dab553fef6caa13d9c801c54e8125c22333","size":19744,"exec":false},{"path":"references/critique.md","sha256":"2731a46adbeec0f9c3b4a77c4b9c3f48ea8b942a1c1541f83f6f0fe620b5aa2d","size":8642,"exec":false},{"path":"references/expert-notes.md","sha256":"9980ee29a301309ab626a67fedefe84948ca03bebb033326f9623b7db8918169","size":25410,"exec":false},{"path":"references/gui-paths.md","sha256":"da2d3546fec852aafec6cb353d021c14ed24ea0bd8a21a3545f0f9563caa84da","size":24245,"exec":false},{"path":"references/procedures.md","sha256":"d28316c0c66d52e921c8e0b6a9c2cd5c9e892763be5b35e10a13999c2a6f258e","size":44495,"exec":false},{"path":"references/sources.md","sha256":"ce76807bc6ca64274be03f998726197261f1946604918a7e3b3be08a9c3f3c4c","size":24539,"exec":false},{"path":"scripts/AgentKit/Performance/PerfAudit.cs","sha256":"1f04553de72dbcfcee20ae496d5e80f256b4376dc7a849558c4586b6101e165b","size":17453,"exec":false},{"path":"scripts/AgentKit/Performance/PerfBuild.cs","sha256":"a89fa1b4532123a5816f97394ecd35ce12a5e45072764dc1abebdcf267f50d0e","size":11692,"exec":false},{"path":"scripts/AgentKit/Performance/PerfCapture.cs","sha256":"0a1788e09d50c2fe1d97fe749bf884b0e887f5ecc7514b86823e1c508d51c743","size":12297,"exec":false},{"path":"scripts/AgentKit/Performance/PerfFrameDebug.cs","sha256":"19e69f94b6aced6358a15edd8c8d78ec992677b25372b55b4fa09383469fd879","size":16446,"exec":false},{"path":"scripts/AgentKit/Performance/PerfLod.cs","sha256":"cc9bf20740a9d5bbaaa7410c75a0003660f0d556737012a067bb02e8dcf4bd70","size":5062,"exec":false},{"path":"scripts/AgentKit/Performance/PerfRendering.cs","sha256":"af7ba69beeb6d74d17de949a81e13ab2112d6d45a47156c3653f4ecae15c1b1b","size":33683,"exec":false},{"path":"scripts/AgentKit/Performance/PerfShaders.cs","sha256":"6aa5cae1262e930c9a4fa07e18555f9b95dcce3391c8da33f82ae266f5cef9cd","size":13473,"exec":false},{"path":"scripts/Runtime/Performance/AgentKit.Performance.Runtime.asmdef","sha256":"eb71145c132486c46bfecc868a63b51c196a4437043c422d61e01de40e30d184","size":382,"exec":false},{"path":"scripts/Runtime/Performance/AllocScope.cs","sha256":"f469ee9f659dbda3d387817854999d141b1fd56889b8a3209f0f4fc63159da8e","size":2206,"exec":false},{"path":"scripts/Runtime/Performance/PerfProbe.cs","sha256":"2a42c3dc9a416d8655bb9f43335ff81535133d25ae04f206ad3ff24a4680c80f","size":28166,"exec":false},{"path":"scripts/ut_perf.py","sha256":"46cb7a5b9922546a6510eef9de4779017989681b1715f3fa3328d1502dd53fb7","size":44913,"exec":false}]},"scenario-unity-pipeline-automation":{"description":"Use when automating a Unity 6.3 content pipeline or build: importing an art drop (FBX, PNG) with As…","category":"engineering-method","tier":"free","origin":"scenario","requires_auth":false,"sha256":"438d60621e5c93a35d0f6085af22e9c1377ed154b24486c134d79cc9d584948c","upstream_path":"skills/game-engines/unity/scenario-unity-pipeline-automation/SKILL.md","requires":["scenario-unity-expert"],"software":["Unity 6.3"],"hasScripts":true,"scriptCount":16,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"438d60621e5c93a35d0f6085af22e9c1377ed154b24486c134d79cc9d584948c","size":19929,"exec":false},{"path":"references/critique.md","sha256":"4f35497615c24efa9c9265557f5794c5c343a20c99fd64e903c588bfa581e684","size":17680,"exec":false},{"path":"references/expert-notes.md","sha256":"134238c2280662d3e71153ea12c6e127713677f9a3b06441ce79250044168bbd","size":24228,"exec":false},{"path":"references/gui-paths.md","sha256":"441f6f46698e99cef420db6de67cf2cbf040d62e149528ad29174392b66c5038","size":15507,"exec":false},{"path":"references/procedures.md","sha256":"04c16e0d2b28fb1b619537f3f491a403d38c6304ea4b7b8befe7ab38575f2431","size":50486,"exec":false},{"path":"references/sources.md","sha256":"9db649871d50cec70bc7909ab0a4fd68bf902745cb128173c3994aa8b8c5a988","size":17352,"exec":false},{"path":"scripts/AgentKit/Pipeline/AddressablesJobs.cs","sha256":"862a2fc4c651be751cbfbf46a0e51b14eff7b7530f8691c72a8f0aedc2cf80dd","size":24414,"exec":false},{"path":"scripts/AgentKit/Pipeline/ArtDropJobs.cs","sha256":"38f5d8d349aa8362b453ae08e179dbaf6778fa2692ddf505601bea56fcd88f2b","size":37459,"exec":false},{"path":"scripts/AgentKit/Pipeline/EditorEditJobs.cs","sha256":"505a5816a64a92fb00536db5bfec238085fb1a95df0c960667260386c9627269","size":16776,"exec":false},{"path":"scripts/AgentKit/Pipeline/PipelineBuild.cs","sha256":"6774278798c91440489cce698138013cb1bf1a821e8b80a02c408656c1702729","size":17557,"exec":false},{"path":"scripts/AgentKit/Pipeline/PipelineChecks.cs","sha256":"558cb4716bd61abb73f6a8ef1ca0ebdb9f15df10be9c669b3f4102d7b1e76f05","size":13428,"exec":false},{"path":"scripts/AgentKit/Pipeline/PipelineRules.cs","sha256":"bec6dd9704f7668e9fed9cec3cc975bdfaebacbec4298b7c8a38056c36a263f4","size":7302,"exec":false},{"path":"scripts/AgentKit/Pipeline/PropImportPostprocessor.cs","sha256":"3cd1dee4c68e92e68e41757b932fa4c4e77f38f316f8d72fb9acfb275e06262e","size":6656,"exec":false},{"path":"scripts/AgentKit/Pipeline/Tests/PipelineContentTests.cs","sha256":"570276dda02f9ea24e887b5e6019ef9634cde5319974bfb2ce9a26e451be7227","size":7083,"exec":false},{"path":"scripts/AgentKit/PipelineCli/AgentKit.PipelineCli.asmdef","sha256":"73e86ed42f8560e39944febced4bc45c4b6087dc452495fe94d017724e7069e2","size":612,"exec":false},{"path":"scripts/AgentKit/PipelineCli/PipelineCliCommands.cs","sha256":"a0143e932002e3f6b54ef9b4bc334415c39823ae794751fc7db2d7dd622203f4","size":4296,"exec":false},{"path":"scripts/Runtime/AgentAddressablesSmoke.cs","sha256":"9dd0d8cb0b95ec65bed284d026eb1abc71ea2e2d62bdaa029f96b6198013ed6d","size":4174,"exec":false},{"path":"scripts/templates/PipelineAsmdef/Editor/Game.Pipeline.Editor.asmdef","sha256":"cf802502971da70b9a6b9c6f18d0d32728d3f5ff9c6c2b948ccdf38ddb918ed3","size":389,"exec":false},{"path":"scripts/templates/PipelineAsmdef/Editor/TextureRuleAudit.cs","sha256":"7c4256ca74aa2be2a1e453c40653807b895b00b7eade2ae601897e3fb596c75b","size":2327,"exec":false},{"path":"scripts/templates/PipelineAsmdef/Tests/Game.Pipeline.Tests.asmdef","sha256":"5289a53406fef4ee53d882bbaa825cc52a063514c1eb8b3e759ef2dc637eab03","size":568,"exec":false},{"path":"scripts/templates/PipelineAsmdef/Tests/TextureRuleTests.cs","sha256":"9ab47e781c293bd22a74ac44ca95b55164b6a1b4f20eb9e446237a15aa8a0e0a","size":1280,"exec":false},{"path":"scripts/ut_pipeline.py","sha256":"8b93ba575f6859d7dc05ba0c664eb8a210ccee22eec123835a59b853d6e5a05d","size":56155,"exec":false}]},"scenario-unity-shaders":{"description":"Use when a Unity 6.3 URP task involves shaders: hand-written HLSL, Shader Graph water, toon, dissol…","category":"media-eventtech","tier":"free","origin":"scenario","requires_auth":false,"sha256":"bf0cf46c1e6bb2d0ffe3852fc363a2ad3d1ded020cf4b7b41ffd7cae7e653de3","upstream_path":"skills/game-engines/unity/scenario-unity-shaders/SKILL.md","requires":["scenario-unity-expert"],"software":["Unity 6.3"],"hasScripts":true,"scriptCount":21,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"bf0cf46c1e6bb2d0ffe3852fc363a2ad3d1ded020cf4b7b41ffd7cae7e653de3","size":20513,"exec":false},{"path":"references/critique.md","sha256":"e8934e753805665a4bb8fbdea78db41268040eeb7695aba5ca790227137fd438","size":12242,"exec":false},{"path":"references/expert-notes.md","sha256":"23ba5da22cdd325b46c91df8b34c3b8a7b14fa28a0374ffb6beb888dea924856","size":25730,"exec":false},{"path":"references/gui-paths.md","sha256":"e00f3a24db197575541a9ae5287c5ac4350487fc517460d19e9c1316cd4ad315","size":18163,"exec":false},{"path":"references/procedures.md","sha256":"e4d6c4b92c3841d0a270533cec5c525bf258ddae8ac0fe8997e356da3630b1fa","size":44695,"exec":false},{"path":"references/sources.md","sha256":"6f498801b0b43200f1d6357f9077001c9fb78267ae0317086a903d27bc6d41f0","size":22805,"exec":false},{"path":"scripts/AgentKit/Shaders/ComputeJobs.cs","sha256":"e22096eb5b25adea4e7b685b4a671189c0a77ed7ecde3b0aa181a05269b2e333","size":6984,"exec":false},{"path":"scripts/AgentKit/Shaders/GpuCostJobs.cs","sha256":"48a53c13e2cabde1c7a13b66cde69baf67c2beeab15204a642cc72dc5edf5d41","size":9740,"exec":false},{"path":"scripts/AgentKit/Shaders/PsoJobs.cs","sha256":"e9e5a28f8f7df32005fbfb8943f328de127c764f004d06ef6fcc575f617056ab","size":9830,"exec":false},{"path":"scripts/AgentKit/Shaders/RendererFeatureJobs.cs","sha256":"db119383b251fced87b28ecf2d832578a576eb0beb8a54b23f5cb371f8385a3c","size":10833,"exec":false},{"path":"scripts/AgentKit/Shaders/ShaderGraphJobs.cs","sha256":"ce896c4f3d082f4f44221af6dfbd1ef0602f128e92a66af0bd626c0aa72808f3","size":9291,"exec":false},{"path":"scripts/AgentKit/Shaders/ShaderJobs.cs","sha256":"844d0757ddbe40212840ff7169d9a2a6a98d3e9ebcfbaad92acea85bf5b4ce98","size":27032,"exec":false},{"path":"scripts/AgentKit/Shaders/ShaderLab.cs","sha256":"87dccaefc8a312f3d0debdcacdcca2510fb415a4c70af987255bf67b6c67fb10","size":25080,"exec":false},{"path":"scripts/AgentKit/Shaders/VariantJobs.cs","sha256":"1b83dfa175aeeb8a12647d5a52221b3222f5b87a7d4fdcab1163bfc0e7b68ad4","size":24544,"exec":false},{"path":"scripts/Runtime/AgentFullscreenFeature.cs","sha256":"a838d52edd32af2fc3cb281d5ea89c957b453f9d7ad13d20213411e31c6f1e87","size":6243,"exec":false},{"path":"scripts/Runtime/AgentPsoProbe.cs","sha256":"d6da1632573854e9327e3a5b2b7d757a514f14e85ed5f7817038df1f73049f36","size":12803,"exec":false},{"path":"scripts/Shaders/AgentCompute.compute","sha256":"e63376793bdbfbd062e828e5c7e9e62b31fc2e3be62697df356a09466b548c80","size":1874,"exec":false},{"path":"scripts/Shaders/AgentCustomLighting.hlsl","sha256":"14092128d768901fadefc3e39d6c077a68c0438faae09a368ddee4fd77dfeb19","size":6105,"exec":false},{"path":"scripts/Shaders/AgentDissolve.shader","sha256":"c26c41c19b486df876c35670c95d7dc8a5417124b0df0b5ded34ab3373455e84","size":9467,"exec":false},{"path":"scripts/Shaders/AgentEdgeOutline.shader","sha256":"eb06e6c478344ed4a75acd2dbaee0f24e821fe6ad52241ad40b9958d4604d08e","size":4025,"exec":false},{"path":"scripts/Shaders/AgentGrayscale.shader","sha256":"3f986574b9af4b8013e0c7d361e410b8da00f327715e9dccbfc00c63fc28404b","size":1493,"exec":false},{"path":"scripts/Shaders/AgentLit.shader","sha256":"519555c889f33d8b53843aa0d3ab1396566d1428275f5f8464c2be45827dfe34","size":9152,"exec":false},{"path":"scripts/Shaders/AgentNoise.hlsl","sha256":"9d70aac4e3aacbf828fc6aaabd64bfa351446e87572ff3e5d618cfe038e4ce43","size":3208,"exec":false},{"path":"scripts/Shaders/AgentPasses.hlsl","sha256":"80b0ce1f1da27983f514228ad6639b5f52591e4bee9083491b2aa1bb551d37d8","size":5452,"exec":false},{"path":"scripts/Shaders/AgentToon.shader","sha256":"8f49cc1da741f457b6da3e0ac9ec564b4d89149d90cee0bff4df3007c05baa15","size":9828,"exec":false},{"path":"scripts/Shaders/AgentWater.shader","sha256":"b865a7bb72a28d1767d88b00e1c85e63676df0778c478b09d3d2e27a348e608a","size":9437,"exec":false},{"path":"scripts/ut_shaders.py","sha256":"71fcd7c4311b784d0fbc70f0d3f4a62f853455715f9dbe0d7952c7d4ec17f2cd","size":56132,"exec":false}]},"scenario-unity-ui":{"description":"Use when building or fixing game UI in Unity 6.3: main menu, settings screen (resolution, quality, …","category":"design-ui-ux","tier":"free","origin":"scenario","requires_auth":false,"sha256":"9cf61b9ca815b3ed1886d5bcec98ebfc22571f948803c5fc06bf9f584333f755","upstream_path":"skills/game-engines/unity/scenario-unity-ui/SKILL.md","requires":["scenario-unity-expert"],"software":["Unity 6.3"],"hasScripts":true,"scriptCount":32,"execBridge":false,"execBridgeFiles":[],"files":[{"path":"SKILL.md","sha256":"9cf61b9ca815b3ed1886d5bcec98ebfc22571f948803c5fc06bf9f584333f755","size":17307,"exec":false},{"path":"references/critique.md","sha256":"1467315c43abf62aad6d2174df54d93e9c4bdb232bf1834296c8f3c3766bb78a","size":8630,"exec":false},{"path":"references/expert-notes.md","sha256":"e0c128bf8645ee5fe221664cd5c781dabfb14ea6248f43d77d0733d378e7af8b","size":23781,"exec":false},{"path":"references/gui-paths.md","sha256":"66807ad5265940a31547daf5a8537cbc62141501065decd7b465e6dd4dbc1026","size":5200,"exec":false},{"path":"references/procedures.md","sha256":"29d4faa6e8f2ad331d9ce4aef85303e8b8d94f56f36df0d8274a2a4fdc0b438e","size":42170,"exec":false},{"path":"references/sources.md","sha256":"1c776696c9d54cf2b4b377ed8e61d2e92f9da8d4bc4a265ac4759ef71c7a28cb","size":16748,"exec":false},{"path":"scripts/AgentKit/UI/UIAudit.cs","sha256":"50714d1cacced7ed3b9744cfee771589ebc693727b275b8d55a0c98ed3795d45","size":39487,"exec":false},{"path":"scripts/AgentKit/UI/UIBuild.cs","sha256":"ac023db602fd898f174a5d2192aba29a629f825c0d5d48f9fbaeb1b2e21ab335","size":29856,"exec":false},{"path":"scripts/AgentKit/UI/UICapture.cs","sha256":"0c1ce99993d28f71c90f68897b7c6ca3ba8fadc85d20b399c48db2c7f2dfb964","size":33102,"exec":false},{"path":"scripts/AgentKit/UI/UIPerf.cs","sha256":"8985971fd81578a6fd3f9919de02108da269965dd6cd96065cbdff64a84368d4","size":26392,"exec":false},{"path":"scripts/AgentKit/UI/UISetup.cs","sha256":"07f23ef2a7e16eb5a1ab1833ed2972c7cdbc69e120309ae637f925e1f87cb2dc","size":5897,"exec":false},{"path":"scripts/AgentKit/UI/UIToolkitBuild.cs","sha256":"75049ce3ec93d2657d98046708afc5bf630fe2ac07f712ca6b584f5f1206d07d","size":20382,"exec":false},{"path":"scripts/Runtime/UI/AgentUI.Runtime.asmdef","sha256":"3c9fb95fe5977b71ae42137d01ba7d24cbfe674b2a0d5c5641b1da5038ccf4e3","size":444,"exec":false},{"path":"scripts/Runtime/UI/CanvasDeviceScale.cs","sha256":"43958d24558a260e2aff19a68a4d6dae2a4ee477dbda7ba050b8a1213fb0fa73","size":2069,"exec":false},{"path":"scripts/Runtime/UI/ClampedWidth.cs","sha256":"54f78309df4637a53c76250542a93189cdf0f9bfa06a15e24449a8857fe740ae","size":1994,"exec":false},{"path":"scripts/Runtime/UI/HudBar.cs","sha256":"14488466f574030b05a3eb35eeaedf9ae66b52d3d77878cbf877e802b5322abf","size":2906,"exec":false},{"path":"scripts/Runtime/UI/HudModel.cs","sha256":"4f44c529b07811e3486ec24816ed24203db203eb75442f76a5cc84cdc79f807b","size":3623,"exec":false},{"path":"scripts/Runtime/UI/HudView.cs","sha256":"712e93b3b1f6490007c4672dd31eb0bb44843af7e53bec9b8af55fac3ae9bbae","size":1481,"exec":false},{"path":"scripts/Runtime/UI/MenuRouter.cs","sha256":"3da348c7952fbf9c9e6f078a1eff44571b7e6dfe19697930b979ecd00ff3fdaa","size":4163,"exec":false},{"path":"scripts/Runtime/UI/MenuScreen.cs","sha256":"7b26db2aa577b13f19fd9a69c4d7112756e14c83d097258d65d84a56f3b6324d","size":4843,"exec":false},{"path":"scripts/Runtime/UI/PanelChangeLog.cs","sha256":"18ff907c0d203f42a47910a288337196d547176934600274fafbc49e123010fd","size":3676,"exec":false},{"path":"scripts/Runtime/UI/RebindRow.cs","sha256":"5650a5dcf31f703125916cf1af73b893ef9533286f91a0cd7e896c458b27ce5a","size":3454,"exec":false},{"path":"scripts/Runtime/UI/RebuildProbe.cs","sha256":"52c42816a5cf95d0b0cdb195ebc003db1dc8a75397e9a77b862bf51c4718283f","size":1212,"exec":false},{"path":"scripts/Runtime/UI/ScrollToSelection.cs","sha256":"d9401696c2b0cc5ee1659fef865801f358e0e226715b42a83ae25a9ad7944fbc","size":2264,"exec":false},{"path":"scripts/Runtime/UI/SelectableFeedback.cs","sha256":"ecf8bbab358a0a867208fb92e1e4a790c4cb993b992192d151f52f1ef0d80bad","size":3642,"exec":false},{"path":"scripts/Runtime/UI/SettingsScreen.cs","sha256":"7b386b9e7de1d4425fd4fdaaf9703d1a1499fbb1023baaee05456b23a7fa62e1","size":6628,"exec":false},{"path":"scripts/Runtime/UI/SettingsService.cs","sha256":"d19662fc7ed48b89b22af678db1c64c2ea3558331a5a3d44aad5fb550c7ef246","size":4291,"exec":false},{"path":"scripts/Runtime/UI/UILoader.cs","sha256":"ae3b8087cbe41f65d9c6aab54a2fbb41caa24fd7f8f4b504996945e6e8065f76","size":2504,"exec":false},{"path":"scripts/Runtime/UI/UiCounterSampler.cs","sha256":"34cebfc40d47925f8d378322b978bb84d54a61fc555699bafd77c1fbe649e824","size":4149,"exec":false},{"path":"scripts/Runtime/UI/UiPerfDriver.cs","sha256":"6ce266e1ca0f9e33fcbd4a45fadd0a29f996fe00ceca23cc4ef16591fe7d8288","size":927,"exec":false},{"path":"scripts/Runtime/UI/UiTheme.cs","sha256":"27a513e460d683f6c51f8cd16445aa53d4f7d3b11221d069d0717a13e98f265e","size":2572,"exec":false},{"path":"scripts/Runtime/UI/UitkHideDriver.cs","sha256":"ffe011cbf1b2d148a14b6ebe829269750de922594e52529fc63eae648829ff3b","size":6414,"exec":false},{"path":"scripts/Runtime/UI/UitkTextureGrid.cs","sha256":"8ae73058d4a3e59138d58849e9d2803559079ce0ef4cfb5c95396e980111e2ec","size":3052,"exec":false},{"path":"scripts/Runtime/UI/UitkVertexGrid.cs","sha256":"d33f5fe3c25e51f0e02f858981113fc67c5c63f4ca87f6f490a4f0dc43e8c3bf","size":2121,"exec":false},{"path":"scripts/templates/uitk/AgentRuntimeTheme.tss","sha256":"682e2b1c6cc1d6e332c15f56c840f7d8282cb1e5bc627eb6664580352a752fda","size":38,"exec":false},{"path":"scripts/templates/uitk/Hud.uss","sha256":"774b424ea9f9624b07970ef7c40f70fdec7835290e0d8bebdabb325db8e8d5cb","size":2445,"exec":false},{"path":"scripts/templates/uitk/Hud.uxml","sha256":"d379afb21468f1c516c4c4fe349f96b6d8d46d0e03406e1ac717a460d69b36f3","size":2643,"exec":false},{"path":"scripts/ut_ui.py","sha256":"0b2aaa743a160d4c8c31cb4e16513f9e4503de6a3129fdd4eaa4b13338ec5c46","size":24204,"exec":false}]}},"subagents":{}}
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { basename, dirname, join, resolve, sep } from 'node:path';
5
5
 
@@ -66,10 +66,10 @@ function assertNoSymlink(path) {
66
66
  catch (e) { if (e.code !== 'ENOENT') throw e; }
67
67
  }
68
68
 
69
- // Checks every destination first, then writes; throws before any write on an unsafe path.
70
- export function writeSkillFiles(skillMd, files) {
69
+ // Checks every destination; throws on an unsafe path and touches nothing.
70
+ export function planSkillFiles(skillMd, files) {
71
71
  const dir = resolve(dirname(skillMd));
72
- const plan = files.map((f) => {
72
+ return files.map((f) => {
73
73
  safeRel(f.path);
74
74
  const dest = resolve(dir, ...f.path.split('/'));
75
75
  if (!dest.startsWith(dir + sep)) throw new Error(`Path escapes skill dir: ${f.path}`);
@@ -77,6 +77,9 @@ export function writeSkillFiles(skillMd, files) {
77
77
  assertNoSymlink(dir);
78
78
  return { ...f, dest };
79
79
  });
80
+ }
81
+
82
+ export function writePlan(plan) {
80
83
  for (const f of plan) {
81
84
  mkdirSync(dirname(f.dest), { recursive: true });
82
85
  writeFileSync(f.dest, f.data);
@@ -85,6 +88,80 @@ export function writeSkillFiles(skillMd, files) {
85
88
  return plan.map((f) => f.dest);
86
89
  }
87
90
 
91
+ export const writeSkillFiles = (skillMd, files) => writePlan(planSkillFiles(skillMd, files));
92
+
93
+ const RAW_BASE_RE = /^https:\/\/raw\.githubusercontent\.com\/[\w.-]+\/[\w.-]+$/;
94
+ const ECC_SOURCE = {
95
+ id: 'ecc', label: 'ECC', raw_base: 'https://raw.githubusercontent.com/affaan-m/ECC',
96
+ license: 'MIT', copyright: 'Copyright (c) 2026 Affaan Mustafa', third_party: true,
97
+ };
98
+
99
+ // Test overrides: AOS_STORE_INDEX / AOS_STORE_RAW_BASE (ECC), AOS_STORE_INDEX_SCENARIO / AOS_STORE_RAW_BASE_SCENARIO (Scenario).
100
+ export function loadSources(root, env = process.env) {
101
+ const specs = [
102
+ { file: env.AOS_STORE_INDEX || join(root, 'lib', 'ecc-store-index.json'), base: env.AOS_STORE_RAW_BASE, fallback: ECC_SOURCE },
103
+ { file: env.AOS_STORE_INDEX_SCENARIO || join(root, 'lib', 'scenario-store-index.json'), base: env.AOS_STORE_RAW_BASE_SCENARIO },
104
+ ];
105
+ return specs.map(({ file, base, fallback }) => {
106
+ const index = JSON.parse(readFileSync(file, 'utf8'));
107
+ if (!index.pinned_commit || !index.skills || !index.subagents) throw new Error(`Invalid AOS store index: ${file}`);
108
+ const meta = { ...fallback, ...index.source };
109
+ if (!meta.id || !meta.label) throw new Error(`Store index has no source descriptor: ${file}`);
110
+ if (!base && !RAW_BASE_RE.test(meta.raw_base || '')) throw new Error(`Untrusted raw_base in ${file}: ${meta.raw_base}`);
111
+ return { ...meta, raw_base: (base || meta.raw_base).replace(/\/+$/, ''), pinned_commit: index.pinned_commit, index };
112
+ });
113
+ }
114
+
115
+ export function findItems(sources, name) {
116
+ const found = [];
117
+ for (const src of sources) {
118
+ for (const type of ['skills', 'subagents']) {
119
+ if (src.index[type]?.[name]) found.push({ name, item: src.index[type][name], type, src });
120
+ }
121
+ }
122
+ return found;
123
+ }
124
+
125
+ export function findItem(sources, name) {
126
+ const found = findItems(sources, name);
127
+ if (found.length > 1) {
128
+ throw new Error(`Name collision: '${name}' exists in ${found.map((f) => f.src.label).join(' and ')}; refusing to pick one or overwrite`);
129
+ }
130
+ return found[0] || null;
131
+ }
132
+
133
+ // The item plus the skills it requires (same source), dependencies first.
134
+ export function installPlan(sources, found) {
135
+ const plan = [];
136
+ const visit = (entry, chain) => {
137
+ if (chain.includes(entry.name)) throw new Error(`Circular requires: ${[...chain, entry.name].join(' -> ')}`);
138
+ for (const dep of entry.item.requires || []) {
139
+ const hit = findItem(sources, dep);
140
+ if (!hit || hit.src !== entry.src || hit.type !== 'skills') throw new Error(`${entry.name} requires '${dep}', which is not available in ${entry.src.label}`);
141
+ visit(hit, [...chain, entry.name]);
142
+ }
143
+ if (!plan.some((p) => p.name === entry.name)) plan.push(entry);
144
+ };
145
+ visit(found, []);
146
+ return plan;
147
+ }
148
+
149
+ export function planSummary(plan) {
150
+ const all = plan.flatMap((p) => (p.item.files || []).map((f) => ({ ...f, skill: p.name })));
151
+ const bridge = plan.flatMap((p) => (p.item.execBridgeFiles || []).map((f) => ({ skill: p.name, ...f })));
152
+ return {
153
+ skills: plan.map((p) => p.name),
154
+ requires: plan.slice(0, -1).map((p) => p.name),
155
+ fileCount: all.length || plan.length,
156
+ totalSize: all.reduce((n, f) => n + f.size, 0),
157
+ scriptCount: plan.reduce((n, p) => n + (p.item.scriptCount || 0), 0),
158
+ hasScripts: plan.some((p) => p.item.hasScripts),
159
+ execBridge: plan.some((p) => p.item.execBridge),
160
+ execBridgeFiles: bridge,
161
+ software: [...new Set(plan.flatMap((p) => p.item.software || []))],
162
+ };
163
+ }
164
+
88
165
  export function localSkills(root) {
89
166
  const found = new Map();
90
167
  const visit = (dir) => {
@@ -56,6 +56,8 @@ dialog h2{font-family:var(--serif);font-weight:500;margin:0 0 .6rem}
56
56
  dialog pre{margin:.6rem 0;padding:.7rem;border-radius:.6rem;background:var(--ink);color:var(--white);font-size:.7rem;overflow-x:auto;white-space:pre-wrap;word-break:break-all}
57
57
  dialog .acts{display:flex;gap:.5rem;justify-content:flex-end;margin-top:1rem}
58
58
  .err{color:oklch(0.5 0.2 25);font-size:.75rem}
59
+ .badge.warn{border-color:oklch(0.5 0.2 25);color:oklch(0.5 0.2 25)}
60
+ .warnbox{padding:.6rem .8rem;border:2px solid oklch(0.5 0.2 25);border-radius:.6rem;color:oklch(0.5 0.2 25);font-size:.78rem;font-weight:700}
59
61
  </style></head><body>
60
62
  <div class="container"><main id="app"></main></div>
61
63
  <dialog id="dlg"></dialog>
@@ -70,7 +72,7 @@ const visible=()=>{const q=S.q.toLowerCase();return base().filter(i=>(S.sub==='i
70
72
  const uniq=f=>[...new Set(S.data.items.map(f))].sort();
71
73
  const count=(f,v)=>base().filter(i=>f(i)===v).length;
72
74
  const chip=(a,v,label,n)=>`<button class="chip" aria-pressed="${S[a]===v}" data-a="${a}" data-v="${esc(v)}">${esc(label)}<b>${n}</b></button>`;
73
- function strip(){const all=S.data.items;const n=s=>all.filter(i=>i.source===s).length;return `<div class="strip"><div><strong>${all.length}</strong><small>total</small></div><div><strong>${n('AOS Core')}</strong><small>AOS Core</small></div><div><strong>${n('ECC')}</strong><small>ECC</small></div></div>`}
75
+ function strip(){const all=S.data.items;return `<div class="strip"><div><strong>${all.length}</strong><small>total</small></div>${uniq(i=>i.source).map(s=>`<div><strong>${all.filter(i=>i.source===s).length}</strong><small>${esc(s)}</small></div>`).join('')}</div>`}
74
76
  function tabs(){const n=base(),ic=n.filter(i=>i.installed).length,k=x=>S.data.items.filter(i=>i.kind===x).length;return `<div class="tabs">${chip('tab','skill','Skills',k('skill'))}${chip('tab','agent','Agents',k('agent'))}<span style="width:1rem"></span>${chip('sub','installed','Installed',ic)}${chip('sub','discover','Discover',n.length-ic)}</div>`}
75
77
  const scopeToggle=()=>`<div class="scope"><span class="kicker" style="margin:0">Scope</span>${chip('scope','global','Global','')}${chip('scope','project','Project','')}</div>`;
76
78
  function act(i){
@@ -81,7 +83,7 @@ function act(i){
81
83
  }
82
84
  function row(i){
83
85
  const k=key(i),open=S.expanded.has(k);
84
- const badge=i.fileCount>1?`<span class="badge">${i.fileCount} files</span>`:'';
86
+ const badge=(i.fileCount>1?`<span class="badge">${i.fileCount} files</span>`:'')+(i.execBridge?'<span class="badge warn">exec bridge</span>':'')+(i.requires.length?`<span class="badge">+${i.requires.length} required</span>`:'');
85
87
  let h=`<div class="rowwrap"><div class="row"><span class="dot ${i.installed?'g':''}"></span><span class="n" data-a="exp" data-v="${esc(k)}" style="cursor:pointer" title="Show description">${esc(i.name)}${badge}</span><span class="d" style="color:var(--muted)">${esc(i.description)}</span><span class="c cat">${esc(i.category)}</span><span class="s src">${esc(i.source)}</span><span style="text-align:right">${act(i)}</span></div>`;
86
88
  if(open){const d=i.core?i.description:S.detail[k];h+=`<div class="detail">${d===undefined?'Loading...':esc(d)}</div>`}
87
89
  return h+'</div>';
@@ -109,9 +111,16 @@ async function preview(k){
109
111
  try{
110
112
  const d=await post('/api/preview',i);
111
113
  const ok=d.ok&&d.targets.length;
112
- const tc=i.fileCount>0?Math.round(d.targets.length/i.fileCount):1;const size=i.totalSize!=null?` = ${i.fileCount} file(s) \u00d7 ${tc} target(s), ${(i.totalSize/1024).toFixed(1)} KB each`:'';
113
- const warn=i.hasHooks?'<p class="err">Includes hooks \u2014 copied only, NOT registered in your harness settings; the skill may need manual wiring.</p>':'';
114
- dlg.innerHTML=`<h2>Add ${esc(i.name)}</h2><p class="kicker">${scopeName()} scope \u2014 will write ${d.targets.length} file(s)${size}</p>${warn}<pre style="max-height:16rem;overflow:auto">${esc(ok?d.targets.join('\n'):(d.stdout+d.stderr).trim())}</pre><p class="cmdnote">Downloads ${i.fileCount>1?i.fileCount+' files':'the file'} from ECC at ${esc(S.data.pinned_commit.slice(0,10))}, each verified by SHA-256.</p><div id="res"></div><div class="acts"><button class="button secondary" data-a="close">Cancel</button>${ok?`<button class="button" data-a="confirm" data-v="${esc(k)}">Install to ${scopeName()}</button>`:''}</div>`;
114
+ const m=d.meta||{};
115
+ const fc=m.fileCount||i.fileCount,tc=fc>0?Math.round(d.targets.length/fc):1;const size=m.totalSize>0?` = ${fc} file(s) \u00d7 ${tc} target(s), ${(m.totalSize/1024).toFixed(1)} KB each`:(i.totalSize!=null?` = ${i.fileCount} file(s) \u00d7 ${tc} target(s), ${(i.totalSize/1024).toFixed(1)} KB each`:'');
116
+ const warn=(i.hasHooks?'<p class="err">Includes hooks \u2014 copied only, NOT registered in your harness settings; the skill may need manual wiring.</p>':'')
117
+ +(m.execBridge?`<p class="warnbox">Warning: contains a script that executes code received over a local socket or file channel without authentication \u2014 install only if you use this bridge.<br><span style="font-weight:400">${esc((m.execBridgeFiles||[]).map(f=>f.skill+'/'+f.path).join(', '))}</span></p>`:(m.hasScripts?`<p class="cmdnote">Contains ${m.scriptCount} script file(s) that run on your machine when the skill is used. Review them before use.</p>`:''))
118
+ +(m.software&&m.software.length?`<p class="cmdnote">Needs third-party software that is not included: ${esc(m.software.join(', '))}.</p>`:'')
119
+ +(m.requires&&m.requires.length?`<p class="cmdnote">Also installs required skill(s): ${esc(m.requires.join(', '))}.</p>`:'');
120
+ const srcLabel=m.source||i.source,pin=(m.pinned_commit||S.data.pinned_commit).slice(0,10);
121
+ const lic=m.license?` \u2014 ${esc(m.license)}${m.copyright?', '+esc(m.copyright):''}`:'';
122
+ const nfiles=m.fileCount||i.fileCount;
123
+ dlg.innerHTML=`<h2>Add ${esc(i.name)}</h2><p class="kicker">${scopeName()} scope \u2014 will write ${d.targets.length} file(s)${size}</p><p class="cmdnote">Source: ${esc(srcLabel)}${lic}${m.thirdParty?' (third-party content)':''}</p>${warn}<pre style="max-height:16rem;overflow:auto">${esc(ok?d.targets.join('\n'):(d.stdout+d.stderr).trim())}</pre><p class="cmdnote">Downloads ${nfiles>1?nfiles+' files':'the file'} from ${esc(srcLabel)} at ${esc(pin)}, each verified by SHA-256.</p><div id="res"></div><div class="acts"><button class="button secondary" data-a="close">Cancel</button>${ok?`<button class="button" data-a="confirm" data-v="${esc(k)}">Install to ${scopeName()}</button>`:''}</div>`;
115
124
  dlg.showModal();
116
125
  }catch(e){alert('Preview failed: '+e.message)}
117
126
  finally{S.busy=null;render()}
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import { randomBytes, timingSafeEqual } from 'node:crypto';
6
6
  import { fileURLToPath } from 'node:url';
7
- import { isInstalled, localAgents, localSkills } from '../store-shared.mjs';
7
+ import { findItem, installPlan, isInstalled, loadSources, localAgents, localSkills, planSummary } from '../store-shared.mjs';
8
8
 
9
9
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
10
10
  const CLI = join(ROOT, 'bin', 'aos-store.mjs');
@@ -18,7 +18,7 @@ const frontDescription = (file) => {
18
18
  };
19
19
 
20
20
  function catalog(scope, cwd) {
21
- const idx = readJson('lib', 'ecc-store-index.json');
21
+ const sources = loadSources(ROOT);
22
22
  const project = scope === 'project';
23
23
  const items = [];
24
24
  const core = { skill: localSkills(ROOT), agent: localAgents(ROOT) };
@@ -27,35 +27,57 @@ function catalog(scope, cwd) {
27
27
  items.push({ name, kind, description: frontDescription(file), category: 'core', source: 'AOS Core', installed: true, core: true });
28
28
  }
29
29
  }
30
- for (const [kind, key, type] of [['skill', 'skills', 'skills'], ['agent', 'subagents', 'agents']]) {
31
- for (const [name, it] of Object.entries(idx[key] || {})) {
32
- if (core[kind].has(name)) continue;
33
- const installed = isInstalled(type, name, it, { project, cwd });
34
- const files = kind === 'skill' && it.files?.length > 1 ? it.files : null;
35
- items.push({
36
- name, kind, description: it.description || '', category: it.category || '', source: 'ECC', installed, upstreamPath: it.upstream_path,
37
- fileCount: files ? files.length : 1,
38
- totalSize: files ? files.reduce((n, f) => n + f.size, 0) : null,
39
- hasHooks: !!files?.some((f) => f.path.split('/').includes('hooks')),
40
- });
30
+ for (const src of sources) {
31
+ for (const [kind, key, type] of [['skill', 'skills', 'skills'], ['agent', 'subagents', 'agents']]) {
32
+ for (const [name, it] of Object.entries(src.index[key] || {})) {
33
+ if (core[kind].has(name)) continue;
34
+ const installed = isInstalled(type, name, it, { project, cwd });
35
+ const files = kind === 'skill' && it.files?.length > 1 ? it.files : null;
36
+ let plan = [];
37
+ let problem = '';
38
+ if (kind === 'skill') {
39
+ try { plan = installPlan(sources, { name, item: it, type, src }); } catch (e) { problem = e.message; }
40
+ }
41
+ const sum = plan.length ? planSummary(plan) : {};
42
+ items.push({
43
+ name, kind, description: it.description || '', category: it.category || '', source: src.label, installed, upstreamPath: it.upstream_path,
44
+ fileCount: files ? files.length : 1,
45
+ totalSize: files ? files.reduce((n, f) => n + f.size, 0) : null,
46
+ hasHooks: !!files?.some((f) => f.path.split('/').includes('hooks')),
47
+ thirdParty: !!src.third_party, license: src.license, requiresAuth: !!it.requires_auth,
48
+ requires: it.requires || [], hasScripts: !!sum.hasScripts, execBridge: !!sum.execBridge, software: sum.software || [], problem,
49
+ });
50
+ }
41
51
  }
42
52
  }
43
- return { scope, cwd, pinned_commit: idx.pinned_commit, items };
53
+ return {
54
+ scope, cwd, pinned_commit: sources[0].pinned_commit, items,
55
+ sources: sources.map((s) => ({ label: s.label, pinned_commit: s.pinned_commit, license: s.license, copyright: s.copyright, upstream: s.upstream })),
56
+ };
44
57
  }
45
58
 
46
59
  const details = new Map();
47
- async function fetchDetail(name, item, commit) {
48
- if (details.has(name)) return details.get(name);
49
- const url = `https://raw.githubusercontent.com/affaan-m/ECC/${commit}/${item.upstreamPath}`;
60
+ async function fetchDetail(item, src) {
61
+ const cacheKey = `${src.label}:${item.name}`;
62
+ if (details.has(cacheKey)) return details.get(cacheKey);
63
+ const url = `${src.raw_base}/${src.pinned_commit}/${item.upstreamPath}`;
50
64
  const res = await fetch(url, { headers: { 'user-agent': 'aos-store' }, signal: AbortSignal.timeout(10000) });
51
65
  if (!res.ok) throw new Error(`upstream ${res.status}`);
52
66
  const fm = (await res.text()).match(/^---\n([\s\S]*?)\n---/);
53
67
  const d = fm?.[1].match(/^description:\s*(?:>-?|\|-?)?\s*([\s\S]*?)(?=\n[A-Za-z_-]+:|(?![\s\S]))/m);
54
68
  const text = (d?.[1] || '').replace(/\s*\n\s*/g, ' ').trim().replace(/^["']|["']$/g, '');
55
- details.set(name, text);
69
+ details.set(cacheKey, text);
56
70
  return text;
57
71
  }
58
72
 
73
+ function previewMeta(name, kind) {
74
+ const sources = loadSources(ROOT);
75
+ const found = findItem(sources, name);
76
+ const plan = kind === 'skill' ? installPlan(sources, found) : [found];
77
+ const { src } = found;
78
+ return { source: src.label, license: src.license, copyright: src.copyright, pinned_commit: src.pinned_commit, thirdParty: !!src.third_party, ...planSummary(plan) };
79
+ }
80
+
59
81
  const runCli = (args, cwd, timeout) => new Promise((done) => {
60
82
  execFile(process.execPath, [CLI, ...args], { cwd, timeout, maxBuffer: 1 << 20 }, (error, stdout, stderr) => {
61
83
  done({ ok: !error, stdout, stderr: stderr || (error && error.message) || '' });
@@ -96,9 +118,10 @@ export function createStoreServer({ cwd = process.cwd() } = {}) {
96
118
  const name = url.searchParams.get('name') || '';
97
119
  if (!NAME_RE.test(name)) return send(res, 400, { error: 'invalid name' });
98
120
  const data = catalog('global', cwd);
99
- const item = data.items.find((i) => i.name === name && i.source === 'ECC' && i.kind === url.searchParams.get('kind'));
121
+ const item = data.items.find((i) => i.name === name && !i.core && i.kind === url.searchParams.get('kind'));
100
122
  if (!item) return send(res, 404, { error: 'not found' });
101
- try { return send(res, 200, { description: await fetchDetail(name, item, data.pinned_commit) }); }
123
+ const src = loadSources(ROOT).find((x) => x.label === item.source);
124
+ try { return send(res, 200, { description: await fetchDetail(item, src) }); }
102
125
  catch (e) { return send(res, 502, { error: e.message }); }
103
126
  }
104
127
  if (p === '/' || p === '/index.html') {
@@ -122,7 +145,7 @@ export function createStoreServer({ cwd = process.cwd() } = {}) {
122
145
  if (p === '/api/preview') {
123
146
  const r = await runCli(['install', name, '--dry-run', ...flags], cwd, 10000);
124
147
  const targets = r.stdout.split('\n').filter((l) => l.startsWith('[dry-run] write ')).map((l) => l.slice(16));
125
- return send(res, 200, { ...r, scope, targets });
148
+ return send(res, 200, { ...r, scope, targets, meta: (() => { try { return previewMeta(name, kind); } catch { return null; } })() });
126
149
  }
127
150
  const r = await runCli(['install', name, '--net', ...flags], cwd, 30000);
128
151
  const installed = catalog(scope, cwd).items.find((i) => i.name === name && i.kind === kind).installed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybridlabor-api/aos",
3
- "version": "4.12.1",
3
+ "version": "4.13.0",
4
4
  "description": "AOS — A Curated AI AGENT OS. Optimized agent skills and add-ons like memB, OpenWiki, Heimdall Token Saver, and Godmode architectures.",
5
5
  "main": "installer.js",
6
6
  "engines": {
@@ -27,7 +27,7 @@
27
27
  "plugin:build": "node scripts/build-plugin-manifest.mjs",
28
28
  "plugin:check": "node scripts/build-plugin-manifest.mjs --check",
29
29
  "doctor": "node bin/aos-doctor.mjs",
30
- "test": "node scripts/validate-skills.mjs --selftest && node scripts/validate-skills.mjs && node scripts/build-plugin-manifest.mjs --check && node --test tests/opencode-graph-gate.test.js && node --test tests/cross-harness-hooks.test.js && node tests/aos-store.test.mjs && node --test tests/aos-store-ui.test.mjs && node tests/aos-doctor.test.mjs && node tests/build-plugin-manifest.test.mjs"
30
+ "test": "node scripts/validate-skills.mjs --selftest && node scripts/validate-skills.mjs && node scripts/build-plugin-manifest.mjs --check && node --test tests/opencode-graph-gate.test.js && node --test tests/cross-harness-hooks.test.js && node tests/aos-store.test.mjs && node --test tests/aos-store-ui.test.mjs && node tests/aos-store-scenario.test.mjs && node tests/aos-doctor.test.mjs && node tests/build-plugin-manifest.test.mjs && node --test tests/agent-models.test.js"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Generates .claude-plugin/plugin.json and the plugin-root agents/ directory.
3
+ // Generates .claude-plugin/plugin.json, the plugin-root agents/ directory and the
4
+ // subfolder plugin plugins/bdb-aos/ (same manifest data + skills/agents symlinks
5
+ // into the repo root, so the marketplace install cache stays small).
4
6
  //
5
7
  // Claude Code resolves plugin components two different ways, and only one of
6
8
  // them is obvious:
@@ -21,7 +23,7 @@
21
23
  //
22
24
  // Usage: node scripts/build-plugin-manifest.mjs [--check]
23
25
 
24
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
26
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
25
27
  import { dirname, join, relative, resolve } from 'node:path';
26
28
  import { fileURLToPath } from 'node:url';
27
29
 
@@ -94,9 +96,20 @@ const manifest = {
94
96
  skills,
95
97
  };
96
98
 
97
- const target = join(ROOT, '.claude-plugin', 'plugin.json');
98
99
  const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
99
- const current = existsSync(target) ? readFileSync(target, 'utf8') : '';
100
+ const SUBDIR = join(ROOT, 'plugins', 'bdb-aos');
101
+ // Both manifests come from the same data; the subfolder one resolves the same
102
+ // ./skills/... paths through its symlinks.
103
+ const targets = [
104
+ join(ROOT, '.claude-plugin', 'plugin.json'),
105
+ join(SUBDIR, '.claude-plugin', 'plugin.json'),
106
+ ];
107
+ const stale = targets.filter((t) => !existsSync(t) || readFileSync(t, 'utf8') !== serialized);
108
+ const links = { skills: '../../skills', agents: '../../agents' };
109
+ const badLinks = Object.entries(links).filter(([name, to]) => {
110
+ const path = join(SUBDIR, name);
111
+ return !existsSync(path) || !lstatSync(path).isSymbolicLink() || readlinkSync(path) !== to;
112
+ });
100
113
 
101
114
  // `claude plugin tag` refuses to cut a release unless plugin.json and the
102
115
  // enclosing marketplace entry agree on the version, so the marketplace copy of
@@ -110,8 +123,12 @@ const marketSerialized = `${JSON.stringify(market, null, 2)}\n`;
110
123
  const marketCurrent = readFileSync(marketPath, 'utf8');
111
124
 
112
125
  if (CHECK) {
113
- if (current !== serialized) {
114
- console.error('plugin.json is out of date — run: node scripts/build-plugin-manifest.mjs');
126
+ if (stale.length > 0) {
127
+ console.error(`plugin.json is out of date (${stale.map((t) => relative(ROOT, t)).join(', ')}) — run: node scripts/build-plugin-manifest.mjs`);
128
+ process.exit(1);
129
+ }
130
+ if (badLinks.length > 0) {
131
+ console.error(`plugins/bdb-aos symlinks missing or wrong (${badLinks.map(([n]) => n).join(', ')}) — run: node scripts/build-plugin-manifest.mjs`);
115
132
  process.exit(1);
116
133
  }
117
134
  if (marketCurrent !== marketSerialized) {
@@ -122,10 +139,16 @@ if (CHECK) {
122
139
  console.error(`agents/ is out of date (${drift.join(', ')}) — run: node scripts/build-plugin-manifest.mjs`);
123
140
  process.exit(1);
124
141
  }
125
- console.log(`plugin manifest up to date: ${skills.length} skills, ${agentNames.length} agents`);
142
+ console.log(`plugin manifests up to date: ${skills.length} skills, ${agentNames.length} agents`);
126
143
  } else {
127
- mkdirSync(dirname(target), { recursive: true });
128
- writeFileSync(target, serialized);
144
+ for (const t of targets) {
145
+ mkdirSync(dirname(t), { recursive: true });
146
+ writeFileSync(t, serialized);
147
+ }
148
+ for (const [name, to] of badLinks) {
149
+ rmSync(join(SUBDIR, name), { force: true });
150
+ symlinkSync(to, join(SUBDIR, name));
151
+ }
129
152
  writeFileSync(marketPath, marketSerialized);
130
- console.log(`Wrote .claude-plugin/plugin.json with ${skills.length} skills and ${agentNames.length} agents`);
153
+ console.log(`Wrote 2 plugin.json files with ${skills.length} skills and ${agentNames.length} agents`);
131
154
  }