@pilotspace/add 1.13.0 → 1.15.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 (308) hide show
  1. package/CHANGELOG.md +189 -1
  2. package/THIRD_PARTY_NOTICES.md +41 -0
  3. package/bin/cli.js +240 -26
  4. package/docs/02-the-flow.md +1 -1
  5. package/docs/03-step-1-specify.md +4 -0
  6. package/docs/04-step-2-scenarios.md +4 -0
  7. package/docs/06-step-4-tests.md +4 -0
  8. package/docs/17-components.md +23 -5
  9. package/docs/18-personas.md +80 -0
  10. package/docs/add-milestone-task-lifecycle.png +0 -0
  11. package/docs/add-task-growth-wheel.png +0 -0
  12. package/docs/appendix-c-glossary.md +8 -4
  13. package/docs/appendix-d-worked-example.md +79 -0
  14. package/package.json +3 -1
  15. package/personas-teacher/LICENSE +21 -0
  16. package/personas-teacher/README.md +1067 -0
  17. package/personas-teacher/VENDOR.md +13 -0
  18. package/personas-teacher/academic/academic-anthropologist.md +125 -0
  19. package/personas-teacher/academic/academic-geographer.md +127 -0
  20. package/personas-teacher/academic/academic-historian.md +123 -0
  21. package/personas-teacher/academic/academic-narratologist.md +118 -0
  22. package/personas-teacher/academic/academic-psychologist.md +118 -0
  23. package/personas-teacher/design/design-brand-guardian.md +322 -0
  24. package/personas-teacher/design/design-image-prompt-engineer.md +236 -0
  25. package/personas-teacher/design/design-inclusive-visuals-specialist.md +71 -0
  26. package/personas-teacher/design/design-persona-walkthrough.md +272 -0
  27. package/personas-teacher/design/design-ui-designer.md +383 -0
  28. package/personas-teacher/design/design-ux-architect.md +469 -0
  29. package/personas-teacher/design/design-ux-researcher.md +329 -0
  30. package/personas-teacher/design/design-visual-storyteller.md +149 -0
  31. package/personas-teacher/design/design-whimsy-injector.md +438 -0
  32. package/personas-teacher/divisions.json +21 -0
  33. package/personas-teacher/engineering/engineering-ai-data-remediation-engineer.md +211 -0
  34. package/personas-teacher/engineering/engineering-ai-engineer.md +146 -0
  35. package/personas-teacher/engineering/engineering-autonomous-optimization-architect.md +107 -0
  36. package/personas-teacher/engineering/engineering-backend-architect.md +236 -0
  37. package/personas-teacher/engineering/engineering-cms-developer.md +536 -0
  38. package/personas-teacher/engineering/engineering-code-reviewer.md +76 -0
  39. package/personas-teacher/engineering/engineering-codebase-onboarding-engineer.md +173 -0
  40. package/personas-teacher/engineering/engineering-data-engineer.md +306 -0
  41. package/personas-teacher/engineering/engineering-database-optimizer.md +176 -0
  42. package/personas-teacher/engineering/engineering-devops-automator.md +376 -0
  43. package/personas-teacher/engineering/engineering-drupal-shopping-cart.md +360 -0
  44. package/personas-teacher/engineering/engineering-email-intelligence-engineer.md +353 -0
  45. package/personas-teacher/engineering/engineering-embedded-firmware-engineer.md +173 -0
  46. package/personas-teacher/engineering/engineering-feishu-integration-developer.md +598 -0
  47. package/personas-teacher/engineering/engineering-filament-optimization-specialist.md +283 -0
  48. package/personas-teacher/engineering/engineering-frontend-developer.md +225 -0
  49. package/personas-teacher/engineering/engineering-git-workflow-master.md +84 -0
  50. package/personas-teacher/engineering/engineering-incident-response-commander.md +444 -0
  51. package/personas-teacher/engineering/engineering-it-service-manager.md +561 -0
  52. package/personas-teacher/engineering/engineering-minimal-change-engineer.md +207 -0
  53. package/personas-teacher/engineering/engineering-mobile-app-builder.md +493 -0
  54. package/personas-teacher/engineering/engineering-multi-agent-systems-architect.md +600 -0
  55. package/personas-teacher/engineering/engineering-orgscript-engineer.md +113 -0
  56. package/personas-teacher/engineering/engineering-prompt-engineer.md +202 -0
  57. package/personas-teacher/engineering/engineering-rapid-prototyper.md +462 -0
  58. package/personas-teacher/engineering/engineering-senior-developer.md +176 -0
  59. package/personas-teacher/engineering/engineering-software-architect.md +112 -0
  60. package/personas-teacher/engineering/engineering-solidity-smart-contract-engineer.md +522 -0
  61. package/personas-teacher/engineering/engineering-sre.md +90 -0
  62. package/personas-teacher/engineering/engineering-technical-writer.md +393 -0
  63. package/personas-teacher/engineering/engineering-voice-ai-integration-engineer.md +561 -0
  64. package/personas-teacher/engineering/engineering-wechat-mini-program-developer.md +350 -0
  65. package/personas-teacher/engineering/engineering-wordpress-shopping-cart.md +346 -0
  66. package/personas-teacher/examples/README.md +48 -0
  67. package/personas-teacher/examples/nexus-spatial-discovery.md +852 -0
  68. package/personas-teacher/examples/workflow-book-chapter.md +55 -0
  69. package/personas-teacher/examples/workflow-landing-page.md +119 -0
  70. package/personas-teacher/examples/workflow-startup-mvp.md +155 -0
  71. package/personas-teacher/examples/workflow-with-memory.md +238 -0
  72. package/personas-teacher/finance/finance-bookkeeper-controller.md +260 -0
  73. package/personas-teacher/finance/finance-financial-analyst.md +234 -0
  74. package/personas-teacher/finance/finance-fpa-analyst.md +263 -0
  75. package/personas-teacher/finance/finance-investment-researcher.md +272 -0
  76. package/personas-teacher/finance/finance-tax-strategist.md +239 -0
  77. package/personas-teacher/game-development/blender/blender-addon-engineer.md +234 -0
  78. package/personas-teacher/game-development/game-audio-engineer.md +264 -0
  79. package/personas-teacher/game-development/game-designer.md +167 -0
  80. package/personas-teacher/game-development/godot/godot-gameplay-scripter.md +334 -0
  81. package/personas-teacher/game-development/godot/godot-multiplayer-engineer.md +297 -0
  82. package/personas-teacher/game-development/godot/godot-shader-developer.md +266 -0
  83. package/personas-teacher/game-development/level-designer.md +208 -0
  84. package/personas-teacher/game-development/narrative-designer.md +243 -0
  85. package/personas-teacher/game-development/roblox-studio/roblox-avatar-creator.md +297 -0
  86. package/personas-teacher/game-development/roblox-studio/roblox-experience-designer.md +305 -0
  87. package/personas-teacher/game-development/roblox-studio/roblox-systems-scripter.md +325 -0
  88. package/personas-teacher/game-development/technical-artist.md +229 -0
  89. package/personas-teacher/game-development/unity/unity-architect.md +271 -0
  90. package/personas-teacher/game-development/unity/unity-editor-tool-developer.md +310 -0
  91. package/personas-teacher/game-development/unity/unity-multiplayer-engineer.md +321 -0
  92. package/personas-teacher/game-development/unity/unity-shader-graph-artist.md +269 -0
  93. package/personas-teacher/game-development/unreal-engine/unreal-multiplayer-architect.md +313 -0
  94. package/personas-teacher/game-development/unreal-engine/unreal-systems-engineer.md +310 -0
  95. package/personas-teacher/game-development/unreal-engine/unreal-technical-artist.md +256 -0
  96. package/personas-teacher/game-development/unreal-engine/unreal-world-builder.md +273 -0
  97. package/personas-teacher/gis/gis-3d-scene-developer.md +111 -0
  98. package/personas-teacher/gis/gis-analyst.md +91 -0
  99. package/personas-teacher/gis/gis-bim-specialist.md +108 -0
  100. package/personas-teacher/gis/gis-cartography-designer.md +150 -0
  101. package/personas-teacher/gis/gis-drone-reality-mapping.md +120 -0
  102. package/personas-teacher/gis/gis-geoai-ml-engineer.md +105 -0
  103. package/personas-teacher/gis/gis-geoprocessing-specialist.md +97 -0
  104. package/personas-teacher/gis/gis-qa-engineer.md +133 -0
  105. package/personas-teacher/gis/gis-solution-engineer.md +101 -0
  106. package/personas-teacher/gis/gis-spatial-data-engineer.md +97 -0
  107. package/personas-teacher/gis/gis-spatial-data-scientist.md +111 -0
  108. package/personas-teacher/gis/gis-technical-consultant.md +86 -0
  109. package/personas-teacher/gis/gis-web-gis-developer.md +108 -0
  110. package/personas-teacher/marketing/marketing-aeo-foundations.md +264 -0
  111. package/personas-teacher/marketing/marketing-agentic-search-optimizer.md +311 -0
  112. package/personas-teacher/marketing/marketing-ai-citation-strategist.md +170 -0
  113. package/personas-teacher/marketing/marketing-app-store-optimizer.md +321 -0
  114. package/personas-teacher/marketing/marketing-baidu-seo-specialist.md +226 -0
  115. package/personas-teacher/marketing/marketing-bilibili-content-strategist.md +199 -0
  116. package/personas-teacher/marketing/marketing-book-co-author.md +110 -0
  117. package/personas-teacher/marketing/marketing-carousel-growth-engine.md +199 -0
  118. package/personas-teacher/marketing/marketing-china-ecommerce-operator.md +283 -0
  119. package/personas-teacher/marketing/marketing-china-market-localization-strategist.md +283 -0
  120. package/personas-teacher/marketing/marketing-content-creator.md +54 -0
  121. package/personas-teacher/marketing/marketing-cross-border-ecommerce.md +259 -0
  122. package/personas-teacher/marketing/marketing-douyin-strategist.md +149 -0
  123. package/personas-teacher/marketing/marketing-email-strategist.md +249 -0
  124. package/personas-teacher/marketing/marketing-global-podcast-strategist.md +206 -0
  125. package/personas-teacher/marketing/marketing-growth-hacker.md +54 -0
  126. package/personas-teacher/marketing/marketing-instagram-curator.md +113 -0
  127. package/personas-teacher/marketing/marketing-kuaishou-strategist.md +223 -0
  128. package/personas-teacher/marketing/marketing-linkedin-content-creator.md +214 -0
  129. package/personas-teacher/marketing/marketing-livestream-commerce-coach.md +305 -0
  130. package/personas-teacher/marketing/marketing-multi-platform-publisher.md +217 -0
  131. package/personas-teacher/marketing/marketing-podcast-strategist.md +277 -0
  132. package/personas-teacher/marketing/marketing-pr-communications-manager.md +473 -0
  133. package/personas-teacher/marketing/marketing-private-domain-operator.md +308 -0
  134. package/personas-teacher/marketing/marketing-reddit-community-builder.md +123 -0
  135. package/personas-teacher/marketing/marketing-seo-specialist.md +321 -0
  136. package/personas-teacher/marketing/marketing-short-video-editing-coach.md +412 -0
  137. package/personas-teacher/marketing/marketing-social-media-strategist.md +125 -0
  138. package/personas-teacher/marketing/marketing-tiktok-strategist.md +125 -0
  139. package/personas-teacher/marketing/marketing-twitter-engager.md +126 -0
  140. package/personas-teacher/marketing/marketing-video-optimization-specialist.md +119 -0
  141. package/personas-teacher/marketing/marketing-wechat-official-account.md +145 -0
  142. package/personas-teacher/marketing/marketing-weibo-strategist.md +240 -0
  143. package/personas-teacher/marketing/marketing-x-twitter-intelligence-analyst.md +161 -0
  144. package/personas-teacher/marketing/marketing-xiaohongshu-specialist.md +138 -0
  145. package/personas-teacher/marketing/marketing-zhihu-strategist.md +162 -0
  146. package/personas-teacher/paid-media/paid-media-auditor.md +71 -0
  147. package/personas-teacher/paid-media/paid-media-creative-strategist.md +71 -0
  148. package/personas-teacher/paid-media/paid-media-paid-social-strategist.md +71 -0
  149. package/personas-teacher/paid-media/paid-media-ppc-strategist.md +71 -0
  150. package/personas-teacher/paid-media/paid-media-programmatic-buyer.md +71 -0
  151. package/personas-teacher/paid-media/paid-media-search-query-analyst.md +71 -0
  152. package/personas-teacher/paid-media/paid-media-tracking-specialist.md +71 -0
  153. package/personas-teacher/product/product-behavioral-nudge-engine.md +80 -0
  154. package/personas-teacher/product/product-feedback-synthesizer.md +119 -0
  155. package/personas-teacher/product/product-manager.md +469 -0
  156. package/personas-teacher/product/product-sprint-prioritizer.md +154 -0
  157. package/personas-teacher/product/product-trend-researcher.md +159 -0
  158. package/personas-teacher/project-management/project-management-experiment-tracker.md +198 -0
  159. package/personas-teacher/project-management/project-management-jira-workflow-steward.md +230 -0
  160. package/personas-teacher/project-management/project-management-meeting-notes-specialist.md +95 -0
  161. package/personas-teacher/project-management/project-management-project-shepherd.md +194 -0
  162. package/personas-teacher/project-management/project-management-studio-operations.md +200 -0
  163. package/personas-teacher/project-management/project-management-studio-producer.md +203 -0
  164. package/personas-teacher/project-management/project-manager-senior.md +135 -0
  165. package/personas-teacher/sales/sales-account-strategist.md +227 -0
  166. package/personas-teacher/sales/sales-coach.md +271 -0
  167. package/personas-teacher/sales/sales-deal-strategist.md +180 -0
  168. package/personas-teacher/sales/sales-discovery-coach.md +225 -0
  169. package/personas-teacher/sales/sales-engineer.md +182 -0
  170. package/personas-teacher/sales/sales-offer-lead-gen-strategist.md +257 -0
  171. package/personas-teacher/sales/sales-outbound-strategist.md +201 -0
  172. package/personas-teacher/sales/sales-pipeline-analyst.md +267 -0
  173. package/personas-teacher/sales/sales-proposal-strategist.md +217 -0
  174. package/personas-teacher/security/security-appsec-engineer.md +491 -0
  175. package/personas-teacher/security/security-architect.md +304 -0
  176. package/personas-teacher/security/security-blockchain-security-auditor.md +463 -0
  177. package/personas-teacher/security/security-cloud-security-architect.md +523 -0
  178. package/personas-teacher/security/security-compliance-auditor.md +158 -0
  179. package/personas-teacher/security/security-incident-responder.md +437 -0
  180. package/personas-teacher/security/security-penetration-tester.md +399 -0
  181. package/personas-teacher/security/security-senior-secops.md +750 -0
  182. package/personas-teacher/security/security-threat-detection-engineer.md +534 -0
  183. package/personas-teacher/security/security-threat-intelligence-analyst.md +644 -0
  184. package/personas-teacher/spatial-computing/macos-spatial-metal-engineer.md +337 -0
  185. package/personas-teacher/spatial-computing/terminal-integration-specialist.md +70 -0
  186. package/personas-teacher/spatial-computing/visionos-spatial-engineer.md +54 -0
  187. package/personas-teacher/spatial-computing/xr-cockpit-interaction-specialist.md +32 -0
  188. package/personas-teacher/spatial-computing/xr-immersive-developer.md +32 -0
  189. package/personas-teacher/spatial-computing/xr-interface-architect.md +32 -0
  190. package/personas-teacher/specialized/accounts-payable-agent.md +185 -0
  191. package/personas-teacher/specialized/agentic-identity-trust.md +387 -0
  192. package/personas-teacher/specialized/agents-orchestrator.md +367 -0
  193. package/personas-teacher/specialized/automation-governance-architect.md +216 -0
  194. package/personas-teacher/specialized/business-strategist.md +488 -0
  195. package/personas-teacher/specialized/change-management-consultant.md +497 -0
  196. package/personas-teacher/specialized/chief-financial-officer.md +388 -0
  197. package/personas-teacher/specialized/corporate-training-designer.md +192 -0
  198. package/personas-teacher/specialized/customer-service.md +398 -0
  199. package/personas-teacher/specialized/customer-success-manager.md +460 -0
  200. package/personas-teacher/specialized/data-consolidation-agent.md +60 -0
  201. package/personas-teacher/specialized/data-privacy-officer.md +412 -0
  202. package/personas-teacher/specialized/esg-sustainability-officer.md +396 -0
  203. package/personas-teacher/specialized/government-digital-presales-consultant.md +363 -0
  204. package/personas-teacher/specialized/grant-writer.md +511 -0
  205. package/personas-teacher/specialized/healthcare-customer-service.md +389 -0
  206. package/personas-teacher/specialized/healthcare-marketing-compliance.md +395 -0
  207. package/personas-teacher/specialized/hospitality-guest-services.md +603 -0
  208. package/personas-teacher/specialized/hr-onboarding.md +451 -0
  209. package/personas-teacher/specialized/identity-graph-operator.md +260 -0
  210. package/personas-teacher/specialized/language-translator.md +264 -0
  211. package/personas-teacher/specialized/legal-billing-time-tracking.md +569 -0
  212. package/personas-teacher/specialized/legal-client-intake.md +492 -0
  213. package/personas-teacher/specialized/legal-document-review.md +454 -0
  214. package/personas-teacher/specialized/loan-officer-assistant.md +555 -0
  215. package/personas-teacher/specialized/lsp-index-engineer.md +314 -0
  216. package/personas-teacher/specialized/ma-integration-manager.md +427 -0
  217. package/personas-teacher/specialized/medical-billing-coding-specialist.md +491 -0
  218. package/personas-teacher/specialized/operations-manager.md +399 -0
  219. package/personas-teacher/specialized/organizational-psychologist.md +391 -0
  220. package/personas-teacher/specialized/personal-growth-mentor.md +159 -0
  221. package/personas-teacher/specialized/real-estate-buyer-seller.md +596 -0
  222. package/personas-teacher/specialized/recruitment-specialist.md +509 -0
  223. package/personas-teacher/specialized/report-distribution-agent.md +65 -0
  224. package/personas-teacher/specialized/retail-customer-returns.md +566 -0
  225. package/personas-teacher/specialized/sales-data-extraction-agent.md +67 -0
  226. package/personas-teacher/specialized/sales-outreach.md +425 -0
  227. package/personas-teacher/specialized/specialized-chief-of-staff.md +279 -0
  228. package/personas-teacher/specialized/specialized-civil-engineer.md +356 -0
  229. package/personas-teacher/specialized/specialized-cultural-intelligence-strategist.md +88 -0
  230. package/personas-teacher/specialized/specialized-developer-advocate.md +317 -0
  231. package/personas-teacher/specialized/specialized-document-generator.md +55 -0
  232. package/personas-teacher/specialized/specialized-french-consulting-market.md +192 -0
  233. package/personas-teacher/specialized/specialized-korean-business-navigator.md +216 -0
  234. package/personas-teacher/specialized/specialized-mcp-builder.md +248 -0
  235. package/personas-teacher/specialized/specialized-model-qa.md +488 -0
  236. package/personas-teacher/specialized/specialized-pricing-analyst.md +243 -0
  237. package/personas-teacher/specialized/specialized-salesforce-architect.md +180 -0
  238. package/personas-teacher/specialized/specialized-strategy-duel-agent.md +130 -0
  239. package/personas-teacher/specialized/specialized-workflow-architect.md +597 -0
  240. package/personas-teacher/specialized/study-abroad-advisor.md +282 -0
  241. package/personas-teacher/specialized/supply-chain-strategist.md +582 -0
  242. package/personas-teacher/specialized/zk-steward.md +211 -0
  243. package/personas-teacher/strategy/EXECUTIVE-BRIEF.md +95 -0
  244. package/personas-teacher/strategy/QUICKSTART.md +194 -0
  245. package/personas-teacher/strategy/coordination/agent-activation-prompts.md +401 -0
  246. package/personas-teacher/strategy/coordination/handoff-templates.md +357 -0
  247. package/personas-teacher/strategy/nexus-strategy.md +1110 -0
  248. package/personas-teacher/strategy/playbooks/phase-0-discovery.md +178 -0
  249. package/personas-teacher/strategy/playbooks/phase-1-strategy.md +238 -0
  250. package/personas-teacher/strategy/playbooks/phase-2-foundation.md +278 -0
  251. package/personas-teacher/strategy/playbooks/phase-3-build.md +286 -0
  252. package/personas-teacher/strategy/playbooks/phase-4-hardening.md +332 -0
  253. package/personas-teacher/strategy/playbooks/phase-5-launch.md +277 -0
  254. package/personas-teacher/strategy/playbooks/phase-6-operate.md +318 -0
  255. package/personas-teacher/strategy/runbooks/scenario-enterprise-feature.md +157 -0
  256. package/personas-teacher/strategy/runbooks/scenario-incident-response.md +217 -0
  257. package/personas-teacher/strategy/runbooks/scenario-marketing-campaign.md +187 -0
  258. package/personas-teacher/strategy/runbooks/scenario-startup-mvp.md +154 -0
  259. package/personas-teacher/support/support-analytics-reporter.md +365 -0
  260. package/personas-teacher/support/support-executive-summary-generator.md +212 -0
  261. package/personas-teacher/support/support-finance-tracker.md +442 -0
  262. package/personas-teacher/support/support-infrastructure-maintainer.md +618 -0
  263. package/personas-teacher/support/support-legal-compliance-checker.md +588 -0
  264. package/personas-teacher/support/support-support-responder.md +585 -0
  265. package/personas-teacher/testing/testing-accessibility-auditor.md +316 -0
  266. package/personas-teacher/testing/testing-api-tester.md +306 -0
  267. package/personas-teacher/testing/testing-evidence-collector.md +210 -0
  268. package/personas-teacher/testing/testing-performance-benchmarker.md +268 -0
  269. package/personas-teacher/testing/testing-reality-checker.md +236 -0
  270. package/personas-teacher/testing/testing-test-results-analyzer.md +305 -0
  271. package/personas-teacher/testing/testing-tool-evaluator.md +394 -0
  272. package/personas-teacher/testing/testing-workflow-optimizer.md +450 -0
  273. package/personas-teacher/tools.json +19 -0
  274. package/skill/add/SKILL.md +17 -12
  275. package/skill/add/advisor.md +31 -34
  276. package/skill/add/components.md +21 -23
  277. package/skill/add/confidence.md +1 -1
  278. package/skill/add/deltas.md +12 -13
  279. package/skill/add/design.md +52 -49
  280. package/skill/add/fold.md +18 -16
  281. package/skill/add/intake.md +4 -3
  282. package/skill/add/loop.md +1 -2
  283. package/skill/add/phases/0-ground.md +32 -34
  284. package/skill/add/phases/0-setup.md +22 -21
  285. package/skill/add/phases/1-specify.md +8 -6
  286. package/skill/add/phases/3-contract.md +1 -1
  287. package/skill/add/phases/4-tests.md +9 -10
  288. package/skill/add/phases/5-build.md +17 -13
  289. package/skill/add/phases/6-verify.md +7 -4
  290. package/skill/add/phases/7-observe.md +2 -1
  291. package/skill/add/report-template.md +61 -18
  292. package/skill/add/run.md +4 -3
  293. package/skill/add/scope.md +2 -2
  294. package/skill/add/sensitivity.md +51 -0
  295. package/skill/add/streams.md +24 -23
  296. package/tooling/add.py +1076 -39
  297. package/tooling/add_engine/autonomy.py +36 -1
  298. package/tooling/add_engine/constants.py +56 -3
  299. package/tooling/add_engine/guidelines.py +22 -20
  300. package/tooling/add_engine/predicates.py +56 -3
  301. package/tooling/add_engine/search.py +165 -0
  302. package/tooling/templates/GLOSSARY.md.tmpl +16 -0
  303. package/tooling/templates/MILESTONE.md.tmpl +9 -0
  304. package/tooling/templates/PROMPT.persona.md.tmpl +44 -0
  305. package/tooling/templates/TASK.fast.md.tmpl +2 -1
  306. package/tooling/templates/TASK.md.tmpl +36 -6
  307. package/tooling/templates/gitignore.tmpl +12 -0
  308. package/tooling/templates/personas/_template.md.tmpl +60 -0
