@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.
- package/CHANGELOG.md +189 -1
- package/THIRD_PARTY_NOTICES.md +41 -0
- package/bin/cli.js +240 -26
- package/docs/02-the-flow.md +1 -1
- package/docs/03-step-1-specify.md +4 -0
- package/docs/04-step-2-scenarios.md +4 -0
- package/docs/06-step-4-tests.md +4 -0
- package/docs/17-components.md +23 -5
- package/docs/18-personas.md +80 -0
- package/docs/add-milestone-task-lifecycle.png +0 -0
- package/docs/add-task-growth-wheel.png +0 -0
- package/docs/appendix-c-glossary.md +8 -4
- package/docs/appendix-d-worked-example.md +79 -0
- package/package.json +3 -1
- package/personas-teacher/LICENSE +21 -0
- package/personas-teacher/README.md +1067 -0
- package/personas-teacher/VENDOR.md +13 -0
- package/personas-teacher/academic/academic-anthropologist.md +125 -0
- package/personas-teacher/academic/academic-geographer.md +127 -0
- package/personas-teacher/academic/academic-historian.md +123 -0
- package/personas-teacher/academic/academic-narratologist.md +118 -0
- package/personas-teacher/academic/academic-psychologist.md +118 -0
- package/personas-teacher/design/design-brand-guardian.md +322 -0
- package/personas-teacher/design/design-image-prompt-engineer.md +236 -0
- package/personas-teacher/design/design-inclusive-visuals-specialist.md +71 -0
- package/personas-teacher/design/design-persona-walkthrough.md +272 -0
- package/personas-teacher/design/design-ui-designer.md +383 -0
- package/personas-teacher/design/design-ux-architect.md +469 -0
- package/personas-teacher/design/design-ux-researcher.md +329 -0
- package/personas-teacher/design/design-visual-storyteller.md +149 -0
- package/personas-teacher/design/design-whimsy-injector.md +438 -0
- package/personas-teacher/divisions.json +21 -0
- package/personas-teacher/engineering/engineering-ai-data-remediation-engineer.md +211 -0
- package/personas-teacher/engineering/engineering-ai-engineer.md +146 -0
- package/personas-teacher/engineering/engineering-autonomous-optimization-architect.md +107 -0
- package/personas-teacher/engineering/engineering-backend-architect.md +236 -0
- package/personas-teacher/engineering/engineering-cms-developer.md +536 -0
- package/personas-teacher/engineering/engineering-code-reviewer.md +76 -0
- package/personas-teacher/engineering/engineering-codebase-onboarding-engineer.md +173 -0
- package/personas-teacher/engineering/engineering-data-engineer.md +306 -0
- package/personas-teacher/engineering/engineering-database-optimizer.md +176 -0
- package/personas-teacher/engineering/engineering-devops-automator.md +376 -0
- package/personas-teacher/engineering/engineering-drupal-shopping-cart.md +360 -0
- package/personas-teacher/engineering/engineering-email-intelligence-engineer.md +353 -0
- package/personas-teacher/engineering/engineering-embedded-firmware-engineer.md +173 -0
- package/personas-teacher/engineering/engineering-feishu-integration-developer.md +598 -0
- package/personas-teacher/engineering/engineering-filament-optimization-specialist.md +283 -0
- package/personas-teacher/engineering/engineering-frontend-developer.md +225 -0
- package/personas-teacher/engineering/engineering-git-workflow-master.md +84 -0
- package/personas-teacher/engineering/engineering-incident-response-commander.md +444 -0
- package/personas-teacher/engineering/engineering-it-service-manager.md +561 -0
- package/personas-teacher/engineering/engineering-minimal-change-engineer.md +207 -0
- package/personas-teacher/engineering/engineering-mobile-app-builder.md +493 -0
- package/personas-teacher/engineering/engineering-multi-agent-systems-architect.md +600 -0
- package/personas-teacher/engineering/engineering-orgscript-engineer.md +113 -0
- package/personas-teacher/engineering/engineering-prompt-engineer.md +202 -0
- package/personas-teacher/engineering/engineering-rapid-prototyper.md +462 -0
- package/personas-teacher/engineering/engineering-senior-developer.md +176 -0
- package/personas-teacher/engineering/engineering-software-architect.md +112 -0
- package/personas-teacher/engineering/engineering-solidity-smart-contract-engineer.md +522 -0
- package/personas-teacher/engineering/engineering-sre.md +90 -0
- package/personas-teacher/engineering/engineering-technical-writer.md +393 -0
- package/personas-teacher/engineering/engineering-voice-ai-integration-engineer.md +561 -0
- package/personas-teacher/engineering/engineering-wechat-mini-program-developer.md +350 -0
- package/personas-teacher/engineering/engineering-wordpress-shopping-cart.md +346 -0
- package/personas-teacher/examples/README.md +48 -0
- package/personas-teacher/examples/nexus-spatial-discovery.md +852 -0
- package/personas-teacher/examples/workflow-book-chapter.md +55 -0
- package/personas-teacher/examples/workflow-landing-page.md +119 -0
- package/personas-teacher/examples/workflow-startup-mvp.md +155 -0
- package/personas-teacher/examples/workflow-with-memory.md +238 -0
- package/personas-teacher/finance/finance-bookkeeper-controller.md +260 -0
- package/personas-teacher/finance/finance-financial-analyst.md +234 -0
- package/personas-teacher/finance/finance-fpa-analyst.md +263 -0
- package/personas-teacher/finance/finance-investment-researcher.md +272 -0
- package/personas-teacher/finance/finance-tax-strategist.md +239 -0
- package/personas-teacher/game-development/blender/blender-addon-engineer.md +234 -0
- package/personas-teacher/game-development/game-audio-engineer.md +264 -0
- package/personas-teacher/game-development/game-designer.md +167 -0
- package/personas-teacher/game-development/godot/godot-gameplay-scripter.md +334 -0
- package/personas-teacher/game-development/godot/godot-multiplayer-engineer.md +297 -0
- package/personas-teacher/game-development/godot/godot-shader-developer.md +266 -0
- package/personas-teacher/game-development/level-designer.md +208 -0
- package/personas-teacher/game-development/narrative-designer.md +243 -0
- package/personas-teacher/game-development/roblox-studio/roblox-avatar-creator.md +297 -0
- package/personas-teacher/game-development/roblox-studio/roblox-experience-designer.md +305 -0
- package/personas-teacher/game-development/roblox-studio/roblox-systems-scripter.md +325 -0
- package/personas-teacher/game-development/technical-artist.md +229 -0
- package/personas-teacher/game-development/unity/unity-architect.md +271 -0
- package/personas-teacher/game-development/unity/unity-editor-tool-developer.md +310 -0
- package/personas-teacher/game-development/unity/unity-multiplayer-engineer.md +321 -0
- package/personas-teacher/game-development/unity/unity-shader-graph-artist.md +269 -0
- package/personas-teacher/game-development/unreal-engine/unreal-multiplayer-architect.md +313 -0
- package/personas-teacher/game-development/unreal-engine/unreal-systems-engineer.md +310 -0
- package/personas-teacher/game-development/unreal-engine/unreal-technical-artist.md +256 -0
- package/personas-teacher/game-development/unreal-engine/unreal-world-builder.md +273 -0
- package/personas-teacher/gis/gis-3d-scene-developer.md +111 -0
- package/personas-teacher/gis/gis-analyst.md +91 -0
- package/personas-teacher/gis/gis-bim-specialist.md +108 -0
- package/personas-teacher/gis/gis-cartography-designer.md +150 -0
- package/personas-teacher/gis/gis-drone-reality-mapping.md +120 -0
- package/personas-teacher/gis/gis-geoai-ml-engineer.md +105 -0
- package/personas-teacher/gis/gis-geoprocessing-specialist.md +97 -0
- package/personas-teacher/gis/gis-qa-engineer.md +133 -0
- package/personas-teacher/gis/gis-solution-engineer.md +101 -0
- package/personas-teacher/gis/gis-spatial-data-engineer.md +97 -0
- package/personas-teacher/gis/gis-spatial-data-scientist.md +111 -0
- package/personas-teacher/gis/gis-technical-consultant.md +86 -0
- package/personas-teacher/gis/gis-web-gis-developer.md +108 -0
- package/personas-teacher/marketing/marketing-aeo-foundations.md +264 -0
- package/personas-teacher/marketing/marketing-agentic-search-optimizer.md +311 -0
- package/personas-teacher/marketing/marketing-ai-citation-strategist.md +170 -0
- package/personas-teacher/marketing/marketing-app-store-optimizer.md +321 -0
- package/personas-teacher/marketing/marketing-baidu-seo-specialist.md +226 -0
- package/personas-teacher/marketing/marketing-bilibili-content-strategist.md +199 -0
- package/personas-teacher/marketing/marketing-book-co-author.md +110 -0
- package/personas-teacher/marketing/marketing-carousel-growth-engine.md +199 -0
- package/personas-teacher/marketing/marketing-china-ecommerce-operator.md +283 -0
- package/personas-teacher/marketing/marketing-china-market-localization-strategist.md +283 -0
- package/personas-teacher/marketing/marketing-content-creator.md +54 -0
- package/personas-teacher/marketing/marketing-cross-border-ecommerce.md +259 -0
- package/personas-teacher/marketing/marketing-douyin-strategist.md +149 -0
- package/personas-teacher/marketing/marketing-email-strategist.md +249 -0
- package/personas-teacher/marketing/marketing-global-podcast-strategist.md +206 -0
- package/personas-teacher/marketing/marketing-growth-hacker.md +54 -0
- package/personas-teacher/marketing/marketing-instagram-curator.md +113 -0
- package/personas-teacher/marketing/marketing-kuaishou-strategist.md +223 -0
- package/personas-teacher/marketing/marketing-linkedin-content-creator.md +214 -0
- package/personas-teacher/marketing/marketing-livestream-commerce-coach.md +305 -0
- package/personas-teacher/marketing/marketing-multi-platform-publisher.md +217 -0
- package/personas-teacher/marketing/marketing-podcast-strategist.md +277 -0
- package/personas-teacher/marketing/marketing-pr-communications-manager.md +473 -0
- package/personas-teacher/marketing/marketing-private-domain-operator.md +308 -0
- package/personas-teacher/marketing/marketing-reddit-community-builder.md +123 -0
- package/personas-teacher/marketing/marketing-seo-specialist.md +321 -0
- package/personas-teacher/marketing/marketing-short-video-editing-coach.md +412 -0
- package/personas-teacher/marketing/marketing-social-media-strategist.md +125 -0
- package/personas-teacher/marketing/marketing-tiktok-strategist.md +125 -0
- package/personas-teacher/marketing/marketing-twitter-engager.md +126 -0
- package/personas-teacher/marketing/marketing-video-optimization-specialist.md +119 -0
- package/personas-teacher/marketing/marketing-wechat-official-account.md +145 -0
- package/personas-teacher/marketing/marketing-weibo-strategist.md +240 -0
- package/personas-teacher/marketing/marketing-x-twitter-intelligence-analyst.md +161 -0
- package/personas-teacher/marketing/marketing-xiaohongshu-specialist.md +138 -0
- package/personas-teacher/marketing/marketing-zhihu-strategist.md +162 -0
- package/personas-teacher/paid-media/paid-media-auditor.md +71 -0
- package/personas-teacher/paid-media/paid-media-creative-strategist.md +71 -0
- package/personas-teacher/paid-media/paid-media-paid-social-strategist.md +71 -0
- package/personas-teacher/paid-media/paid-media-ppc-strategist.md +71 -0
- package/personas-teacher/paid-media/paid-media-programmatic-buyer.md +71 -0
- package/personas-teacher/paid-media/paid-media-search-query-analyst.md +71 -0
- package/personas-teacher/paid-media/paid-media-tracking-specialist.md +71 -0
- package/personas-teacher/product/product-behavioral-nudge-engine.md +80 -0
- package/personas-teacher/product/product-feedback-synthesizer.md +119 -0
- package/personas-teacher/product/product-manager.md +469 -0
- package/personas-teacher/product/product-sprint-prioritizer.md +154 -0
- package/personas-teacher/product/product-trend-researcher.md +159 -0
- package/personas-teacher/project-management/project-management-experiment-tracker.md +198 -0
- package/personas-teacher/project-management/project-management-jira-workflow-steward.md +230 -0
- package/personas-teacher/project-management/project-management-meeting-notes-specialist.md +95 -0
- package/personas-teacher/project-management/project-management-project-shepherd.md +194 -0
- package/personas-teacher/project-management/project-management-studio-operations.md +200 -0
- package/personas-teacher/project-management/project-management-studio-producer.md +203 -0
- package/personas-teacher/project-management/project-manager-senior.md +135 -0
- package/personas-teacher/sales/sales-account-strategist.md +227 -0
- package/personas-teacher/sales/sales-coach.md +271 -0
- package/personas-teacher/sales/sales-deal-strategist.md +180 -0
- package/personas-teacher/sales/sales-discovery-coach.md +225 -0
- package/personas-teacher/sales/sales-engineer.md +182 -0
- package/personas-teacher/sales/sales-offer-lead-gen-strategist.md +257 -0
- package/personas-teacher/sales/sales-outbound-strategist.md +201 -0
- package/personas-teacher/sales/sales-pipeline-analyst.md +267 -0
- package/personas-teacher/sales/sales-proposal-strategist.md +217 -0
- package/personas-teacher/security/security-appsec-engineer.md +491 -0
- package/personas-teacher/security/security-architect.md +304 -0
- package/personas-teacher/security/security-blockchain-security-auditor.md +463 -0
- package/personas-teacher/security/security-cloud-security-architect.md +523 -0
- package/personas-teacher/security/security-compliance-auditor.md +158 -0
- package/personas-teacher/security/security-incident-responder.md +437 -0
- package/personas-teacher/security/security-penetration-tester.md +399 -0
- package/personas-teacher/security/security-senior-secops.md +750 -0
- package/personas-teacher/security/security-threat-detection-engineer.md +534 -0
- package/personas-teacher/security/security-threat-intelligence-analyst.md +644 -0
- package/personas-teacher/spatial-computing/macos-spatial-metal-engineer.md +337 -0
- package/personas-teacher/spatial-computing/terminal-integration-specialist.md +70 -0
- package/personas-teacher/spatial-computing/visionos-spatial-engineer.md +54 -0
- package/personas-teacher/spatial-computing/xr-cockpit-interaction-specialist.md +32 -0
- package/personas-teacher/spatial-computing/xr-immersive-developer.md +32 -0
- package/personas-teacher/spatial-computing/xr-interface-architect.md +32 -0
- package/personas-teacher/specialized/accounts-payable-agent.md +185 -0
- package/personas-teacher/specialized/agentic-identity-trust.md +387 -0
- package/personas-teacher/specialized/agents-orchestrator.md +367 -0
- package/personas-teacher/specialized/automation-governance-architect.md +216 -0
- package/personas-teacher/specialized/business-strategist.md +488 -0
- package/personas-teacher/specialized/change-management-consultant.md +497 -0
- package/personas-teacher/specialized/chief-financial-officer.md +388 -0
- package/personas-teacher/specialized/corporate-training-designer.md +192 -0
- package/personas-teacher/specialized/customer-service.md +398 -0
- package/personas-teacher/specialized/customer-success-manager.md +460 -0
- package/personas-teacher/specialized/data-consolidation-agent.md +60 -0
- package/personas-teacher/specialized/data-privacy-officer.md +412 -0
- package/personas-teacher/specialized/esg-sustainability-officer.md +396 -0
- package/personas-teacher/specialized/government-digital-presales-consultant.md +363 -0
- package/personas-teacher/specialized/grant-writer.md +511 -0
- package/personas-teacher/specialized/healthcare-customer-service.md +389 -0
- package/personas-teacher/specialized/healthcare-marketing-compliance.md +395 -0
- package/personas-teacher/specialized/hospitality-guest-services.md +603 -0
- package/personas-teacher/specialized/hr-onboarding.md +451 -0
- package/personas-teacher/specialized/identity-graph-operator.md +260 -0
- package/personas-teacher/specialized/language-translator.md +264 -0
- package/personas-teacher/specialized/legal-billing-time-tracking.md +569 -0
- package/personas-teacher/specialized/legal-client-intake.md +492 -0
- package/personas-teacher/specialized/legal-document-review.md +454 -0
- package/personas-teacher/specialized/loan-officer-assistant.md +555 -0
- package/personas-teacher/specialized/lsp-index-engineer.md +314 -0
- package/personas-teacher/specialized/ma-integration-manager.md +427 -0
- package/personas-teacher/specialized/medical-billing-coding-specialist.md +491 -0
- package/personas-teacher/specialized/operations-manager.md +399 -0
- package/personas-teacher/specialized/organizational-psychologist.md +391 -0
- package/personas-teacher/specialized/personal-growth-mentor.md +159 -0
- package/personas-teacher/specialized/real-estate-buyer-seller.md +596 -0
- package/personas-teacher/specialized/recruitment-specialist.md +509 -0
- package/personas-teacher/specialized/report-distribution-agent.md +65 -0
- package/personas-teacher/specialized/retail-customer-returns.md +566 -0
- package/personas-teacher/specialized/sales-data-extraction-agent.md +67 -0
- package/personas-teacher/specialized/sales-outreach.md +425 -0
- package/personas-teacher/specialized/specialized-chief-of-staff.md +279 -0
- package/personas-teacher/specialized/specialized-civil-engineer.md +356 -0
- package/personas-teacher/specialized/specialized-cultural-intelligence-strategist.md +88 -0
- package/personas-teacher/specialized/specialized-developer-advocate.md +317 -0
- package/personas-teacher/specialized/specialized-document-generator.md +55 -0
- package/personas-teacher/specialized/specialized-french-consulting-market.md +192 -0
- package/personas-teacher/specialized/specialized-korean-business-navigator.md +216 -0
- package/personas-teacher/specialized/specialized-mcp-builder.md +248 -0
- package/personas-teacher/specialized/specialized-model-qa.md +488 -0
- package/personas-teacher/specialized/specialized-pricing-analyst.md +243 -0
- package/personas-teacher/specialized/specialized-salesforce-architect.md +180 -0
- package/personas-teacher/specialized/specialized-strategy-duel-agent.md +130 -0
- package/personas-teacher/specialized/specialized-workflow-architect.md +597 -0
- package/personas-teacher/specialized/study-abroad-advisor.md +282 -0
- package/personas-teacher/specialized/supply-chain-strategist.md +582 -0
- package/personas-teacher/specialized/zk-steward.md +211 -0
- package/personas-teacher/strategy/EXECUTIVE-BRIEF.md +95 -0
- package/personas-teacher/strategy/QUICKSTART.md +194 -0
- package/personas-teacher/strategy/coordination/agent-activation-prompts.md +401 -0
- package/personas-teacher/strategy/coordination/handoff-templates.md +357 -0
- package/personas-teacher/strategy/nexus-strategy.md +1110 -0
- package/personas-teacher/strategy/playbooks/phase-0-discovery.md +178 -0
- package/personas-teacher/strategy/playbooks/phase-1-strategy.md +238 -0
- package/personas-teacher/strategy/playbooks/phase-2-foundation.md +278 -0
- package/personas-teacher/strategy/playbooks/phase-3-build.md +286 -0
- package/personas-teacher/strategy/playbooks/phase-4-hardening.md +332 -0
- package/personas-teacher/strategy/playbooks/phase-5-launch.md +277 -0
- package/personas-teacher/strategy/playbooks/phase-6-operate.md +318 -0
- package/personas-teacher/strategy/runbooks/scenario-enterprise-feature.md +157 -0
- package/personas-teacher/strategy/runbooks/scenario-incident-response.md +217 -0
- package/personas-teacher/strategy/runbooks/scenario-marketing-campaign.md +187 -0
- package/personas-teacher/strategy/runbooks/scenario-startup-mvp.md +154 -0
- package/personas-teacher/support/support-analytics-reporter.md +365 -0
- package/personas-teacher/support/support-executive-summary-generator.md +212 -0
- package/personas-teacher/support/support-finance-tracker.md +442 -0
- package/personas-teacher/support/support-infrastructure-maintainer.md +618 -0
- package/personas-teacher/support/support-legal-compliance-checker.md +588 -0
- package/personas-teacher/support/support-support-responder.md +585 -0
- package/personas-teacher/testing/testing-accessibility-auditor.md +316 -0
- package/personas-teacher/testing/testing-api-tester.md +306 -0
- package/personas-teacher/testing/testing-evidence-collector.md +210 -0
- package/personas-teacher/testing/testing-performance-benchmarker.md +268 -0
- package/personas-teacher/testing/testing-reality-checker.md +236 -0
- package/personas-teacher/testing/testing-test-results-analyzer.md +305 -0
- package/personas-teacher/testing/testing-tool-evaluator.md +394 -0
- package/personas-teacher/testing/testing-workflow-optimizer.md +450 -0
- package/personas-teacher/tools.json +19 -0
- package/skill/add/SKILL.md +17 -12
- package/skill/add/advisor.md +31 -34
- package/skill/add/components.md +21 -23
- package/skill/add/confidence.md +1 -1
- package/skill/add/deltas.md +12 -13
- package/skill/add/design.md +52 -49
- package/skill/add/fold.md +18 -16
- package/skill/add/intake.md +4 -3
- package/skill/add/loop.md +1 -2
- package/skill/add/phases/0-ground.md +32 -34
- package/skill/add/phases/0-setup.md +22 -21
- package/skill/add/phases/1-specify.md +8 -6
- package/skill/add/phases/3-contract.md +1 -1
- package/skill/add/phases/4-tests.md +9 -10
- package/skill/add/phases/5-build.md +17 -13
- package/skill/add/phases/6-verify.md +7 -4
- package/skill/add/phases/7-observe.md +2 -1
- package/skill/add/report-template.md +61 -18
- package/skill/add/run.md +4 -3
- package/skill/add/scope.md +2 -2
- package/skill/add/sensitivity.md +51 -0
- package/skill/add/streams.md +24 -23
- package/tooling/add.py +1076 -39
- package/tooling/add_engine/autonomy.py +36 -1
- package/tooling/add_engine/constants.py +56 -3
- package/tooling/add_engine/guidelines.py +22 -20
- package/tooling/add_engine/predicates.py +56 -3
- package/tooling/add_engine/search.py +165 -0
- package/tooling/templates/GLOSSARY.md.tmpl +16 -0
- package/tooling/templates/MILESTONE.md.tmpl +9 -0
- package/tooling/templates/PROMPT.persona.md.tmpl +44 -0
- package/tooling/templates/TASK.fast.md.tmpl +2 -1
- package/tooling/templates/TASK.md.tmpl +36 -6
- package/tooling/templates/gitignore.tmpl +12 -0
- 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,
|
|
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
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
946
|
-
|
|
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(
|
|
950
|
-
kept.push(
|
|
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
|
-
(
|
|
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
|
-
|
|
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 ·
|
|
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");
|
package/docs/02-the-flow.md
CHANGED
|
@@ -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
|
|
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.
|
package/docs/06-step-4-tests.md
CHANGED
|
@@ -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).
|
package/docs/17-components.md
CHANGED
|
@@ -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
|
-
|
|
27
|
+
green_bar = "pytest + pyright"
|
|
28
28
|
|
|
29
29
|
[component.dashboard]
|
|
30
30
|
root = "apps/web"
|
|
31
|
-
|
|
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,
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
Binary file
|