package/bin/cli.js CHANGED
@@ -39,7 +39,8 @@ function parseArgs(argv) {
39
39
  // defaults the stage and infers the name from the folder, so the manual-init
40
40
  // hint only echoes flags the user actually chose (shortest true command).
41
41
  const args = { _: [], force: false, check: false, noSkill: false, stage: null, name: null,
42
- yes: false, nonInteractive: false, global: false, globalData: false, ruleFile: false };
42
+ yes: false, nonInteractive: false, global: false, globalData: false,
43
+ fromGlobalData: false, ruleFile: false };
43
44
  for (let i = 0; i < argv.length; i++) {
44
45
  const a = argv[i];
45
46
  if (a === "--force") args.force = true;
@@ -50,6 +51,9 @@ function parseArgs(argv) {
50
51
  // --global-data: (implies --global) ALSO persist this project's user-data under
51
52
  // <home>/data/<key> keyed by path (opt-in, one-way snapshot).
52
53
  else if (a === "--global-data") args.globalData = true;
54
+ // --from-global-data: the INVERSE — rehydrate this project's user-data FROM the shared
55
+ // home on a fresh clone (non-destructive fill-gaps; --force overwrites with a .bak).
56
+ else if (a === "--from-global-data") args.fromGlobalData = true;
53
57
  // --yes / --non-interactive: skip all prompts, take defaults — the explicit
54
58
  // non-interactive selector the interactive() gate honors (CI/pipes do this too).
55
59
  else if (a === "--yes" || a === "-y") args.yes = true;
@@ -581,11 +585,21 @@ function seedSoulMd(target) {
581
585
  }
582
586
  }
583
587
 
584
- // Ensure .add/.gitignore lists the engine's transient artifacts. Seed it from the bundled
585
- // tooling/templates/gitignore.tmpl if absent; else APPEND-IF-ABSENT each pattern line the
586
- // template carries that the file lacks additive only, never reorders/removes user lines,
587
- // idempotent; comment/blank lines are not appended to an existing file. Fail-soft. Twin of
588
- // _installer.py:_seed_gitignore.
588
+ // kept OUTSIDE add_engine/constants.py / gitignore.tmpl deliberately: the engine's own
589
+ // _GITIGNORE_BODY constant must never contain "personas-teacher" (test_engine_unchanged_
590
+ // and_handsoff the engine stays hands-off of the teacher vendor tree). The INSTALLER
591
+ // already names that tree explicitly (MANAGED/OPTIONAL), so it is free to seed this one
592
+ // extra ignore line itself. BARE (not repo-root style): .add/.gitignore lives INSIDE
593
+ // .add/, so git resolves its patterns relative to .add/ itself. Twin of
594
+ // _installer.py:_INSTALLER_MANAGED_IGNORE_EXTRA.
595
+ const INSTALLER_MANAGED_IGNORE_EXTRA = ["personas-teacher/"];
596
+
597
+ // Ensure .add/.gitignore lists the engine's transient artifacts + managed vendor trees.
598
+ // Seed it from the bundled tooling/templates/gitignore.tmpl (plus
599
+ // INSTALLER_MANAGED_IGNORE_EXTRA) if absent; else APPEND-IF-ABSENT each pattern line that
600
+ // combined body carries that the file lacks — additive only, never reorders/removes user
601
+ // lines, idempotent; comment/blank lines are not appended to an existing file. Fail-soft.
602
+ // Twin of _installer.py:_seed_gitignore.
589
603
  function seedGitignore(target) {
590
604
  const source = path.join(PKG_ROOT, "tooling", "templates", "gitignore.tmpl");
591
605
  if (!fs.existsSync(source)) {
@@ -594,7 +608,9 @@ function seedGitignore(target) {
594
608
  }
595
609
  const dest = path.join(target, ".add", ".gitignore");
596
610
  try {
597
- const body = fs.readFileSync(source, "utf8");
611
+ let body = fs.readFileSync(source, "utf8");
612
+ if (!body.endsWith("\n")) body += "\n";
613
+ body += INSTALLER_MANAGED_IGNORE_EXTRA.join("\n") + "\n";
598
614
  if (!fs.existsSync(dest)) {
599
615
  fs.mkdirSync(path.dirname(dest), { recursive: true });
600
616
  fs.writeFileSync(dest, body); // seed-if-missing
@@ -694,12 +710,23 @@ async function cmdInit(args) {
694
710
  }
695
711
  }
696
712
  if (args.globalData) args.global = true; // --global-data implies --global (need a home)
713
+ // OPT-IN restore: the home MUST already exist — no_global_home is a HARD fail, checked FAST
714
+ // here so nothing lands in the target on a missing home (the restore itself runs after the drop).
715
+ if (args.fromGlobalData) {
716
+ const home = resolveGlobalHome(process.env);
717
+ if (!fs.existsSync(path.join(home, STAMP_FILE))) {
718
+ fail("no global ADD install at " + home + " (.add-version not found) — nothing to restore " +
719
+ "from; run `init --global-data` on a source checkout first");
720
+ }
721
+ }
697
722
  // OPT-IN global home, BEFORE the per-project drop (fail-closed if the home is unwritable
698
723
  // or its registry is corrupt — the package + the self-contained default stay usable).
699
724
  if (args.global) installGlobal(args, chosenTarget);
700
725
  dropFiles(args, chosenTarget, profile, intent);
701
726
  // OPT-IN data persist, AFTER the drop (one-way snapshot of existing user-data).
702
727
  if (args.globalData) installGlobalData(chosenTarget);
728
+ // OPT-IN restore (consume), AFTER the drop: rehydrate user-data from the home into this clone.
729
+ if (args.fromGlobalData) installGlobalDataRestore(chosenTarget, args.force);
703
730
  }
704
731
 
705
732
  // --- update: re-materialize the managed layer without a re-install -----------
@@ -710,8 +737,15 @@ const MANAGED = [
710
737
  ["skill/add", [".claude", "skills", "add"], false],
711
738
  ["tooling", [".add", "tooling"], true],
712
739
  ["docs", [".add", "docs"], false],
740
+ ["personas-teacher", [".add", "personas-teacher"], false],
713
741
  ];
742
+ // Optional managed trees: an ENHANCEMENT the persona phase reads, not core runtime. The real
743
+ // package always ships these (guarded by test_packaging); a malformed/older package missing one
744
+ // must NOT abort the install — the core lands and the optional tree is soft-skipped. Twin of
745
+ // _installer.py:OPTIONAL. Design-for-failure.
746
+ const OPTIONAL = new Set(["personas-teacher"]);
714
747
  const STAMP_FILE = ".add-version";
748
+ const LOCK_FILE = ".update.lock"; // the `update --global` home lock (never user-data)
715
749
 
716
750
  function pkgVersion() {
717
751
  try { return require(path.join(PKG_ROOT, "package.json")).version; }
@@ -732,8 +766,30 @@ function writeStamp(addDir, version, channel) {
732
766
  );
733
767
  }
734
768
 
769
+ // The set of FILE paths (recursive leaves) under root, RELATIVE to root. ∅ if absent.
770
+ // Directories are not counted — only files (the "manifest" the roll-up measures).
771
+ function treeFiles(root) {
772
+ const out = new Set();
773
+ if (!fs.existsSync(root)) return out;
774
+ const walk = (dir) => {
775
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
776
+ const full = path.join(dir, entry.name);
777
+ if (entry.isDirectory()) walk(full);
778
+ else out.add(path.relative(root, full));
779
+ }
780
+ };
781
+ walk(root);
782
+ return out;
783
+ }
784
+
785
+ // Returns a file-level roll-up of the heal: { restored, refreshed } where `restored` = a
786
+ // file in the FINAL tree whose relative path was ABSENT before the wipe (fresh or
787
+ // partially-gutted trees heal these), `refreshed` = a final file that was PRESENT before
788
+ // (re-materialized). Orphans (present before, gone after) are swept, counted as neither.
789
+ // Pure observation — copy semantics unchanged. Mirror of _installer.py:_clean_replace.
735
790
  function cleanReplaceTree(src, dest, stripTests) {
736
791
  if (!fs.existsSync(src)) fail("missing packaged source: " + src);
792
+ const before = treeFiles(dest); // snapshot BEFORE the wipe, or it's always ∅
737
793
  fs.mkdirSync(path.dirname(dest), { recursive: true });
738
794
  if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
739
795
  fs.cpSync(src, dest, { recursive: true });
@@ -743,9 +799,12 @@ function cleanReplaceTree(src, dest, stripTests) {
743
799
  if (/^test_.*\.py$/.test(entry)) fs.rmSync(path.join(dest, entry), { force: true });
744
800
  }
745
801
  }
802
+ let restored = 0, refreshed = 0;
803
+ for (const f of treeFiles(dest)) (before.has(f) ? refreshed++ : restored++);
804
+ return { restored: restored, refreshed: refreshed };
746
805
  }
747
806
 
748
- const TREE_LABEL = { "skill/add": "skill", "tooling": "tooling", "docs": "docs" };
807
+ const TREE_LABEL = { "skill/add": "skill", "tooling": "tooling", "docs": "docs", "personas-teacher": "personas" };
749
808
 
750
809
  // Per managed tree: "missing" (dest absent OR empty) or "present".
751
810
  function managedStatus(target) {
@@ -764,15 +823,20 @@ function managedStatus(target) {
764
823
  // a corrupt package leaves the target untouched).
765
824
  function reconcile(args, target, srcRoot) {
766
825
  srcRoot = srcRoot || PKG_ROOT; // default: the package; the global home feeds propagation
767
- const trees = MANAGED.filter(([sub]) => !(sub === "skill/add" && args.noSkill));
826
+ const trees = MANAGED
827
+ .filter(([sub]) => !(sub === "skill/add" && args.noSkill))
828
+ .filter(([sub]) => !(OPTIONAL.has(sub) && !fs.existsSync(path.join(srcRoot, sub)))); // soft-skip absent optional
768
829
  for (const [sub] of trees) {
769
830
  if (!fs.existsSync(path.join(srcRoot, sub))) {
770
831
  fail("missing packaged source: " + path.join(srcRoot, sub));
771
832
  }
772
833
  }
773
834
  const status = managedStatus(target);
835
+ let restored = 0, refreshed = 0;
774
836
  for (const [sub, destParts, stripTests] of trees) {
775
- cleanReplaceTree(path.join(srcRoot, sub), path.join(target, ...destParts), stripTests);
837
+ const roll = cleanReplaceTree(path.join(srcRoot, sub), path.join(target, ...destParts), stripTests);
838
+ restored += roll.restored;
839
+ refreshed += roll.refreshed;
776
840
  const dest = destParts.join("/");
777
841
  if (status[sub] === "missing") {
778
842
  log(" ✓ restored " + TREE_LABEL[sub].padEnd(8) + "-> " + dest + " (was missing)");
@@ -780,7 +844,8 @@ function reconcile(args, target, srcRoot) {
780
844
  log(" ✓ refreshed " + TREE_LABEL[sub].padEnd(8) + "-> " + dest);
781
845
  }
782
846
  }
783
- return status;
847
+ log(" → " + restored + " restored · " + refreshed + " refreshed");
848
+ return { restored: restored, refreshed: refreshed, trees: status };
784
849
  }
785
850
 
786
851
  // --- global home: an OPT-IN shared install (engine+book+skill) updated for all projects ----
@@ -831,18 +896,20 @@ const GLOBAL_TREES = [
831
896
  ["skill/add", ["skill", "add"], false],
832
897
  ["tooling", ["tooling"], true],
833
898
  ["docs", ["docs"], false],
899
+ ["personas-teacher", ["personas-teacher"], false],
834
900
  ];
835
901
 
836
902
  // Clean-replace the bundled managed layer INTO <home> (canonical mirror), then DEPLOY the
837
903
  // skill to ~/.claude/skills/add. Throws if a dir can't be written (caller -> home_unwritable).
838
904
  // Prechecks ALL sources first (design-for-failure: a corrupt package leaves the home as-is).
839
905
  function reconcileGlobal(home, claudeDir, noSkill) {
840
- for (const [sub] of GLOBAL_TREES) {
906
+ const trees = GLOBAL_TREES.filter(([sub]) => !(OPTIONAL.has(sub) && !fs.existsSync(path.join(PKG_ROOT, sub))));
907
+ for (const [sub] of trees) {
841
908
  if (!fs.existsSync(path.join(PKG_ROOT, sub))) {
842
909
  fail("missing packaged source: " + path.join(PKG_ROOT, sub));
843
910
  }
844
911
  }
845
- for (const [sub, destParts, stripTests] of GLOBAL_TREES) {
912
+ for (const [sub, destParts, stripTests] of trees) {
846
913
  cleanReplaceTree(path.join(PKG_ROOT, sub), path.join(home, ...destParts), stripTests);
847
914
  }
848
915
  if (!noSkill) cleanReplaceTree(path.join(home, "skill", "add"), claudeDir, false);
@@ -851,7 +918,7 @@ function reconcileGlobal(home, claudeDir, noSkill) {
851
918
  // --- global DATA: an OPT-IN per-project user-data snapshot under <home>/data/<key> ----------
852
919
  // Strictly additive; copies ONLY user-data (managed trees + transient excluded), clean-replaced,
853
920
  // one-way (project->home). Mirror of _installer.py (identical key + include/exclude rule).
854
- const DATA_EXCLUDE = ["tooling", "docs", ".update-cache", STAMP_FILE]; // managed trees + meta
921
+ const DATA_EXCLUDE = ["tooling", "docs", ".update-cache", STAMP_FILE, LOCK_FILE]; // managed trees + meta + lock
855
922
 
856
923
  // data_key twin: <sanitized-basename>-<sha1(abspath_utf8)[:12]>. Pure · total · separator-free.
857
924
  function dataKey(projectAbspath) {
@@ -902,6 +969,95 @@ function installGlobalData(chosenTarget) {
902
969
  else log(" (no project data to persist yet — run /add to create one, then re-run --global-data)");
903
970
  }
904
971
 
972
+ function isSymlink(p) {
973
+ try { return fs.lstatSync(p).isSymbolicLink(); } catch (_e) { return false; }
974
+ }
975
+
976
+ // Restore USER-DATA from <home>/data/<key> into <project>/.add — the NON-DESTRUCTIVE inverse of
977
+ // persistData. FILL-GAPS by default (write only ABSENT entries); force overwrites a present entry,
978
+ // writing a <name>.bak FIRST. Copies only isUserData entries; DEREFERENCES symlinks to content
979
+ // (cpSync dereference). true if >=1 restored, false if nothing to restore. Throws on an unwritable
980
+ // dest -> restore_failed. Mirror of _installer.py:_restore_data (identical key + fill-gaps rule).
981
+ function restoreData(home, projectAbspath, force) {
982
+ const src = path.join(home, "data", dataKey(projectAbspath));
983
+ if (!fs.existsSync(src)) return false;
984
+ const entries = fs.readdirSync(src).filter(isUserData).sort();
985
+ if (entries.length === 0) return false;
986
+ const addDir = path.join(projectAbspath, ".add");
987
+ fs.mkdirSync(addDir, { recursive: true });
988
+ let restored = false;
989
+ for (const e of entries) {
990
+ const dest = path.join(addDir, e);
991
+ if (fs.existsSync(dest) || isSymlink(dest)) {
992
+ if (!force) continue; // fill-gaps: never clobber a present entry
993
+ const bak = dest + ".bak";
994
+ if (fs.existsSync(bak) || isSymlink(bak)) fs.rmSync(bak, { recursive: true, force: true });
995
+ fs.renameSync(dest, bak); // back up the original BEFORE replacing
996
+ }
997
+ fs.cpSync(path.join(src, e), dest, { recursive: true, dereference: true }); // deref symlinks
998
+ restored = true;
999
+ }
1000
+ return restored;
1001
+ }
1002
+
1003
+ // init --from-global-data: rehydrate user-data after the per-project drop. Resolves the SAME
1004
+ // realpath the snapshot key uses. The home is verified BEFORE the drop (no_global_home fails fast,
1005
+ // in cmdInit). An unwritable dest -> restore_failed; a missing snapshot is an honest skip.
1006
+ function installGlobalDataRestore(chosenTarget, force) {
1007
+ const home = resolveGlobalHome(process.env);
1008
+ let resolved = chosenTarget;
1009
+ try { resolved = fs.realpathSync(chosenTarget); } catch (_e) { /* fall back to the abspath */ }
1010
+ const snap = path.join(home, "data", dataKey(resolved));
1011
+ let restored;
1012
+ try { restored = restoreData(home, resolved, force); }
1013
+ catch (e) {
1014
+ fail("restore_failed: cannot write restored data into " + path.join(resolved, ".add") +
1015
+ " — " + (e && e.message ? e.message : e));
1016
+ }
1017
+ if (restored) log(" ✓ restored data <- " + snap);
1018
+ else log(" (no snapshot for this project at " + snap + " — nothing restored)");
1019
+ }
1020
+
1021
+ // prune-data: reclaim ORPHANED snapshots under <home>/data. An orphan is a <home>/data/<key>
1022
+ // whose key is owned by NO LIVE registry entry (LIVE = a registered path that still EXISTS on
1023
+ // disk) — so unregistered AND registered-but-vanished are BOTH orphans (the explicit reclaim;
1024
+ // DIVERGES from update --global's keep-vanished). Reads the registry FIRST (corrupt throws,
1025
+ // before any removal). Returns {orphans, removed}. Mirror of _installer.py:_prune_data.
1026
+ function pruneData(home, force) {
1027
+ const reg = readRegistry(home); // corrupt -> throw (LOUD, zero removal)
1028
+ const live = new Set(reg.filter((p) => fs.existsSync(p)).map(dataKey));
1029
+ const dataDir = path.join(home, "data");
1030
+ if (!fs.existsSync(dataDir)) return { orphans: [], removed: [] };
1031
+ const orphans = fs.readdirSync(dataDir).filter((name) => {
1032
+ try { return fs.statSync(path.join(dataDir, name)).isDirectory() && !live.has(name); }
1033
+ catch (_e) { return false; }
1034
+ }).sort();
1035
+ const removed = [];
1036
+ if (force) {
1037
+ for (const key of orphans) {
1038
+ fs.rmSync(path.join(dataDir, key), { recursive: true, force: true });
1039
+ removed.push(key);
1040
+ }
1041
+ }
1042
+ return { orphans: orphans, removed: removed };
1043
+ }
1044
+
1045
+ // prune-data command: dry-run lists orphans (removes nothing); --force deletes. no_global_home /
1046
+ // registry_corrupt = fail-closed (LOUD, nothing removed). Mirror of pip _installer.prune_data.
1047
+ function cmdPruneData(args) {
1048
+ const home = resolveGlobalHome(process.env);
1049
+ if (!fs.existsSync(path.join(home, STAMP_FILE))) {
1050
+ fail("no global ADD install at " + home + " (.add-version not found) — nothing to prune");
1051
+ }
1052
+ let result;
1053
+ try { result = pruneData(home, args.force); }
1054
+ catch (_e) { fail("global registry " + registryPath(home) + " is corrupt — fix or delete it; not pruning"); }
1055
+ if (result.orphans.length === 0) { log(" no orphaned snapshots — nothing to prune"); return; }
1056
+ if (args.force) { log(" ✓ " + result.removed.length + " removed"); return; }
1057
+ for (const key of result.orphans) log(" orphan: " + key);
1058
+ log(" " + result.orphans.length + " orphan(s); re-run with --force to remove");
1059
+ }
1060
+
905
1061
  // init --global: install the managed layer ONCE to the shared home + register this project,
906
1062
  // fail-closed BEFORE the per-project drop. Returns the resolved target for the normal drop.
907
1063
  function installGlobal(args, chosenTarget) {
@@ -924,34 +1080,83 @@ function installGlobal(args, chosenTarget) {
924
1080
 
925
1081
  // update --global: refresh the home mirror + skill, then propagate to every registered+existing
926
1082
  // project via reconcile(.., home); prune vanished projects (warn) + rewrite the registry atomically.
1083
+ // True iff path.normalize(p) is an EXISTING ADD project (a dir containing .add/). The is-.add/
1084
+ // backstop: a managed-file reconcile NEVER lands in a dir without a .add/ marker. Absoluteness is
1085
+ // the SEPARATE LOUD gate in cmdUpdateGlobal (a relative path is the traversal vector). pip twin:
1086
+ // _installer.py:_valid_registry_path.
1087
+ function validRegistryPath(p) {
1088
+ const np = path.normalize(String(p));
1089
+ try { return fs.statSync(np).isDirectory() && fs.statSync(path.join(np, ".add")).isDirectory(); }
1090
+ catch (_e) { return false; }
1091
+ }
1092
+
1093
+ // Serialize `update --global` with an EXCLUSIVE lockfile (O_CREAT|O_EXCL via the "wx" flag).
1094
+ // Already held -> update_in_progress (fail-fast). Released on process exit (normal completion OR
1095
+ // fail()'s process.exit), so it never outlives the run; a hard crash may leave a stale lock to
1096
+ // remove by hand. pip twin: _installer.py:_update_lock (the SAME O_EXCL lockfile, cross-compatible).
1097
+ function acquireUpdateLock(home) {
1098
+ fs.mkdirSync(home, { recursive: true });
1099
+ const lockPath = path.join(home, LOCK_FILE);
1100
+ let fd;
1101
+ try { fd = fs.openSync(lockPath, "wx"); }
1102
+ catch (e) {
1103
+ if (e && e.code === "EEXIST") {
1104
+ fail("update_in_progress: another `update --global` is already running — retry shortly " +
1105
+ "(remove " + lockPath + " if it is stale)");
1106
+ }
1107
+ throw e;
1108
+ }
1109
+ const release = () => {
1110
+ try { fs.closeSync(fd); } catch (_e) {}
1111
+ try { fs.unlinkSync(lockPath); } catch (_e) {}
1112
+ };
1113
+ process.on("exit", release); // covers normal completion AND fail()'s process.exit
1114
+ return release;
1115
+ }
1116
+
927
1117
  function cmdUpdateGlobal(args) {
928
1118
  const home = resolveGlobalHome(process.env);
929
1119
  const claudeDir = claudeSkillsDir(process.env);
930
1120
  if (!fs.existsSync(path.join(home, STAMP_FILE))) {
931
- fail("no global ADD install at " + home + " (.add-version not found) — run `init --global` first");
1121
+ fail("no_global_home: no global ADD install at " + home + " (.add-version not found) — run `init --global` first");
932
1122
  }
1123
+ acquireUpdateLock(home); // exclusive; EEXIST -> update_in_progress; released on process exit
933
1124
  // Read the registry BEFORE refreshing the home — a corrupt registry fails closed with ZERO
934
1125
  // writes (never a silent empty-list no-op), leaving the file for the user to fix or delete.
935
1126
  let reg;
936
1127
  try { reg = readRegistry(home); }
937
- catch (_e) { fail("global registry " + registryPath(home) + " is corrupt — fix or delete it; not propagating"); }
1128
+ catch (_e) { fail("registry_corrupt: global registry " + registryPath(home) + " is corrupt — fix or delete it; not propagating"); }
1129
+ // PRE-SCAN (security): a NON-ABSOLUTE entry is the traversal vector -> abort with zero mutations.
1130
+ for (const p of reg) {
1131
+ if (!path.isAbsolute(p)) {
1132
+ fail("unsafe_registry_path: registered project '" + p + "' is not an absolute path — fix or remove it in " + registryPath(home) + "; not propagating");
1133
+ }
1134
+ }
938
1135
  try { reconcileGlobal(home, claudeDir, args.noSkill); }
939
1136
  catch (e) { fail("cannot write global home " + home + " — " + (e && e.message ? e.message : e)); }
940
1137
  const version = pkgVersion();
941
1138
  writeStamp(home, version, "global");
942
1139
  const kept = [];
943
- let pruned = 0;
1140
+ let pruned = 0, dropped = 0;
944
1141
  for (const p of reg) {
945
- if (!fs.existsSync(p)) { log(" ⚠ registered project " + p + " not found — pruning"); pruned++; continue; }
946
- reconcile(args, p, home); // standard MANAGED map, sourced from the home mirror
1142
+ const np = path.normalize(p); // absolute (pre-scan) -> lexically normalized
1143
+ if (!fs.existsSync(np)) { log(" ⚠ registered project " + np + " not found — pruning"); pruned++; continue; }
1144
+ if (!validRegistryPath(np)) { // exists but no .add/ -> NEVER reconcile managed files into it
1145
+ log(" ⚠ registered path " + np + " is not an ADD project (no .add/) — dropping"); dropped++; continue;
1146
+ }
1147
+ reconcile(args, np, home); // standard MANAGED map, sourced from the home mirror
1148
+ seedGitignore(np); // keep .add/.gitignore current too (parity: _installer.py)
947
1149
  // re-persist an opted-in project (one that already has a snapshot); a vanished
948
1150
  // project's snapshot is KEPT above (the backup outlives the dir).
949
- if (fs.existsSync(path.join(home, "data", dataKey(p)))) persistData(home, p);
950
- kept.push(p);
1151
+ if (fs.existsSync(path.join(home, "data", dataKey(np)))) persistData(home, np);
1152
+ kept.push(np); // store the NORMALIZED path (heals a bent legit entry)
951
1153
  }
952
1154
  writeRegistry(home, kept);
1155
+ const bits = [];
1156
+ if (pruned) bits.push(pruned + " pruned");
1157
+ if (dropped) bits.push(dropped + " dropped");
953
1158
  log("ADD " + version + " · global home + " + kept.length + " project(s) reconciled" +
954
- (pruned ? " (" + pruned + " pruned)" : "") + ".");
1159
+ (bits.length ? " (" + bits.join(", ") + ")" : "") + ".");
955
1160
  }
956
1161
 
957
1162
  function cmdUpdate(args) {
@@ -974,7 +1179,10 @@ function cmdUpdate(args) {
974
1179
  // same-version no-op ONLY when nothing is missing — a missing managed tree HEALS
975
1180
  // even at the current version (heal-reconcile).
976
1181
  const status = managedStatus(target);
977
- const missing = MANAGED.some(([sub]) => status[sub] === "missing");
1182
+ // An optional tree absent from BOTH the package and the project can't be healed, so it
1183
+ // never counts as "missing" — otherwise a same-version update would never reach the no-op.
1184
+ const missing = MANAGED.some(([sub]) => status[sub] === "missing"
1185
+ && !(OPTIONAL.has(sub) && !fs.existsSync(path.join(PKG_ROOT, sub))));
978
1186
  if (cur === version && !args.force && !missing) {
979
1187
  log("ADD already at " + version + " — nothing to update (use --force to re-materialize).");
980
1188
  return;
@@ -984,12 +1192,13 @@ function cmdUpdate(args) {
984
1192
  if (fs.existsSync(stateFile)) {
985
1193
  fs.copyFileSync(stateFile, path.join(addDir, "pre-update-state.bak.json"));
986
1194
  }
987
- reconcile(args, target);
1195
+ const roll = reconcile(args, target);
988
1196
  seedSoulMd(target); // pip parity: re-seed a missing user-owned SOUL.md (never clobber)
989
1197
  seedGitignore(target); // pip parity: seed/append-if-absent the engine-transient ignore lines
990
1198
  writeStamp(addDir, version);
991
1199
  log("ADD updated " + (cur || "(unstamped)") + " -> " + version +
992
- " · managed layer reconciled · your project state untouched.");
1200
+ " · managed layer reconciled (" + roll.restored + " restored · " + roll.refreshed +
1201
+ " refreshed) · your project state untouched.");
993
1202
  }
994
1203
 
995
1204
  async function main() {
@@ -1003,16 +1212,21 @@ async function main() {
1003
1212
  case "update":
1004
1213
  cmdUpdate(args);
1005
1214
  break;
1215
+ case "prune-data":
1216
+ cmdPruneData(args);
1217
+ break;
1006
1218
  case "help":
1007
1219
  case "--help":
1008
- log("usage: npx @pilotspace/add <init|update> [targetDir] [--force] [--check] [--no-skill] [--global] [--yes|--non-interactive]");
1220
+ log("usage: npx @pilotspace/add <init|update|prune-data> [targetDir] [--force] [--check] [--no-skill] [--global] [--yes|--non-interactive]");
1009
1221
  log(" init install the ADD skill + tooling + book into a project");
1010
1222
  log(" (--no-skill drops the engine + book only — used by the Claude Code plugin)");
1011
1223
  log(" (--global ALSO installs to a shared home [ADD_HOME|XDG_DATA_HOME/add|~/.add] + registers the project)");
1012
1224
  log(" (--global-data implies --global + persists this project's user-data under <home>/data/<key>)");
1225
+ log(" (--from-global-data rehydrates this project's user-data FROM the shared home on a fresh clone)");
1013
1226
  log(" (interactive in a real terminal; --yes / --non-interactive force the plain path)");
1014
1227
  log(" update re-materialize skill/tooling/docs to this package version (preserves your state)");
1015
1228
  log(" (--global refreshes the shared home + propagates to every registered project)");
1229
+ log(" prune-data remove orphaned per-project snapshots from the shared home (dry-run; --force deletes)");
1016
1230
  break;
1017
1231
  default:
1018
1232
  fail("unknown command '" + cmd + "'. Try: npx @pilotspace/add init");
@@ -93,7 +93,7 @@ The flow runs in two directions under two rules that never conflict. **Backward
93
93
  | 6 Verify | own the residue (security · concurrency · architecture); approve when `conservative` | gather evidence; **auto-PASS on complete evidence** under `autonomy: auto` |
94
94
  | 7 Observe | read the signal; consolidate confirmed deltas into PROJECT.md | run behind a flag; emit lessons learned |
95
95
 
96
- **What the human sees when it is their turn — the decision arc.** Whenever the flow stops for the human — the baseline approval that ends setup, the contract-freeze decision point and an escalated verify gate within each task, and the wider decision points of the loop (intake · scope · milestone close · stage graduation) — the AI opens its report with the **decision arc**: three engine-sourced lines — `goal:` the milestone goal the work serves · `done:` the proven progress toward it · `plan:` what comes next. The arc renders first, above the report's summary, so the human confirms with sight of the whole trajectory rather than a local snapshot. Within that report the AI also presents the **DECISION** itself as a **guided choice** — one highlighted **recommended pick** (`▶ … (recommended)`) plus its real, described alternatives — so the human chooses with the recommendation and each option's consequence in view rather than a bare next-step line. Both are presentation only — they never add a gate or change an outcome, and the guided choice fires at human gates only. See [Appendix C](./appendix-c-glossary.md) and the `add` skill's `report-template.md` for the convention itself.
96
+ **What the human sees when it is their turn — the decision banner and arc.** Whenever the flow stops for the human — the baseline approval that ends setup, the contract-freeze decision point and an escalated verify gate within each task, and the wider decision points of the loop (intake · scope · milestone close · stage graduation) — the AI opens its report with a **banner** (`PLAN · <title> · <gate> → APPROVE?` plus a file-path line) and the **decision arc**: three engine-sourced lines — `goal:` the milestone goal the work serves · `done:` the proven progress toward it · `plan:` what comes next. The arc renders above the report's summary, so the human confirms with sight of the whole trajectory rather than a local snapshot. Within that report the AI also presents **APPROVE** itself as a **guided choice** — one highlighted **recommended pick** (`▶ … (recommended)`) plus its real, described alternatives — so the human chooses with the recommendation and each option's consequence in view rather than a bare next-step line. Both are presentation only — they never add a gate or change an outcome, and the guided choice fires at human gates only. See [Appendix C](./appendix-c-glossary.md) and the `add` skill's `report-template.md` for the convention itself.
97
97
 
98
98
  ## What survives, and what is disposable
99
99
 
@@ -35,6 +35,10 @@ Four parts, kept short:
35
35
 
36
36
  Naming the errors matters. "Reject bad amounts" is an instruction to guess; `amount <= 0 -> "amount_invalid"` is a rule that produces a testable scenario and a defined contract response.
37
37
 
38
+ ### Rule IDs (optional — opt-in by usage)
39
+
40
+ Give each Must a stable `M<n>:` ID; a Reject's own error code already IS its ID (referenced later as `R:<error_code>`). Once a task uses a tag anywhere in §2 SCENARIOS or §4 TESTS, `add.py check` starts asking that every Must/Reject be covered by a scenario tag or a test's `covers:` line — a task that never tags anything is never retro-flagged. See the template's own inline example for the exact grammar.
41
+
38
42
  ## Template
39
43
 
40
44
  ```
@@ -58,6 +58,10 @@ Scenario: not my account
58
58
 
59
59
  The `And no balance changes` line is doing real work: it specifies that a rejected transfer must leave the world untouched — a property the AI could easily violate by deducting before checking.
60
60
 
61
+ ### Tagging back to a rule ID (optional — opt-in by usage)
62
+
63
+ If §1's Musts and Rejects carry stable IDs (`M1:`, and a Reject's own error code as `R:<error_code>`), tag each `Scenario:` line with the ID(s) it covers, e.g. `# M1, R:amount_invalid`. Once a task tags even one scenario this way, `add.py check` starts confirming every §1 ID is covered by a tag here or a `covers:` line in §4 TESTS — a task that never tags anything is left alone. See the template's own inline example for the exact grammar.
64
+
61
65
  ## Cover the edge cases
62
66
 
63
67
  The transfer above is one domain; the same gaps recur in every domain — an HR leave request, a marketing campaign send, a checkout. Beyond the spec's "Reject" rules, sweep the recurring gaps and add a scenario for each that applies (or rule it out on purpose): boundary, duplicate/idempotent, ownership, stale/out-of-order, partial failure, concurrency, malformed input, limits/volume.
@@ -50,6 +50,10 @@ def test_not_my_account():
50
50
 
51
51
  Run this now, with no implementation: all three fail. That is the correct, honest starting point for the build.
52
52
 
53
+ ### Declaring which rule a test covers (optional — opt-in by usage)
54
+
55
+ If §1's Musts and Rejects carry stable IDs, add a trailing `covers: M1, R:amount_invalid` to a test-plan line to declare which ID(s) that test satisfies. Once any test in a task declares a `covers:` line (or any §2 scenario carries a tag), `add.py check` confirms every §1 ID is covered by a tag or a `covers:` line somewhere — a task that never uses either is left alone. See the template's own inline example for the exact grammar.
56
+
53
57
  ## The AI's role here
54
58
 
55
59
  The AI generates the test suite from the scenarios and contract. Your job is to confirm two things it cannot judge for itself: that each test asserts *behavior* rather than internal detail, and that none of them pass by accident before code exists. See `playbook/4_tests.md` in [Appendix B](./appendix-b-prompts.md).
@@ -24,11 +24,11 @@ guess. You name them in `.add/components.toml`:
24
24
  ```toml
25
25
  [component.gateway]
26
26
  root = "apps/gateway"
27
- green-bar = "pytest + pyright"
27
+ green_bar = "pytest + pyright"
28
28
 
29
29
  [component.dashboard]
30
30
  root = "apps/web"
31
- green-bar = "vitest + a11y"
31
+ green_bar = "vitest + a11y"
32
32
  ```
33
33
 
34
34
  Each component owns a **root** (the source subtree it governs) and a
@@ -49,6 +49,13 @@ the right suite for the bound component; the gate checks the **right bar was
49
49
  cited** in the evidence. Two tasks, one milestone, two green bars — each held to
50
50
  its own.
51
51
 
52
+ At the gate the engine also **surfaces** the bound component's `verify` command —
53
+ the literal suite to run (e.g. `pytest -q`) — beside the expected green-bar, and
54
+ records it in the §6 gate record so the ledger shows which suite backed the gate.
55
+ It prints *what* to run; it never runs it (NO-EXEC). In the fast lane the same
56
+ `component:` affordance is available, so a small task in a monorepo can bind a
57
+ component and get its bar and `verify` surfaced too.
58
+
52
59
  ## Freeze a contract between components
53
60
 
54
61
  When one component produces an interface another consumes, that boundary needs a
@@ -83,6 +90,15 @@ frontend proceeds and pins it. The slice is **ordered by the frozen contract**,
83
90
  all inside one milestone — the FE stays downstream of the BE endpoint, not split
84
91
  into a later milestone.
85
92
 
93
+ The hold checks more than existence. Even once a snapshot exists, if a live
94
+ producer task has *re-opened or drifted* its §3 — the snapshot no longer matches
95
+ a frozen producer — the consumer is held `producer_contract_stale` rather than
96
+ pinning a shape that is mid-change (the freeze-recency guard). Outside the hold,
97
+ `add.py check` surfaces the softer `contract_producer_stale` (a live producer
98
+ drifted past a pinned consumer) and `contract_snapshot_hashless` (a snapshot
99
+ carrying no hash to verify against) as **never-red warnings** — measured and
100
+ reported, never blocking.
101
+
86
102
  ## Across repositories: federation
87
103
 
88
104
  Components in separate repositories work the same way; only the
@@ -99,9 +115,11 @@ pin = "v1" # optional — the version this repo expects
99
115
  matching id, a hash, and — if `pin` is set — a matching version), and lands a
100
116
  **byte-for-byte copy** at the local `.add/contracts/gateway-api.json`. From
101
117
  there, the consuming repo's task holds and pins exactly as in a monorepo. The
102
- pull is **fail-loud by design**: an unknown id, an unreadable source, an invalid
103
- snapshot, or a version mismatch each HARD-STOPS and lands nothing federation
104
- never builds an FE against a guessed or stale endpoint. The producer's snapshot
118
+ pull is **fail-loud by design**: an unknown id, an unreadable source, a `source`
119
+ that **escapes the consumer repo's allowlist** (`federation_source_escapes`the
120
+ path is confined to a sibling of the repo root, so a `../../etc`-style traversal
121
+ lands nothing), an invalid snapshot, or a version mismatch each HARD-STOPS — federation
122
+ never builds an FE against a guessed, out-of-tree, or stale endpoint. The producer's snapshot
105
123
  is the published artifact; "publishing" is committing that file in the producer
106
124
  repo. Each repo keeps its own git-native `state.json`; federation transports only
107
125
  the immutable frozen shape, never shared mutable state.
@@ -0,0 +1,80 @@
1
+ # 18 · Personas — the project-fit learning loop
2
+
3
+ [← 17 Components](./17-components.md) · [Contents](./README.md) · Next: [Appendix A Templates →](./appendix-a-templates.md)
4
+
5
+ ---
6
+
7
+ A **persona** is a project-fit requirements lens the agent adopts so its work matches *this*
8
+ codebase's standards instead of a generic default. It is not a chat costume: a persona is a small,
9
+ versioned file under `.add/personas/<slug>.md` with three machine-readable parts —
10
+
11
+ - **Identity** — the stance the worker takes (e.g. *a payments engineer who treats money as exact*).
12
+ - **Critical Rules** — the constraints that must hold (the non-negotiables for this domain).
13
+ - **Success Metrics** — the measurable done-bar (what "good" looks like, in numbers where it can).
14
+
15
+ The **persona loop** is how those files come to exist, improve, and get used: **seed → grow →
16
+ apply**. The loop is opt-in and additive — a project with no personas behaves exactly as before.
17
+
18
+ ## Where personas come from — the teacher
19
+
20
+ ADD does not invent personas from nothing. It learns them from a **teacher**: a corpus of worked
21
+ agent definitions that ADD ships as a vendored local library at `.add/personas-teacher/`.
22
+ The teacher is read **off-build**, by the AI, while drafting a persona — it is a *source of ideas*,
23
+ **never a runtime dependency**: nothing in the engine imports it, fetches it, or needs it present to
24
+ run. You distil a teacher entry down to the three-part shape above and commit the result; from then
25
+ on the project owns that persona outright.
26
+
27
+ ## Seed — at setup
28
+
29
+ The first personas are **seeded** during project setup, from the foundation (`PROJECT.md`'s domain
30
+ and standards). Setup proposes a starter persona or two that fit the project's domain; the human
31
+ confirms. Seeding writes `.add/personas/<slug>.md` and nothing else — no behaviour changes until a
32
+ task actually applies one.
33
+
34
+ ## Grow — observe → delta → consolidate
35
+
36
+ Personas are **living documents**: they improve through the same loop the foundation uses. In a
37
+ task's **observe** phase the AI emits a **persona delta** — a one-line, tagged proposal to add or
38
+ sharpen a critical-rule or success-metric, written `open` with evidence (just like a lesson learned).
39
+ At a retrospective the human **consolidates** confirmed deltas into the persona file (the same
40
+ `fold` step the foundation uses), bumping it forward. The consolidation is judgment-free
41
+ transcription: the engine routes a confirmed delta into the named
42
+ persona's `## Critical Rules` or `## Success Metrics` section and **never clobbers** existing content.
43
+ So a persona gets *more* accurate every milestone instead of drifting.
44
+
45
+ ## Apply — three surfaces
46
+
47
+ A seeded, grown persona earns its keep when work adopts it. Three surfaces apply a persona, and all
48
+ three treat it the same way — as **advice**, never authority:
49
+
50
+ - **UDD (design).** At the design-definition loop a persona frames the requirements lens: which
51
+ rules and metrics a UI/UX slice must satisfy for this project's users.
52
+ - **advisor / streams (delegation).** When you delegate a piece of your plan to a subagent, its
53
+ `<persona>` block **selects** the best-fit `.add/personas/<slug>.md` and the returned verdict
54
+ **records** which persona did the work. The independent earned-green refute-read selects a
55
+ **Code-Reviewer** persona whose findings carry severity markers — 🔴 blocker · 🟡 concern · 💭 note.
56
+ The same canonical persona body rides every runner (Claude, Cursor, Copilot, Codex), so a
57
+ **cross-runner subagent** behaves identically wherever it is spawned.
58
+ - **build (overlay).** While building, the orchestrating agent may load the active persona as a
59
+ domain identity **overlay** layered on `SOUL.md`: SOUL is the voice and trust rules; the persona is
60
+ the domain stance. The overlay **never rewrites** SOUL.md (it is human-owned) and never overrides a
61
+ trust rule.
62
+
63
+ ## The non-negotiable — a persona never lowers a gate
64
+
65
+ A persona changes *how carefully* the work is done; it never changes *what passes*. This is the one
66
+ hard rule across every surface:
67
+
68
+ - A **security** finding is always a **HARD-STOP**, whatever persona was adopted.
69
+ - High-risk scope still escalates to the human; a stronger persona never buys back a gate.
70
+ - The engine stays **NO-EXEC** throughout: it never spawns a subagent, never runs a persona, never
71
+ reads one on the build path. Selecting, loading, and applying a persona is the orchestrating
72
+ agent's judgment — the engine only records what was used (for the audit trail) and measures that
73
+ the record is present; it never auto-passes on a persona's say-so.
74
+
75
+ In short: a persona makes the agent *fit the project*. Direction, freezing, evidence, and the gate
76
+ are exactly as strict as they were before — the loop adds expertise, not permission.
77
+
78
+ ---
79
+
80
+ [← 17 Components](./17-components.md) · [Contents](./README.md) · Next: [Appendix A Templates →](./appendix-a-templates.md)
Binary file