wicked-garden 12.27.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/.claude-plugin/archetypes.json +166 -0
- package/.claude-plugin/components.json +75 -0
- package/.claude-plugin/marketplace.json +15 -0
- package/.claude-plugin/plugin.json +174 -0
- package/.claude-plugin/specialist.json +48 -0
- package/.claude-plugin/stack-registry.json +96 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +712 -0
- package/CONTRIBUTING.md +99 -0
- package/ETHOS.md +83 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/WICKED_GARDEN_BUS_EVENTS.md +149 -0
- package/agents/agentic/architect.md +449 -0
- package/agents/agentic/performance-analyst.md +702 -0
- package/agents/agentic/safety-reviewer.md +662 -0
- package/agents/crew/implementer.md +163 -0
- package/agents/crew/researcher.md +94 -0
- package/agents/crew/reviewer.md +184 -0
- package/agents/data/data-engineer.md +227 -0
- package/agents/engineering/api-documentarian.md +440 -0
- package/agents/engineering/migration-engineer.md +301 -0
- package/agents/engineering/solution-architect.md +208 -0
- package/agents/jam/brainstorm-facilitator.md +297 -0
- package/agents/jam/council.md +322 -0
- package/agents/jam/quick-facilitator.md +85 -0
- package/agents/persona/persona-agent.md +121 -0
- package/agents/platform/compliance-officer.md +266 -0
- package/agents/platform/privacy-expert.md +496 -0
- package/agents/platform/security-engineer.md +287 -0
- package/agents/product/a11y-expert.md +323 -0
- package/agents/product/requirements-analyst.md +238 -0
- package/agents/product/ui-reviewer.md +180 -0
- package/agents/product/ux-designer.md +199 -0
- package/agents/product/value-strategist.md +269 -0
- package/agents/qe/semantic-reviewer.md +78 -0
- package/commands/agentic/audit.md +24 -0
- package/commands/agentic/design.md +24 -0
- package/commands/agentic/frameworks.md +25 -0
- package/commands/agentic/review.md +27 -0
- package/commands/archetype/build.md +12 -0
- package/commands/archetype/decide.md +12 -0
- package/commands/archetype/explore.md +12 -0
- package/commands/archetype/incident.md +12 -0
- package/commands/archetype/migrate.md +12 -0
- package/commands/archetype/modernize.md +12 -0
- package/commands/archetype/review.md +14 -0
- package/commands/archetype/ship.md +12 -0
- package/commands/archetype/specify.md +12 -0
- package/commands/archetype/triage.md +12 -0
- package/commands/compile.md +37 -0
- package/commands/data/analyze.md +18 -0
- package/commands/data/data.md +21 -0
- package/commands/data/ml.md +18 -0
- package/commands/data/ontology.md +22 -0
- package/commands/data/pipeline.md +17 -0
- package/commands/deliberate.md +181 -0
- package/commands/engineering/add-field.md +77 -0
- package/commands/engineering/apply.md +58 -0
- package/commands/engineering/arch.md +26 -0
- package/commands/engineering/debug.md +27 -0
- package/commands/engineering/docs.md +27 -0
- package/commands/engineering/new-generator.md +257 -0
- package/commands/engineering/patch-plan.md +59 -0
- package/commands/engineering/plan.md +29 -0
- package/commands/engineering/remove.md +48 -0
- package/commands/engineering/rename.md +44 -0
- package/commands/engineering/review.md +32 -0
- package/commands/help.md +128 -0
- package/commands/install.md +118 -0
- package/commands/intent.md +54 -0
- package/commands/jam/brainstorm.md +37 -0
- package/commands/jam/council.md +49 -0
- package/commands/jam/quick.md +21 -0
- package/commands/jam/revisit.md +79 -0
- package/commands/persona/as.md +136 -0
- package/commands/persona/define.md +24 -0
- package/commands/persona/list.md +118 -0
- package/commands/platform/actions.md +20 -0
- package/commands/platform/assert.md +18 -0
- package/commands/platform/audit.md +23 -0
- package/commands/platform/compliance.md +20 -0
- package/commands/platform/gh.md +22 -0
- package/commands/platform/health.md +22 -0
- package/commands/platform/incident.md +23 -0
- package/commands/platform/infra.md +24 -0
- package/commands/platform/peer-health.md +103 -0
- package/commands/platform/plugin-health.md +58 -0
- package/commands/platform/security.md +38 -0
- package/commands/platform/toolchain.md +96 -0
- package/commands/platform/traces.md +20 -0
- package/commands/product/a11y.md +19 -0
- package/commands/product/acceptance.md +19 -0
- package/commands/product/align.md +20 -0
- package/commands/product/analyze.md +23 -0
- package/commands/product/elicit.md +19 -0
- package/commands/product/listen.md +24 -0
- package/commands/product/mockup.md +20 -0
- package/commands/product/screenshot.md +19 -0
- package/commands/product/strategy.md +40 -0
- package/commands/product/synthesize.md +23 -0
- package/commands/product/ux-review.md +42 -0
- package/commands/product/ux.md +20 -0
- package/commands/product/visual-direction.md +34 -0
- package/commands/prove.md +47 -0
- package/commands/report-issue.md +243 -0
- package/commands/reset.md +144 -0
- package/commands/search/blast-radius.md +58 -0
- package/commands/search/hotspots.md +55 -0
- package/commands/search/index.md +38 -0
- package/commands/search/lineage.md +47 -0
- package/commands/search/service-map.md +79 -0
- package/commands/setup.md +363 -0
- package/commands/smaht/briefing.md +200 -0
- package/commands/smaht/events-import.md +95 -0
- package/commands/smaht/state.md +19 -0
- package/commands/where-am-i.md +58 -0
- package/docs/README.md +32 -0
- package/docs/adr/0001-code-relationship-graph-engine.md +116 -0
- package/docs/adr/0002-major-cleanup-keep-cut.md +91 -0
- package/docs/adr/0003-peers-gate-required-layers-optin.md +41 -0
- package/docs/adr/0004-code-graph-moves-to-wicked-brain.md +47 -0
- package/docs/brain-chunk-format.md +135 -0
- package/docs/compiler.md +95 -0
- package/docs/daemon-api.md +70 -0
- package/docs/domains.md +197 -0
- package/docs/getting-started.md +208 -0
- package/docs/plans/2026-06-08-wicked-loom-conduct.md +1337 -0
- package/docs/plans/2026-06-08-wicked-loom-contract.md +638 -0
- package/docs/plans/2026-06-08-wicked-loom-cutover.md +1300 -0
- package/docs/plans/2026-06-08-wicked-loom-expand-compose.md +843 -0
- package/docs/plans/2026-06-08-wicked-loom-flow-cutover.md +403 -0
- package/docs/plans/2026-06-10-codegraph-to-brain-phase1a-graph-core.md +838 -0
- package/docs/plans/2026-06-10-codegraph-to-brain-phase1b-injected-edges.md +44 -0
- package/docs/required-peers.md +60 -0
- package/docs/specs/2026-06-08-wicked-loom-extraction.md +225 -0
- package/docs/specs/2026-06-10-codegraph-to-brain-migration.md +168 -0
- package/docs/v11/archetypes.md +231 -0
- package/hooks/hooks.json +275 -0
- package/hooks/scripts/bootstrap.py +1797 -0
- package/hooks/scripts/invoke.py +62 -0
- package/hooks/scripts/notification.py +166 -0
- package/hooks/scripts/permission_request.py +223 -0
- package/hooks/scripts/post_tool.py +1374 -0
- package/hooks/scripts/pre_compact.py +272 -0
- package/hooks/scripts/pre_tool.py +1278 -0
- package/hooks/scripts/prompt_submit.py +1109 -0
- package/hooks/scripts/session_end.py +95 -0
- package/hooks/scripts/stop.py +624 -0
- package/hooks/scripts/subagent_lifecycle.py +464 -0
- package/hooks/scripts/subscribers/on_gate_decided.py +84 -0
- package/hooks/scripts/task_completed.py +322 -0
- package/hooks/scripts/worktree_guard.py +148 -0
- package/hooks/subscriptions/on_gate_decided.json +9 -0
- package/install.mjs +128 -0
- package/package.json +41 -0
- package/pyproject.toml +59 -0
- package/schemas/README.md +32 -0
- package/schemas/assert_contracts.json +37 -0
- package/schemas/health_probe.json +46 -0
- package/schemas/plugin_status.json +37 -0
- package/scripts/_adapters/__init__.py +166 -0
- package/scripts/_adapters/jira_adapter.py +18 -0
- package/scripts/_adapters/linear_adapter.py +18 -0
- package/scripts/_adapters/miro_adapter.py +18 -0
- package/scripts/_adapters/notion_adapter.py +18 -0
- package/scripts/_agents.py +357 -0
- package/scripts/_brain_ingest/__init__.py +2 -0
- package/scripts/_brain_ingest/session_fact_extractor.py +341 -0
- package/scripts/_brain_port.py +163 -0
- package/scripts/_bus.py +1081 -0
- package/scripts/_bus_catalog_gen.py +93 -0
- package/scripts/_bus_consumers.json +46 -0
- package/scripts/_bus_emit.py +43 -0
- package/scripts/_capability_registry.py +334 -0
- package/scripts/_capability_resolver.py +322 -0
- package/scripts/_domain_store.py +559 -0
- package/scripts/_event_log_reader.py +154 -0
- package/scripts/_event_schema.py +305 -0
- package/scripts/_event_store.py +397 -0
- package/scripts/_heavy_cadence.py +324 -0
- package/scripts/_integration_resolver.py +321 -0
- package/scripts/_logger.py +151 -0
- package/scripts/_loom.py +482 -0
- package/scripts/_paths.py +133 -0
- package/scripts/_python.sh +22 -0
- package/scripts/_run.py +137 -0
- package/scripts/_session.py +502 -0
- package/scripts/_validate_registry.py +1042 -0
- package/scripts/_wicked_testing_probe.py +277 -0
- package/scripts/agentic/analyze_agents.py +391 -0
- package/scripts/agentic/detect_framework.py +420 -0
- package/scripts/agentic/issue_taxonomy.py +357 -0
- package/scripts/agentic/pattern_scorer.py +434 -0
- package/scripts/classify/persist.py +169 -0
- package/scripts/compiler/compile.py +552 -0
- package/scripts/compiler/phase0/VERDICT.md +60 -0
- package/scripts/compiler/phase0/detect.py +292 -0
- package/scripts/compiler/phase0/emit.py +228 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.bindings.json +30 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/honest-tests.txt +2 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/command_iq.bindings.json +45 -0
- package/scripts/compiler/phase0/evidence/command_iq.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/honest-tests.txt +3 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/memos.bindings.json +28 -0
- package/scripts/compiler/phase0/evidence/memos.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/honest-tests.txt +2 -0
- package/scripts/compiler/phase0/evidence/memos.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/verdict.json +104 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.bindings.json +29 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/honest-tests.txt +3 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/wire-vault-transcript.txt +60 -0
- package/scripts/compiler/phase0/run_probe.py +234 -0
- package/scripts/compiler/phase0/wire_vault.py +310 -0
- package/scripts/crew/__init__.py +0 -0
- package/scripts/crew/_task_audit_writer.py +243 -0
- package/scripts/crew/_task_reader.py +389 -0
- package/scripts/crew/archetypes_v11.py +348 -0
- package/scripts/crew/detectors/__init__.py +12 -0
- package/scripts/crew/flow_compiler.py +121 -0
- package/scripts/crew/phase_manager.py +703 -0
- package/scripts/crew/scope_delta.py +264 -0
- package/scripts/crew/specialist_resolver.py +218 -0
- package/scripts/crew/stack_registry.py +189 -0
- package/scripts/data/data_profiler.py +271 -0
- package/scripts/data/ontology_recommender.py +231 -0
- package/scripts/data/schema_validator.py +267 -0
- package/scripts/delivery/drift.py +739 -0
- package/scripts/delivery/telemetry.py +614 -0
- package/scripts/engineering/patch/codegraph_db.py +125 -0
- package/scripts/engineering/patch/generators/__init__.py +56 -0
- package/scripts/engineering/patch/generators/base.py +373 -0
- package/scripts/engineering/patch/generators/csharp_generator.py +494 -0
- package/scripts/engineering/patch/generators/go_generator.py +430 -0
- package/scripts/engineering/patch/generators/java_generator.py +928 -0
- package/scripts/engineering/patch/generators/jsp_generator.py +454 -0
- package/scripts/engineering/patch/generators/kotlin_generator.py +561 -0
- package/scripts/engineering/patch/generators/perl_generator.py +510 -0
- package/scripts/engineering/patch/generators/php_generator.py +644 -0
- package/scripts/engineering/patch/generators/propagation_engine.py +929 -0
- package/scripts/engineering/patch/generators/python_generator.py +487 -0
- package/scripts/engineering/patch/generators/ruby_generator.py +463 -0
- package/scripts/engineering/patch/generators/rust_generator.py +400 -0
- package/scripts/engineering/patch/generators/sql_generator.py +594 -0
- package/scripts/engineering/patch/generators/typescript_generator.py +439 -0
- package/scripts/engineering/patch/patch.py +800 -0
- package/scripts/engineering/patch/safety.py +429 -0
- package/scripts/jam/__init__.py +0 -0
- package/scripts/jam/_bus_consumers.py +271 -0
- package/scripts/jam/agentic_cli_registry.py +703 -0
- package/scripts/jam/consensus.py +737 -0
- package/scripts/jam/detect_clis.py +409 -0
- package/scripts/jam/jam.py +283 -0
- package/scripts/jam/save_transcript.py +171 -0
- package/scripts/loom/__init__.py +22 -0
- package/scripts/loom/cli.py +170 -0
- package/scripts/loom/compose.py +180 -0
- package/scripts/loom/gate.py +120 -0
- package/scripts/loom/manifest.py +140 -0
- package/scripts/loom/resolve.py +59 -0
- package/scripts/persona/__init__.py +0 -0
- package/scripts/persona/registry.py +688 -0
- package/scripts/platform/guard_pipeline.py +1214 -0
- package/scripts/platform/guard_profiles.py +216 -0
- package/scripts/platform/observability/assert_contracts.py +434 -0
- package/scripts/platform/observability/health_probe.py +915 -0
- package/scripts/platform/observability/ops_log_viewer.py +209 -0
- package/scripts/platform/prereq_doctor.py +567 -0
- package/scripts/qe/conditions_manifest.py +260 -0
- package/scripts/qe/content_sanitizer.py +160 -0
- package/scripts/qe/coverage_tracker.py +535 -0
- package/scripts/qe/discover_scenarios.py +243 -0
- package/scripts/qe/evidence_tracker.py +232 -0
- package/scripts/qe/prove.py +196 -0
- package/scripts/qe/registry_lookup.py +80 -0
- package/scripts/qe/registry_store.py +176 -0
- package/scripts/qe/semantic_review.py +1005 -0
- package/scripts/qe/vault_gate.py +382 -0
- package/scripts/qe/verdict_audit.py +175 -0
- package/scripts/qe/verdict_schema.py +335 -0
- package/scripts/reset.py +220 -0
- package/scripts/resolve_path.py +22 -0
- package/scripts/sentinel/invariants.py +402 -0
- package/scripts/sentinel/pre_push.py +73 -0
- package/scripts/setup/__init__.py +1 -0
- package/scripts/setup/detect_state.py +114 -0
- package/scripts/setup/migrate_legacy.py +67 -0
- package/scripts/setup/migrate_v6_projects.py +232 -0
- package/scripts/setup/onboarding.py +114 -0
- package/scripts/smaht/_bus_consumers.py +98 -0
- package/scripts/smaht/adapters/__init__.py +83 -0
- package/scripts/smaht/adapters/brain_adapter.py +254 -0
- package/scripts/smaht/adapters/context7_adapter.py +438 -0
- package/scripts/smaht/adapters/delegation_adapter.py +104 -0
- package/scripts/smaht/adapters/domain_adapter.py +206 -0
- package/scripts/smaht/adapters/events_adapter.py +231 -0
- package/scripts/smaht/adapters/tools_adapter.py +49 -0
- package/scripts/smaht/cheatsheet_store.py +236 -0
- package/scripts/smaht/context_package.py +386 -0
- package/scripts/smaht/knowledge_graph.py +395 -0
- package/scripts/smaht/propose_skills.py +856 -0
- package/scripts/smaht/test_cheatsheet_store.py +428 -0
- package/scripts/statusline.py +151 -0
- package/scripts/where_am_i.py +236 -0
- package/skills/agentic/agentic-patterns/SKILL.md +91 -0
- package/skills/agentic/agentic-patterns/refs/anti-patterns-design.md +283 -0
- package/skills/agentic/agentic-patterns/refs/anti-patterns-operational.md +216 -0
- package/skills/agentic/agentic-patterns/refs/pattern-catalog.md +329 -0
- package/skills/agentic/context-engineering/SKILL.md +142 -0
- package/skills/agentic/context-engineering/refs/caching-and-optimization.md +251 -0
- package/skills/agentic/context-engineering/refs/compression-techniques.md +171 -0
- package/skills/agentic/context-engineering/refs/cost-calculation-budget.md +250 -0
- package/skills/agentic/context-engineering/refs/cost-optimization-reporting.md +300 -0
- package/skills/agentic/context-engineering/refs/selective-loading.md +179 -0
- package/skills/agentic/frameworks/SKILL.md +191 -0
- package/skills/agentic/frameworks/refs/framework-profiles-1.md +209 -0
- package/skills/agentic/frameworks/refs/framework-profiles-2.md +214 -0
- package/skills/agentic/frameworks/refs/migration-patterns-paths.md +267 -0
- package/skills/agentic/frameworks/refs/migration-patterns-testing.md +208 -0
- package/skills/agentic/refs/audit.md +134 -0
- package/skills/agentic/refs/design.md +137 -0
- package/skills/agentic/refs/frameworks.md +102 -0
- package/skills/agentic/refs/review.md +160 -0
- package/skills/agentic/review-methodology/SKILL.md +186 -0
- package/skills/agentic/review-methodology/refs/deliverable-templates.md +246 -0
- package/skills/agentic/review-methodology/refs/issue-taxonomy-quality-testing.md +277 -0
- package/skills/agentic/review-methodology/refs/issue-taxonomy-reliability-safety.md +253 -0
- package/skills/agentic/trust-and-safety/SKILL.md +177 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-actions.md +246 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-input-output.md +238 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-resources.md +209 -0
- package/skills/agentic/trust-and-safety/refs/safety-checklist-advanced.md +209 -0
- package/skills/agentic/trust-and-safety/refs/safety-checklist-core.md +215 -0
- package/skills/archetype/SKILL.md +104 -0
- package/skills/archetype/refs/build.md +142 -0
- package/skills/archetype/refs/decide.md +117 -0
- package/skills/archetype/refs/explore.md +66 -0
- package/skills/archetype/refs/incident.md +149 -0
- package/skills/archetype/refs/migrate.md +170 -0
- package/skills/archetype/refs/modernize.md +187 -0
- package/skills/archetype/refs/review.md +145 -0
- package/skills/archetype/refs/ship.md +117 -0
- package/skills/archetype/refs/specify.md +108 -0
- package/skills/archetype/refs/triage.md +62 -0
- package/skills/classify/SKILL.md +157 -0
- package/skills/data/analysis/SKILL.md +125 -0
- package/skills/data/analysis/refs/templates.md +170 -0
- package/skills/data/data/SKILL.md +118 -0
- package/skills/data/data/refs/examples.md +235 -0
- package/skills/data/ml/SKILL.md +125 -0
- package/skills/data/ml/refs/templates.md +172 -0
- package/skills/data/pipeline/SKILL.md +136 -0
- package/skills/data/pipeline/refs/templates.md +175 -0
- package/skills/data/refs/analyze.md +123 -0
- package/skills/data/refs/data.md +95 -0
- package/skills/data/refs/ml.md +118 -0
- package/skills/data/refs/pipeline.md +92 -0
- package/skills/deliberate/SKILL.md +125 -0
- package/skills/deliberate/refs/architecture-lens.md +37 -0
- package/skills/deliberate/refs/assumptions-lens.md +43 -0
- package/skills/deliberate/refs/code-lens.md +38 -0
- package/skills/deliberate/refs/content-lens.md +37 -0
- package/skills/deliberate/refs/data-lens.md +43 -0
- package/skills/deliberate/refs/design-lens.md +37 -0
- package/skills/deliberate/refs/opportunity-patterns.md +107 -0
- package/skills/deliberate/refs/ops-lens.md +43 -0
- package/skills/deliberate/refs/product-lens.md +46 -0
- package/skills/deliberate/refs/rethink-framework.md +77 -0
- package/skills/deliberate/refs/security-lens.md +47 -0
- package/skills/deliberate/refs/testing-lens.md +45 -0
- package/skills/deliberate/refs/timing-lens.md +42 -0
- package/skills/engineering/architecture/SKILL.md +177 -0
- package/skills/engineering/architecture/refs/adr-template.md +268 -0
- package/skills/engineering/architecture/refs/architecture-template-deployment.md +176 -0
- package/skills/engineering/architecture/refs/architecture-template-design.md +300 -0
- package/skills/engineering/architecture/refs/examples-ecommerce-analytics.md +299 -0
- package/skills/engineering/architecture/refs/examples-saas-trading.md +291 -0
- package/skills/engineering/architecture/refs/patterns-advanced.md +238 -0
- package/skills/engineering/architecture/refs/patterns-structural.md +303 -0
- package/skills/engineering/backend/SKILL.md +102 -0
- package/skills/engineering/backend/refs/checklists.md +146 -0
- package/skills/engineering/backend/refs/patterns.md +181 -0
- package/skills/engineering/debugging/SKILL.md +99 -0
- package/skills/engineering/debugging/refs/patterns.md +248 -0
- package/skills/engineering/debugging/refs/process.md +203 -0
- package/skills/engineering/docs/SKILL.md +97 -0
- package/skills/engineering/engineering/SKILL.md +88 -0
- package/skills/engineering/engineering/refs/checklists.md +91 -0
- package/skills/engineering/engineering/refs/templates.md +71 -0
- package/skills/engineering/frontend/SKILL.md +92 -0
- package/skills/engineering/frontend/refs/checklists.md +96 -0
- package/skills/engineering/frontend/refs/patterns.md +167 -0
- package/skills/engineering/integration/SKILL.md +183 -0
- package/skills/engineering/integration/refs/error-handling-best-practices.md +139 -0
- package/skills/engineering/integration/refs/error-handling-error-classes-typescript.md +212 -0
- package/skills/engineering/integration/refs/error-handling-error-response-format.md +301 -0
- package/skills/engineering/integration/refs/error-handling-retry-strategies.md +190 -0
- package/skills/engineering/integration/refs/event-schemas-best-practices.md +225 -0
- package/skills/engineering/integration/refs/event-schemas-event-naming-conventions.md +303 -0
- package/skills/engineering/integration/refs/event-schemas-event-structure.md +288 -0
- package/skills/engineering/integration/refs/graphql-best-practices.md +138 -0
- package/skills/engineering/integration/refs/graphql-error-handling.md +198 -0
- package/skills/engineering/integration/refs/graphql-graphql-basics.md +197 -0
- package/skills/engineering/integration/refs/graphql-query-examples.md +226 -0
- package/skills/engineering/integration/refs/graphql-schema-design-patterns.md +280 -0
- package/skills/engineering/integration/refs/grpc-grpc-overview.md +246 -0
- package/skills/engineering/integration/refs/grpc-implementation-examples.md +269 -0
- package/skills/engineering/integration/refs/grpc-metadata-headers.md +304 -0
- package/skills/engineering/integration/refs/grpc-testing.md +49 -0
- package/skills/engineering/integration/refs/openapi-template.yaml +682 -0
- package/skills/engineering/integration/refs/resilience-best-practices.md +109 -0
- package/skills/engineering/integration/refs/resilience-circuit-breaker-pattern.md +302 -0
- package/skills/engineering/integration/refs/resilience-rate-limiting.md +245 -0
- package/skills/engineering/integration/refs/resilience-timeout-pattern.md +212 -0
- package/skills/engineering/integration/refs/rest-api-fundamentals.md +299 -0
- package/skills/engineering/integration/refs/rest-api-responses-security.md +302 -0
- package/skills/engineering/integration/refs/versioning-breaking-changes.md +237 -0
- package/skills/engineering/integration/refs/versioning-migration-strategies.md +257 -0
- package/skills/engineering/integration/refs/versioning-versioning-strategies.md +296 -0
- package/skills/engineering/large-scale-migration/SKILL.md +65 -0
- package/skills/engineering/patch/SKILL.md +184 -0
- package/skills/engineering/refs/arch.md +126 -0
- package/skills/engineering/refs/debug.md +142 -0
- package/skills/engineering/refs/docs.md +190 -0
- package/skills/engineering/refs/plan.md +109 -0
- package/skills/engineering/refs/review.md +138 -0
- package/skills/engineering/system-design/SKILL.md +154 -0
- package/skills/engineering/system-design/refs/anti-patterns-big-ball-of-mud.md +254 -0
- package/skills/engineering/system-design/refs/anti-patterns-circular-dependencies.md +241 -0
- package/skills/engineering/system-design/refs/anti-patterns-feature-envy.md +168 -0
- package/skills/engineering/system-design/refs/anti-patterns-god-object.md +247 -0
- package/skills/engineering/system-design/refs/component-template-lifecycle.md +210 -0
- package/skills/engineering/system-design/refs/component-template-maintenance.md +219 -0
- package/skills/engineering/system-design/refs/component-template-structure.md +276 -0
- package/skills/engineering/system-design/refs/dependencies-best-practices.md +260 -0
- package/skills/engineering/system-design/refs/dependencies-dependency-principles.md +295 -0
- package/skills/engineering/system-design/refs/dependencies-dependency-tracking.md +261 -0
- package/skills/engineering/system-design/refs/interface-template-lifecycle.md +211 -0
- package/skills/engineering/system-design/refs/interface-template-maintenance.md +212 -0
- package/skills/engineering/system-design/refs/interface-template-structure.md +269 -0
- package/skills/engineering/system-design/refs/patterns-dependency-injection.md +151 -0
- package/skills/engineering/system-design/refs/patterns-facade-pattern.md +257 -0
- package/skills/engineering/system-design/refs/patterns-layered-pattern.md +204 -0
- package/skills/engineering/system-design/refs/patterns-plugin-architecture.md +229 -0
- package/skills/engineering/unit-test-quality/SKILL.md +194 -0
- package/skills/ground/SKILL.md +130 -0
- package/skills/integration-discovery/SKILL.md +168 -0
- package/skills/integration-discovery/refs/capabilities.md +282 -0
- package/skills/integration-discovery/refs/cli-detection.md +295 -0
- package/skills/integration-discovery/refs/discovery-sources.md +233 -0
- package/skills/integration-discovery/refs/task-patterns.md +206 -0
- package/skills/jam/SKILL.md +45 -0
- package/skills/jam/refs/facilitation-patterns.md +125 -0
- package/skills/jam/refs/quick.md +64 -0
- package/skills/jam/refs/synthesis-patterns.md +114 -0
- package/skills/multi-model/SKILL.md +168 -0
- package/skills/multi-model/refs/aichat.md +119 -0
- package/skills/multi-model/refs/aider.md +100 -0
- package/skills/multi-model/refs/auditability.md +238 -0
- package/skills/multi-model/refs/codex.md +169 -0
- package/skills/multi-model/refs/context.md +306 -0
- package/skills/multi-model/refs/copilot.md +161 -0
- package/skills/multi-model/refs/examples.md +224 -0
- package/skills/multi-model/refs/gemini.md +185 -0
- package/skills/multi-model/refs/goose.md +112 -0
- package/skills/multi-model/refs/llm.md +124 -0
- package/skills/multi-model/refs/opencode.md +196 -0
- package/skills/multi-model/refs/orchestration.md +188 -0
- package/skills/multi-model/refs/pi.md +208 -0
- package/skills/persona/SKILL.md +144 -0
- package/skills/persona/refs/define.md +133 -0
- package/skills/platform/actions/refs/actions.md +114 -0
- package/skills/platform/audit/SKILL.md +174 -0
- package/skills/platform/audit/refs/audit.md +145 -0
- package/skills/platform/audit/refs/checklists-evidence-operations.md +144 -0
- package/skills/platform/audit/refs/checklists-gdpr-pci-evidence.md +213 -0
- package/skills/platform/audit/refs/checklists-soc2-hipaa.md +120 -0
- package/skills/platform/audit/refs/frameworks-gdpr-pci.md +225 -0
- package/skills/platform/audit/refs/frameworks-soc2-hipaa.md +228 -0
- package/skills/platform/compliance/SKILL.md +169 -0
- package/skills/platform/compliance/refs/checklists.md +303 -0
- package/skills/platform/compliance/refs/compliance.md +117 -0
- package/skills/platform/compliance/refs/frameworks.md +282 -0
- package/skills/platform/errors/SKILL.md +183 -0
- package/skills/platform/errors/refs/patterns.md +271 -0
- package/skills/platform/errors/refs/severity.md +264 -0
- package/skills/platform/gate-benchmark-rebaseline/SKILL.md +108 -0
- package/skills/platform/gh/refs/gh.md +105 -0
- package/skills/platform/gh-cli/SKILL.md +120 -0
- package/skills/platform/gh-cli/refs/automation.md +63 -0
- package/skills/platform/gh-cli/refs/gh-ops.md +103 -0
- package/skills/platform/gh-cli/refs/patterns.md +132 -0
- package/skills/platform/gh-cli/scripts/gh_ops.py +454 -0
- package/skills/platform/github-actions/SKILL.md +159 -0
- package/skills/platform/github-actions/refs/security.md +150 -0
- package/skills/platform/github-actions/refs/templates.md +230 -0
- package/skills/platform/github-actions/refs/troubleshooting.md +174 -0
- package/skills/platform/github-actions/scripts/workflow_gen.py +215 -0
- package/skills/platform/gitlab-ci/SKILL.md +140 -0
- package/skills/platform/gitlab-ci/refs/templates.md +223 -0
- package/skills/platform/gitlab-ci/refs/troubleshooting.md +213 -0
- package/skills/platform/glab-cli/SKILL.md +133 -0
- package/skills/platform/glab-cli/refs/ci-debugging.md +95 -0
- package/skills/platform/glab-cli/refs/glab-ops.md +78 -0
- package/skills/platform/glab-cli/refs/patterns.md +131 -0
- package/skills/platform/glab-cli/scripts/glab_ops.py +313 -0
- package/skills/platform/health/SKILL.md +177 -0
- package/skills/platform/health/refs/health.md +72 -0
- package/skills/platform/health/refs/sources.md +302 -0
- package/skills/platform/incident/refs/incident.md +136 -0
- package/skills/platform/infra/refs/infra.md +104 -0
- package/skills/platform/observability/SKILL.md +128 -0
- package/skills/platform/observability/refs/toolchain-discovery.md +243 -0
- package/skills/platform/prereq-doctor/SKILL.md +57 -0
- package/skills/platform/prereq-doctor/refs/tool-registry.md +58 -0
- package/skills/platform/traces/refs/traces.md +101 -0
- package/skills/product/acceptance-criteria/SKILL.md +193 -0
- package/skills/product/acceptance-criteria/refs/ac-anti-patterns.md +291 -0
- package/skills/product/acceptance-criteria/refs/ac-examples.md +277 -0
- package/skills/product/accessibility/SKILL.md +197 -0
- package/skills/product/accessibility/refs/aria-patterns-basics.md +250 -0
- package/skills/product/accessibility/refs/aria-patterns-dynamic.md +187 -0
- package/skills/product/accessibility/refs/aria-patterns-interactive.md +246 -0
- package/skills/product/accessibility/refs/audit-report-findings.md +248 -0
- package/skills/product/accessibility/refs/audit-report-results.md +276 -0
- package/skills/product/accessibility/refs/common-violations-html-css.md +270 -0
- package/skills/product/accessibility/refs/common-violations-interactive.md +303 -0
- package/skills/product/accessibility/refs/keyboard-testing-basics.md +222 -0
- package/skills/product/accessibility/refs/keyboard-testing-patterns.md +173 -0
- package/skills/product/accessibility/refs/screen-reader-testing-advanced.md +294 -0
- package/skills/product/accessibility/refs/screen-reader-testing-commands.md +255 -0
- package/skills/product/accessibility/refs/wcag-checklist.md +262 -0
- package/skills/product/analyze/SKILL.md +164 -0
- package/skills/product/analyze/refs/algorithms.md +238 -0
- package/skills/product/analyze/refs/sentiment-patterns.md +180 -0
- package/skills/product/imagery/SKILL.md +97 -0
- package/skills/product/imagery/alter/SKILL.md +105 -0
- package/skills/product/imagery/alter/refs/editing-reference.md +146 -0
- package/skills/product/imagery/create/SKILL.md +109 -0
- package/skills/product/imagery/create/refs/prompt-engineering.md +112 -0
- package/skills/product/imagery/create/refs/provider-reference.md +219 -0
- package/skills/product/imagery/review/SKILL.md +101 -0
- package/skills/product/imagery/review/refs/analysis-lenses.md +136 -0
- package/skills/product/imagery/review/refs/quality-gates.md +153 -0
- package/skills/product/imagery/scripts/analyze.sh +23 -0
- package/skills/product/imagery/scripts/edit.sh +14 -0
- package/skills/product/imagery/scripts/generate.sh +13 -0
- package/skills/product/imagery/scripts/provider.py +787 -0
- package/skills/product/listen/SKILL.md +175 -0
- package/skills/product/listen/refs/channels.md +220 -0
- package/skills/product/mockup/SKILL.md +106 -0
- package/skills/product/mockup/refs/templates.md +95 -0
- package/skills/product/refs/a11y.md +109 -0
- package/skills/product/refs/acceptance.md +79 -0
- package/skills/product/refs/align.md +67 -0
- package/skills/product/refs/analyze.md +77 -0
- package/skills/product/refs/elicit.md +67 -0
- package/skills/product/refs/listen.md +58 -0
- package/skills/product/refs/mockup.md +77 -0
- package/skills/product/refs/screenshot.md +59 -0
- package/skills/product/refs/strategy.md +77 -0
- package/skills/product/refs/synthesize.md +54 -0
- package/skills/product/refs/ux-review.md +68 -0
- package/skills/product/refs/ux.md +101 -0
- package/skills/product/requirements-analysis/SKILL.md +136 -0
- package/skills/product/requirements-analysis/refs/requirements-example-auth.md +264 -0
- package/skills/product/requirements-analysis/refs/requirements-example-export-integration.md +88 -0
- package/skills/product/requirements-analysis/refs/requirements-output-format-template.md +247 -0
- package/skills/product/requirements-analysis/refs/user-story-guide.md +296 -0
- package/skills/product/requirements-graph/SKILL.md +155 -0
- package/skills/product/requirements-graph/refs/examples.md +223 -0
- package/skills/product/requirements-graph/refs/schema.md +162 -0
- package/skills/product/requirements-migrate/SKILL.md +90 -0
- package/skills/product/requirements-navigate/SKILL.md +141 -0
- package/skills/product/requirements-navigate/refs/meta-templates.md +141 -0
- package/skills/product/screenshot/SKILL.md +147 -0
- package/skills/product/strategy/SKILL.md +198 -0
- package/skills/product/strategy/refs/frameworks.md +295 -0
- package/skills/product/strategy/refs/templates-competitive-market.md +299 -0
- package/skills/product/strategy/refs/templates-roi-value.md +207 -0
- package/skills/product/synthesize/SKILL.md +116 -0
- package/skills/product/synthesize/refs/journey-mapping.md +212 -0
- package/skills/product/synthesize/refs/prioritization.md +193 -0
- package/skills/product/ux-review/SKILL.md +164 -0
- package/skills/product/visual-direction/SKILL.md +137 -0
- package/skills/product/visual-review/SKILL.md +131 -0
- package/skills/runtime-exec/SKILL.md +138 -0
- package/skills/search/codebase-narrator/SKILL.md +131 -0
- package/skills/search/codebase-narrator/refs/output-template.md +100 -0
- package/skills/smaht/SKILL.md +72 -0
- package/skills/smaht/discovery/SKILL.md +65 -0
- package/skills/smaht/intent/SKILL.md +89 -0
- package/skills/smaht/propose-skills/SKILL.md +142 -0
- package/skills/smaht/refs/state.md +76 -0
- package/skills/swarm/SKILL.md +132 -0
- package/skills/swarm/refs/fan-out.md +104 -0
- package/skills/swarm/refs/independent-verification.md +86 -0
- package/skills/swarm/refs/receipts-and-evidence.md +69 -0
- package/skills/swarm/refs/recon-synthesis.md +87 -0
- package/skills/swarm/refs/ship-discipline.md +90 -0
- package/skills/wickedizer/SKILL.md +147 -0
- package/skills/wickedizer/refs/artifact-types.md +238 -0
- package/skills/wickedizer/refs/complexity.md +278 -0
- package/skills/wickedizer/refs/de-ai-pass.md +140 -0
- package/skills/wickedizer/refs/failure-modes.md +249 -0
- package/skills/wickedizer/refs/patterns.md +282 -0
- package/skills/wickedizer/refs/review.md +221 -0
- package/skills/wickedizer/refs/structure.md +282 -0
- package/skills/wickedizer/refs/voice-profile.md +125 -0
- package/skills/workflow/SKILL.md +132 -0
- package/skills/workflow/refs/archetype-adjustments.md +205 -0
- package/skills/workflow/refs/evidence.md +58 -0
- package/skills/workflow/refs/integration.md +130 -0
- package/skills/workflow/refs/risk-dimension-signals.md +260 -0
- package/skills/workflow/refs/scoring-rubric.md +49 -0
- package/skills/workflow/refs/signal-keywords.md +176 -0
- package/skills/workflow/refs/specialist-routing-rules.md +159 -0
- package/skills/worktrees/SKILL.md +87 -0
- package/skills/worktrees/refs/recipes.md +96 -0
|
@@ -0,0 +1,1300 @@
|
|
|
1
|
+
# wicked-loom — Cutover Phase (garden-side strangler migration) — Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Cut wicked-garden's in-process runtime over to the shipped `wicked-loom` CLI, **one surface at a time, lowest-risk first** — `resolve` → `gate` → `flow` — then add `wicked-loom` as the 5th required peer and introduce the garden archetype→flow-definition compiler (the §3.1 seam). Each cutover ships behind a **contract test** asserting the loom-shelled path produces results IDENTICAL to the in-process path it replaces (the strangler safety net), and stays **fail-soft during transition** (loom unresolvable → fall back to the still-present in-process code; gates still fail **closed**). The old in-process code is LEFT IN PLACE behind the shim — rollback is trivial. Deleting it is the **contract** phase (a later plan).
|
|
6
|
+
|
|
7
|
+
**Architecture:** Garden already invokes Python via `sh "${CLAUDE_PLUGIN_ROOT}/scripts/_python.sh" <script.py>` and peers via `npx <peer>`. The cutover adds one thin garden-side module — `scripts/_loom.py` — that resolves the loom CLI on a ladder mirroring the vault's (`WICKED_LOOM_BIN` → config → `PATH` → `node_modules/.bin` → `npx wicked-loom`, with `WICKED_LOOM_BIN=""` as the kill-switch) and runs it as a subprocess returning parsed JSON. Each cutover target (`resolve_vault`, `cross_check`/`gate_satisfied`, phase mechanics) gains a **try-loom-first, fall-back-to-in-process** branch gated by a single feature flag (`WICKED_LOOM_CUTOVER`, default `auto`). No in-process function is deleted; each grows a shim head. The flow-definition compiler (`scripts/crew/flow_compiler.py`) is a NEW pure module that reads `.claude-plugin/archetypes.json` + the `_HARD_GATE_PHASES` map and emits the §3.1 flow-definition JSON that `loom flow run` consumes.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3.9+ (stdlib only — mirrors garden's hook discipline; loom is reached as a subprocess, never imported). `pytest` (garden's existing suite; `testpaths = ["tests"]`, `tests/conftest.py` puts `scripts/` on `sys.path[0]`). Tests inject the loom subprocess runner (same pattern as `vault_gate._run`) so no test spawns a real loom or touches the network. The shipped loom CLI surface this plan targets:
|
|
10
|
+
|
|
11
|
+
- `loom resolve <peer>` → `{"peer","command":[...]|null}`, exit 0 iff resolvable (wicked-loom 0.1.0).
|
|
12
|
+
- `loom gate <produces> --scope S [--verifier-spec PATH] [--with-attestations]` → `{"gate": <verdict>}`, exit 0 iff `gate.satisfied` (wicked-loom 0.2.0 / conduct).
|
|
13
|
+
- `loom flow run <flow-def.json> [--state-dir D]` · `loom flow status <flow-id>` · `loom flow resume <flow-id>` → `{"flow": <state>}` (wicked-loom 0.2.0 / conduct).
|
|
14
|
+
|
|
15
|
+
**Decisions locked for this plan (override in review):**
|
|
16
|
+
- **loom resolution ladder = a faithful mirror of the vault ladder** (D5; §10 invariants). `WICKED_LOOM_BIN` env (empty = kill-switch) → `tool_preferences.wicked-loom` in `config.json` → `wicked-loom` on `PATH` → `node_modules/.bin/wicked-loom` → `npx --yes wicked-loom`. This is the exact shape of `vault_gate.resolve_vault`, lifted to a peer-neutral helper.
|
|
17
|
+
- **One cutover feature flag, `WICKED_LOOM_CUTOVER`** ∈ `{auto, on, off}`, default `auto`. `auto` = use loom **iff resolvable**, else in-process (the transition default). `on` = loom required for that surface (used by the contract tests to force the loom path). `off` = in-process only (instant rollback / kill-switch). Read once per call via a tiny `_loom.cutover_mode()` helper.
|
|
18
|
+
- **Garden never imports loom Python** — it shells `npx wicked-loom` (or the resolved bin) exactly as it shells the vault. Loom stays a sibling primitive; garden's plugin packaging stays decoupled from loom's Python env (D5).
|
|
19
|
+
- **Gate fail-closed posture is preserved verbatim.** The loom `gate` path and the in-process `gate_satisfied` path BOTH fail closed when the vault is unresolvable. Loom unresolvable for the *gate surface* falls back to the in-process gate (which itself fails closed) — never a vacuous pass (I2).
|
|
20
|
+
- **`flow` cutover is additive, not a replacement of `phase_manager`'s storage.** Garden keeps `phase_manager` as its project-state store; the `flow` cutover mirrors *advance/approve/park* decisions through `loom flow` and contract-tests that the park-at-hard-gate verdict matches `_HARD_GATE_PHASES`. The DomainStore-backed `ProjectState` is unchanged (it is part of the STAY surface; the contract phase decides its fate).
|
|
21
|
+
- **Target repos:** garden = `~/Projects/wicked-garden` (this plan edits it); loom = `~/Projects/wicked-loom` (consumed as a published peer `npx wicked-loom`, **not** edited here; conduct/0.2.0 must be published first — see Execution Handoff).
|
|
22
|
+
|
|
23
|
+
**Source material to read in wicked-garden before Task 1 (grounding):**
|
|
24
|
+
- `scripts/qe/vault_gate.py` — the `resolve_vault` ladder (lines 69–106), `_argv_for` (58–66), `_read_config_preference` (117–130), `cross_check` (170–213), `gate_satisfied` (220–293), and the CLI dispatch (304–348). The `resolve` and `gate` cutovers shim these.
|
|
25
|
+
- `scripts/crew/phase_manager.py` — `_HARD_GATE_PHASES` (287–296), `_is_hard_gate` (299–304), `approve_phase` (307–422), and the CLI (460–610). The `flow` cutover mirrors these decisions through `loom flow`.
|
|
26
|
+
- `.claude-plugin/archetypes.json` — `archetypes.{name}.{phases,produces,hitl}` and `$hitl_levels`. The flow compiler reads this.
|
|
27
|
+
- `docs/required-peers.md` — the four-peer table + "required at install, resilient at runtime" stance. Task 4 adds loom as the 5th row.
|
|
28
|
+
- `commands/setup.md` §2.5–2.7 — the per-peer verify pattern. Task 4 adds a loom verify step in the same shape.
|
|
29
|
+
- Plan A (`docs/plans/2026-06-08-wicked-loom-expand-compose.md`) and Plan B (`docs/plans/2026-06-08-wicked-loom-conduct.md`) — the exact loom CLI contract (commands, JSON output keys, exit codes) this plan shells to.
|
|
30
|
+
|
|
31
|
+
**Out of scope (DEFERRED — do NOT do in this plan):**
|
|
32
|
+
- The **CONTRACT phase** — deleting the now-dead in-process runtime code (the `→ loom` rows in spec §6). That is a separate, *later* plan, run only after this cutover is proven in production. This cutover deliberately LEAVES the in-process code in place behind the shim so rollback is `WICKED_LOOM_CUTOVER=off` (or a one-line revert).
|
|
33
|
+
- The **headless daemon / bus-consumer** (spec D3, §9 D-headless). `loom flow` is driven synchronously from garden; no unattended execution, no projector.
|
|
34
|
+
- Editing the loom repo. Conduct (0.2.0) is Plan B's deliverable; this plan assumes it is published and reachable via `npx wicked-loom`.
|
|
35
|
+
- The compiler↔loom synergy (spec §9): the garden `/wicked-garden:compile` emitter stays self-contained and resolves the vault directly via npx — untouched here.
|
|
36
|
+
|
|
37
|
+
**Bulletproof standards:** R1–R6 (no dead code, no bare panics, no magic values, no swallowed errors, no unbounded ops, no god functions) and T1–T6 (determinism, no sleep-based sync, isolation, single-assertion focus, descriptive names, provenance). The loom subprocess is injected everywhere so no unit test spawns a real loom or hits the network. Contract tests are the load-bearing artifact: each asserts `loom_path(x) == in_process_path(x)` for the same inputs.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## File Structure
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
wicked-garden/
|
|
45
|
+
├── scripts/
|
|
46
|
+
│ ├── _loom.py # NEW — peer-neutral loom CLI resolver + JSON subprocess runner
|
|
47
|
+
│ │ # + cutover_mode() flag. Mirrors vault_gate's ladder.
|
|
48
|
+
│ ├── qe/
|
|
49
|
+
│ │ └── vault_gate.py # MODIFY — resolve_vault grows a loom-first head (Task 1);
|
|
50
|
+
│ │ # cross_check grows a loom-gate head (Task 2).
|
|
51
|
+
│ │ # In-process bodies STAY as the fallback.
|
|
52
|
+
│ └── crew/
|
|
53
|
+
│ ├── flow_compiler.py # NEW — archetype catalog → flow-definition JSON (the §3.1 seam, Task 5)
|
|
54
|
+
│ └── phase_manager.py # MODIFY — approve_phase/advance grow a loom-flow mirror head (Task 3);
|
|
55
|
+
│ # in-process body STAYS.
|
|
56
|
+
├── hooks/scripts/
|
|
57
|
+
│ └── bootstrap.py # MODIFY — _check_loom_dependency added (Task 4, peer-detection portion only)
|
|
58
|
+
├── docs/
|
|
59
|
+
│ └── required-peers.md # MODIFY — add the 5th peer row + stance note (Task 4)
|
|
60
|
+
├── commands/
|
|
61
|
+
│ └── setup.md # MODIFY — add §2.8 Verify wicked-loom (Task 4)
|
|
62
|
+
├── .claude-plugin/
|
|
63
|
+
│ └── plugin.json # MODIFY — add wicked-loom pin to the peer description (Task 4)
|
|
64
|
+
└── tests/
|
|
65
|
+
├── crew/
|
|
66
|
+
│ ├── test_loom_resolver.py # NEW — _loom.py unit tests (Task 0)
|
|
67
|
+
│ ├── test_loom_resolve_contract.py # NEW — resolve cutover contract test (Task 1)
|
|
68
|
+
│ ├── test_loom_flow_contract.py # NEW — flow cutover contract test (Task 3)
|
|
69
|
+
│ └── test_flow_compiler.py # NEW — archetype→flow-def compiler tests (Task 5)
|
|
70
|
+
└── qe/
|
|
71
|
+
└── test_loom_gate_contract.py # NEW — gate cutover contract test (Task 2)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Responsibilities (one per file, justifying the layout):**
|
|
75
|
+
- `scripts/_loom.py` = *find loom + run it + read the flag* (the only file that knows how to reach the loom CLI). Peer-neutral resolution helper (mirrors `vault_gate.resolve_vault`), a JSON subprocess runner, and `cutover_mode()`. Pure of any surface logic — `resolve`/`gate`/`flow` shims call into it.
|
|
76
|
+
- `scripts/qe/vault_gate.py` (modified) = keeps both paths for the resolve + gate surfaces; the shim head tries loom, the unchanged body is the fallback. One responsibility per function still holds — the head is a 4-line guard, not a rewrite.
|
|
77
|
+
- `scripts/crew/flow_compiler.py` = *translate one archetype into a flow definition* (the §3.1 handoff contract). Pure, deterministic, reads the catalog + the hard-gate map; emits the JSON loom consumes. No I/O beyond reading the catalog.
|
|
78
|
+
- `scripts/crew/phase_manager.py` (modified) = keeps its DomainStore state authority; the shim head mirrors the advance/park decision through `loom flow` for parity, never replacing the disk write.
|
|
79
|
+
- `hooks/scripts/bootstrap.py` (modified) = adds one peer-detection function in the exact shape of `_check_vault_dependency` / `_check_bus_dependency`.
|
|
80
|
+
|
|
81
|
+
Why `_loom.py` is its own file and not folded into `vault_gate.py`: it is peer-neutral (used by the resolve, gate, AND flow cutovers) and must be importable from `scripts/` and `scripts/crew/` alike; co-locating it with the vault gate would couple three surfaces to one. Why the compiler is separate from `phase_manager.py`: the compiler is pure catalog→JSON with zero state; mixing it into the stateful phase manager would violate R6.
|
|
82
|
+
|
|
83
|
+
**The flow-definition shape this plan emits** (spec §3.1; consumed verbatim by `loom flow run`, per Plan B):
|
|
84
|
+
|
|
85
|
+
```jsonc
|
|
86
|
+
{
|
|
87
|
+
"flow_id": "build-<project>",
|
|
88
|
+
"phases": [
|
|
89
|
+
{ "name": "plan", "gate": null, "hitl": "none", "produces": [] },
|
|
90
|
+
{ "name": "implement", "gate": null, "hitl": "none", "produces": [] },
|
|
91
|
+
{ "name": "test", "gate": "produces:test-report", "hitl": "discrete:review", "produces": ["test-report"] },
|
|
92
|
+
{ "name": "review", "gate": "produces:verdict", "hitl": "hard:final-verdict","produces": ["verdict"] }
|
|
93
|
+
],
|
|
94
|
+
"peers_required": ["vault", "testing"],
|
|
95
|
+
"verifier_spec_ref": null
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Task 0: The loom CLI resolver + JSON runner + cutover flag (`scripts/_loom.py`)
|
|
102
|
+
|
|
103
|
+
**Files:**
|
|
104
|
+
- Create: `~/Projects/wicked-garden/scripts/_loom.py`
|
|
105
|
+
- Test: `~/Projects/wicked-garden/tests/crew/test_loom_resolver.py`
|
|
106
|
+
|
|
107
|
+
This is the foundation every cutover stands on: resolve the loom CLI on a ladder that mirrors the vault's, run it as a subprocess that returns parsed JSON, and read the `WICKED_LOOM_CUTOVER` flag. Ported in shape from `vault_gate.resolve_vault` / `_argv_for` / `_read_config_preference` / `_run`, generalized to "loom" and made injectable for tests (the `run` parameter, same as `vault_gate`).
|
|
108
|
+
|
|
109
|
+
- [ ] **Step 1: Write the failing test**
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import json
|
|
113
|
+
import os
|
|
114
|
+
import shutil
|
|
115
|
+
import sys
|
|
116
|
+
import unittest
|
|
117
|
+
from pathlib import Path
|
|
118
|
+
from unittest.mock import patch
|
|
119
|
+
|
|
120
|
+
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
121
|
+
if str(_REPO_ROOT / "scripts") not in sys.path:
|
|
122
|
+
sys.path.insert(0, str(_REPO_ROOT / "scripts"))
|
|
123
|
+
|
|
124
|
+
import _loom # noqa: E402
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ResolveLadderTests(unittest.TestCase):
|
|
128
|
+
def setUp(self):
|
|
129
|
+
self._saved = os.environ.get("WICKED_LOOM_BIN")
|
|
130
|
+
os.environ.pop("WICKED_LOOM_BIN", None)
|
|
131
|
+
|
|
132
|
+
def tearDown(self):
|
|
133
|
+
if self._saved is None:
|
|
134
|
+
os.environ.pop("WICKED_LOOM_BIN", None)
|
|
135
|
+
else:
|
|
136
|
+
os.environ["WICKED_LOOM_BIN"] = self._saved
|
|
137
|
+
|
|
138
|
+
def test_env_override_wins(self):
|
|
139
|
+
os.environ["WICKED_LOOM_BIN"] = "/opt/custom/loom"
|
|
140
|
+
self.assertEqual(_loom.resolve_loom(), ["/opt/custom/loom"])
|
|
141
|
+
|
|
142
|
+
def test_empty_env_is_killswitch(self):
|
|
143
|
+
os.environ["WICKED_LOOM_BIN"] = ""
|
|
144
|
+
self.assertIsNone(_loom.resolve_loom())
|
|
145
|
+
|
|
146
|
+
def test_mjs_override_invoked_via_node(self):
|
|
147
|
+
os.environ["WICKED_LOOM_BIN"] = "/some/loom.mjs"
|
|
148
|
+
self.assertEqual(_loom.resolve_loom(), ["node", "/some/loom.mjs"])
|
|
149
|
+
|
|
150
|
+
def test_path_lookup_when_no_env(self):
|
|
151
|
+
with patch.object(shutil, "which", side_effect=lambda b: "/usr/local/bin/wicked-loom" if b == "wicked-loom" else None):
|
|
152
|
+
self.assertEqual(_loom.resolve_loom(allow_npx=False), ["/usr/local/bin/wicked-loom"])
|
|
153
|
+
|
|
154
|
+
def test_npx_fallback_when_not_on_path(self):
|
|
155
|
+
with patch.object(shutil, "which", side_effect=lambda b: "/usr/bin/npx" if b == "npx" else None):
|
|
156
|
+
self.assertEqual(_loom.resolve_loom(), ["npx", "--yes", "wicked-loom"])
|
|
157
|
+
|
|
158
|
+
def test_loom_available_excludes_npx(self):
|
|
159
|
+
with patch.object(shutil, "which", side_effect=lambda b: "/usr/bin/npx" if b == "npx" else None):
|
|
160
|
+
self.assertFalse(_loom.loom_available())
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class RunTests(unittest.TestCase):
|
|
164
|
+
def test_run_returns_parsed_json_on_success(self):
|
|
165
|
+
def fake_run(prefix, args, timeout):
|
|
166
|
+
return {"exit_code": 0, "stdout": '{"peer":"vault","command":["npx","wicked-vault"]}',
|
|
167
|
+
"stderr": "", "error": None}
|
|
168
|
+
with patch.object(_loom, "resolve_loom", return_value=["wicked-loom"]):
|
|
169
|
+
out = _loom.run_json(["resolve", "vault"], _run=fake_run)
|
|
170
|
+
self.assertEqual(out["exit_code"], 0)
|
|
171
|
+
self.assertEqual(out["json"]["command"], ["npx", "wicked-vault"])
|
|
172
|
+
|
|
173
|
+
def test_run_unresolvable_reports_error_not_raise(self):
|
|
174
|
+
with patch.object(_loom, "resolve_loom", return_value=None):
|
|
175
|
+
out = _loom.run_json(["resolve", "vault"])
|
|
176
|
+
self.assertIsNone(out["json"])
|
|
177
|
+
self.assertEqual(out["error"], "wicked-loom not resolvable")
|
|
178
|
+
|
|
179
|
+
def test_run_non_json_output_is_error_not_raise(self):
|
|
180
|
+
def fake_run(prefix, args, timeout):
|
|
181
|
+
return {"exit_code": 0, "stdout": "not json", "stderr": "", "error": None}
|
|
182
|
+
with patch.object(_loom, "resolve_loom", return_value=["wicked-loom"]):
|
|
183
|
+
out = _loom.run_json(["doctor"], _run=fake_run)
|
|
184
|
+
self.assertIsNone(out["json"])
|
|
185
|
+
self.assertIn("non-JSON", out["error"])
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class CutoverModeTests(unittest.TestCase):
|
|
189
|
+
def setUp(self):
|
|
190
|
+
self._saved = os.environ.get("WICKED_LOOM_CUTOVER")
|
|
191
|
+
os.environ.pop("WICKED_LOOM_CUTOVER", None)
|
|
192
|
+
|
|
193
|
+
def tearDown(self):
|
|
194
|
+
if self._saved is None:
|
|
195
|
+
os.environ.pop("WICKED_LOOM_CUTOVER", None)
|
|
196
|
+
else:
|
|
197
|
+
os.environ["WICKED_LOOM_CUTOVER"] = self._saved
|
|
198
|
+
|
|
199
|
+
def test_default_is_auto(self):
|
|
200
|
+
self.assertEqual(_loom.cutover_mode(), "auto")
|
|
201
|
+
|
|
202
|
+
def test_explicit_off(self):
|
|
203
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "off"
|
|
204
|
+
self.assertEqual(_loom.cutover_mode(), "off")
|
|
205
|
+
|
|
206
|
+
def test_unknown_value_falls_back_to_auto(self):
|
|
207
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "frobnicate"
|
|
208
|
+
self.assertEqual(_loom.cutover_mode(), "auto")
|
|
209
|
+
|
|
210
|
+
def test_use_loom_off_is_false(self):
|
|
211
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "off"
|
|
212
|
+
self.assertFalse(_loom.use_loom())
|
|
213
|
+
|
|
214
|
+
def test_use_loom_auto_true_only_when_resolvable(self):
|
|
215
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "auto"
|
|
216
|
+
with patch.object(_loom, "resolve_loom", return_value=None):
|
|
217
|
+
self.assertFalse(_loom.use_loom())
|
|
218
|
+
with patch.object(_loom, "resolve_loom", return_value=["wicked-loom"]):
|
|
219
|
+
self.assertTrue(_loom.use_loom())
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
unittest.main()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
227
|
+
|
|
228
|
+
Run: `cd ~/Projects/wicked-garden && python3 -m pytest tests/crew/test_loom_resolver.py -v`
|
|
229
|
+
Expected: FAIL — `ModuleNotFoundError: No module named '_loom'`
|
|
230
|
+
|
|
231
|
+
- [ ] **Step 3: Write `scripts/_loom.py`**
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
#!/usr/bin/env python3
|
|
235
|
+
"""_loom.py — resolve + run the wicked-loom CLI, peer-neutral.
|
|
236
|
+
|
|
237
|
+
The garden's strangler shim toward wicked-loom. Garden NEVER imports loom's
|
|
238
|
+
Python; it shells the published ``wicked-loom`` CLI exactly as it shells
|
|
239
|
+
``wicked-vault``. This module owns three things and nothing else:
|
|
240
|
+
|
|
241
|
+
1. resolve_loom() — the runtime resolution ladder, a faithful mirror of
|
|
242
|
+
vault_gate.resolve_vault: WICKED_LOOM_BIN env (empty = kill-switch) ->
|
|
243
|
+
config tool_preferences.wicked-loom -> PATH -> node_modules/.bin -> npx.
|
|
244
|
+
2. run_json() — run a loom subcommand, return {exit_code, json, stdout,
|
|
245
|
+
stderr, error}. Never raises (R4 — surface as data). Non-JSON / not-found
|
|
246
|
+
/ timeout all come back as error, json=None.
|
|
247
|
+
3. cutover_mode() / use_loom() — the WICKED_LOOM_CUTOVER feature flag
|
|
248
|
+
({auto,on,off}, default auto) that lets every cutover try loom first and
|
|
249
|
+
fall back to the in-process path (the transition default), or be killed
|
|
250
|
+
(off) for instant rollback.
|
|
251
|
+
|
|
252
|
+
Stdlib-only. Cross-platform (argv lists, shutil.which, no shell).
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
from __future__ import annotations
|
|
256
|
+
|
|
257
|
+
import json
|
|
258
|
+
import os
|
|
259
|
+
import shutil
|
|
260
|
+
import subprocess # noqa: S404 — argv lists only, shell=False
|
|
261
|
+
from pathlib import Path
|
|
262
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
263
|
+
|
|
264
|
+
_CONFIG_PATH = Path.home() / ".something-wicked" / "wicked-garden" / "config.json"
|
|
265
|
+
_DEFAULT_TIMEOUT = 120
|
|
266
|
+
_CUTOVER_MODES = ("auto", "on", "off")
|
|
267
|
+
_PACKAGE = "wicked-loom"
|
|
268
|
+
_ENV_BIN = "WICKED_LOOM_BIN"
|
|
269
|
+
_ENV_FLAG = "WICKED_LOOM_CUTOVER"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _argv_for(target: str) -> List[str]:
|
|
273
|
+
"""A .mjs/.js path is a script -> invoke via node; else run directly."""
|
|
274
|
+
if target.endswith((".mjs", ".js")):
|
|
275
|
+
return ["node", target]
|
|
276
|
+
return [target]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _read_config_preference(key: str) -> Optional[str]:
|
|
280
|
+
"""Read ``tool_preferences.{key}`` from config.json; None on any error."""
|
|
281
|
+
try:
|
|
282
|
+
if not _CONFIG_PATH.exists():
|
|
283
|
+
return None
|
|
284
|
+
data = json.loads(_CONFIG_PATH.read_text(encoding="utf-8"))
|
|
285
|
+
prefs = data.get("tool_preferences")
|
|
286
|
+
if isinstance(prefs, dict):
|
|
287
|
+
value = prefs.get(key)
|
|
288
|
+
if isinstance(value, str) and value.strip():
|
|
289
|
+
return value.strip()
|
|
290
|
+
return None
|
|
291
|
+
except (json.JSONDecodeError, OSError):
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def resolve_loom(*, allow_npx: bool = True,
|
|
296
|
+
project_dir: Optional[Path] = None) -> Optional[List[str]]:
|
|
297
|
+
"""Return the argv prefix that invokes wicked-loom, or None.
|
|
298
|
+
|
|
299
|
+
Ladder (first hit wins), mirroring vault_gate.resolve_vault:
|
|
300
|
+
1. WICKED_LOOM_BIN env. Set-but-empty is the kill-switch -> None.
|
|
301
|
+
2. tool_preferences.wicked-loom in config.json.
|
|
302
|
+
3. wicked-loom on PATH.
|
|
303
|
+
4. project-local node_modules/.bin/wicked-loom.
|
|
304
|
+
5. npx --yes wicked-loom (only when allow_npx).
|
|
305
|
+
"""
|
|
306
|
+
if _ENV_BIN in os.environ:
|
|
307
|
+
env = os.environ[_ENV_BIN].strip()
|
|
308
|
+
return _argv_for(env) if env else None # empty == kill-switch
|
|
309
|
+
|
|
310
|
+
pref = _read_config_preference(_PACKAGE)
|
|
311
|
+
if pref:
|
|
312
|
+
return _argv_for(pref)
|
|
313
|
+
|
|
314
|
+
found = shutil.which(_PACKAGE)
|
|
315
|
+
if found:
|
|
316
|
+
return [found]
|
|
317
|
+
|
|
318
|
+
base = Path(project_dir) if project_dir else Path.cwd()
|
|
319
|
+
local = base / "node_modules" / ".bin" / _PACKAGE
|
|
320
|
+
if local.exists():
|
|
321
|
+
return [str(local)]
|
|
322
|
+
|
|
323
|
+
if allow_npx and shutil.which("npx"):
|
|
324
|
+
return ["npx", "--yes", _PACKAGE]
|
|
325
|
+
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def loom_available(project_dir: Optional[Path] = None) -> bool:
|
|
330
|
+
"""True iff a concrete loom install resolves (not the npx last-resort).
|
|
331
|
+
The signal setup + bootstrap use to decide whether the peer is installed."""
|
|
332
|
+
return resolve_loom(allow_npx=False, project_dir=project_dir) is not None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _default_run(prefix: List[str], args: List[str], timeout: int) -> Dict[str, Any]:
|
|
336
|
+
try:
|
|
337
|
+
proc = subprocess.run( # noqa: S603 — argv list, shell=False
|
|
338
|
+
prefix + args, capture_output=True, text=True, timeout=timeout,
|
|
339
|
+
)
|
|
340
|
+
return {"exit_code": proc.returncode, "stdout": proc.stdout,
|
|
341
|
+
"stderr": proc.stderr, "error": None}
|
|
342
|
+
except FileNotFoundError:
|
|
343
|
+
return {"exit_code": None, "stdout": "", "stderr": "",
|
|
344
|
+
"error": "loom executable not found"}
|
|
345
|
+
except subprocess.TimeoutExpired:
|
|
346
|
+
return {"exit_code": None, "stdout": "", "stderr": "",
|
|
347
|
+
"error": f"loom call exceeded {timeout}s"}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
Runner = Callable[..., Dict[str, Any]]
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def run_json(args: List[str], *, timeout: int = _DEFAULT_TIMEOUT,
|
|
354
|
+
project_dir: Optional[Path] = None,
|
|
355
|
+
_run: Runner = _default_run) -> Dict[str, Any]:
|
|
356
|
+
"""Run ``loom <args>``; return {exit_code, json, stdout, stderr, error}.
|
|
357
|
+
|
|
358
|
+
Never raises (R4). json is the parsed stdout or None (unresolvable,
|
|
359
|
+
not-found, timeout, or non-JSON output).
|
|
360
|
+
"""
|
|
361
|
+
prefix = resolve_loom(project_dir=project_dir)
|
|
362
|
+
if prefix is None:
|
|
363
|
+
return {"exit_code": None, "json": None, "stdout": "", "stderr": "",
|
|
364
|
+
"error": "wicked-loom not resolvable"}
|
|
365
|
+
run = _run(prefix, args, timeout)
|
|
366
|
+
if run["error"] is not None:
|
|
367
|
+
return {"exit_code": None, "json": None, "stdout": "", "stderr": "",
|
|
368
|
+
"error": run["error"]}
|
|
369
|
+
try:
|
|
370
|
+
parsed = json.loads(run["stdout"]) if (run["stdout"] or "").strip() else {}
|
|
371
|
+
except json.JSONDecodeError:
|
|
372
|
+
return {"exit_code": run["exit_code"], "json": None,
|
|
373
|
+
"stdout": run["stdout"], "stderr": run["stderr"],
|
|
374
|
+
"error": "loom returned non-JSON output"}
|
|
375
|
+
return {"exit_code": run["exit_code"], "json": parsed,
|
|
376
|
+
"stdout": run["stdout"], "stderr": run["stderr"], "error": None}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def cutover_mode() -> str:
|
|
380
|
+
"""The WICKED_LOOM_CUTOVER flag, normalized. Unknown -> 'auto'."""
|
|
381
|
+
raw = os.environ.get(_ENV_FLAG, "").strip().lower()
|
|
382
|
+
return raw if raw in _CUTOVER_MODES else "auto"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def use_loom(*, project_dir: Optional[Path] = None) -> bool:
|
|
386
|
+
"""Should this call go through loom?
|
|
387
|
+
|
|
388
|
+
off -> never. on -> always (caller must handle an unresolvable loom).
|
|
389
|
+
auto -> only when loom resolves (the transition default: fall back to the
|
|
390
|
+
in-process path otherwise).
|
|
391
|
+
"""
|
|
392
|
+
mode = cutover_mode()
|
|
393
|
+
if mode == "off":
|
|
394
|
+
return False
|
|
395
|
+
if mode == "on":
|
|
396
|
+
return True
|
|
397
|
+
return resolve_loom(project_dir=project_dir) is not None
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
__all__ = ["resolve_loom", "loom_available", "run_json",
|
|
401
|
+
"cutover_mode", "use_loom"]
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
405
|
+
|
|
406
|
+
Run: `cd ~/Projects/wicked-garden && python3 -m pytest tests/crew/test_loom_resolver.py -v`
|
|
407
|
+
Expected: PASS (14 tests).
|
|
408
|
+
|
|
409
|
+
- [ ] **Step 5: Commit**
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
cd ~/Projects/wicked-garden
|
|
413
|
+
git add scripts/_loom.py tests/crew/test_loom_resolver.py
|
|
414
|
+
git commit -m "feat(loom-cutover): loom CLI resolver + JSON runner + cutover flag"
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Task 1: Cut over `resolve` — garden's peer resolution shells to `loom resolve`
|
|
420
|
+
|
|
421
|
+
**Files:**
|
|
422
|
+
- Modify: `~/Projects/wicked-garden/scripts/qe/vault_gate.py` (`resolve_vault`, lines 69–106)
|
|
423
|
+
- Test: `~/Projects/wicked-garden/tests/crew/test_loom_resolve_contract.py`
|
|
424
|
+
|
|
425
|
+
Lowest-risk surface first (spec §7: `resolve` → `gate` → `flow`). `resolve_vault` grows a loom-first head: when `use_loom()` is true, shell `loom resolve vault` and use its `command` array; otherwise (or on any loom error) fall through to the unchanged in-process ladder. The **contract test** asserts the loom path returns the SAME argv the in-process path returns for the same environment.
|
|
426
|
+
|
|
427
|
+
- [ ] **Step 1: Write the failing contract test**
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
import os
|
|
431
|
+
import shutil
|
|
432
|
+
import sys
|
|
433
|
+
import unittest
|
|
434
|
+
from pathlib import Path
|
|
435
|
+
from unittest.mock import patch
|
|
436
|
+
|
|
437
|
+
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
438
|
+
for _p in (_REPO_ROOT / "scripts", _REPO_ROOT / "scripts" / "qe"):
|
|
439
|
+
if str(_p) not in sys.path:
|
|
440
|
+
sys.path.insert(0, str(_p))
|
|
441
|
+
|
|
442
|
+
import _loom # noqa: E402
|
|
443
|
+
import vault_gate as vg # noqa: E402
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class ResolveCutoverContract(unittest.TestCase):
|
|
447
|
+
"""Strangler safety net: the loom-shelled resolve path must return the
|
|
448
|
+
SAME argv the in-process resolve_vault returns for the same inputs."""
|
|
449
|
+
|
|
450
|
+
def setUp(self):
|
|
451
|
+
for v in ("WICKED_VAULT_BIN", "WICKED_LOOM_BIN", "WICKED_LOOM_CUTOVER"):
|
|
452
|
+
os.environ.pop(v, None)
|
|
453
|
+
|
|
454
|
+
def test_loom_resolve_matches_in_process_for_npx_case(self):
|
|
455
|
+
# In-process: no env, not on PATH -> npx --yes wicked-vault.
|
|
456
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "off"
|
|
457
|
+
with patch.object(shutil, "which", side_effect=lambda b: "/usr/bin/npx" if b == "npx" else None):
|
|
458
|
+
in_proc = vg.resolve_vault()
|
|
459
|
+
self.assertEqual(in_proc, ["npx", "--yes", "wicked-vault"])
|
|
460
|
+
|
|
461
|
+
# Loom path: force loom on; loom resolve vault returns the SAME argv.
|
|
462
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "on"
|
|
463
|
+
|
|
464
|
+
def fake_run(prefix, args, timeout):
|
|
465
|
+
return {"exit_code": 0,
|
|
466
|
+
"stdout": '{"peer":"vault","command":["npx","--yes","wicked-vault"]}',
|
|
467
|
+
"stderr": "", "error": None}
|
|
468
|
+
with patch.object(_loom, "resolve_loom", return_value=["wicked-loom"]), \
|
|
469
|
+
patch.object(_loom, "_default_run", fake_run):
|
|
470
|
+
via_loom = vg.resolve_vault()
|
|
471
|
+
self.assertEqual(via_loom, in_proc)
|
|
472
|
+
|
|
473
|
+
def test_loom_unresolvable_falls_back_to_in_process(self):
|
|
474
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "auto"
|
|
475
|
+
with patch.object(_loom, "resolve_loom", return_value=None), \
|
|
476
|
+
patch.object(shutil, "which", side_effect=lambda b: "/usr/bin/npx" if b == "npx" else None):
|
|
477
|
+
via = vg.resolve_vault()
|
|
478
|
+
# auto + loom unresolvable -> in-process path used (fail-soft).
|
|
479
|
+
self.assertEqual(via, ["npx", "--yes", "wicked-vault"])
|
|
480
|
+
|
|
481
|
+
def test_loom_killswitch_env_still_honored_through_loom(self):
|
|
482
|
+
# WICKED_VAULT_BIN="" is the vault kill-switch; the loom resolve path
|
|
483
|
+
# must surface the same None (loom resolve returns command=null).
|
|
484
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "on"
|
|
485
|
+
os.environ["WICKED_VAULT_BIN"] = ""
|
|
486
|
+
|
|
487
|
+
def fake_run(prefix, args, timeout):
|
|
488
|
+
return {"exit_code": 1, "stdout": '{"peer":"vault","command":null}',
|
|
489
|
+
"stderr": "", "error": None}
|
|
490
|
+
with patch.object(_loom, "resolve_loom", return_value=["wicked-loom"]), \
|
|
491
|
+
patch.object(_loom, "_default_run", fake_run):
|
|
492
|
+
self.assertIsNone(vg.resolve_vault())
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
if __name__ == "__main__":
|
|
496
|
+
unittest.main()
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
500
|
+
|
|
501
|
+
Run: `cd ~/Projects/wicked-garden && python3 -m pytest tests/crew/test_loom_resolve_contract.py -v`
|
|
502
|
+
Expected: FAIL — `test_loom_resolve_matches_in_process_for_npx_case` (and the others) fail because `resolve_vault` does not yet consult loom; the `on` path still runs in-process and ignores the mocked loom output.
|
|
503
|
+
|
|
504
|
+
- [ ] **Step 3: Edit `vault_gate.py` — add the loom-first head to `resolve_vault`**
|
|
505
|
+
|
|
506
|
+
Add the import near the top of the file (after the existing stdlib imports, before `_CONFIG_PATH`):
|
|
507
|
+
|
|
508
|
+
```python
|
|
509
|
+
# Strangler shim toward wicked-loom (cutover phase). The in-process body below
|
|
510
|
+
# STAYS as the fallback; loom is tried first only when the cutover flag allows.
|
|
511
|
+
try:
|
|
512
|
+
import _loom # scripts/ is on sys.path for hook + CLI invocations
|
|
513
|
+
except ImportError: # pragma: no cover — loom shim absent => pure in-process
|
|
514
|
+
_loom = None # type: ignore
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Then insert the loom head at the very top of `resolve_vault` (before the existing `if "WICKED_VAULT_BIN" in os.environ:` block at line 86):
|
|
518
|
+
|
|
519
|
+
```python
|
|
520
|
+
# --- loom cutover head (resolve surface) ---------------------------------
|
|
521
|
+
# Try loom first iff the cutover flag allows AND loom is reachable. Any loom
|
|
522
|
+
# error falls through to the unchanged in-process ladder (fail-soft, §7/§10).
|
|
523
|
+
if _loom is not None and allow_npx and _loom.use_loom(project_dir=project_dir):
|
|
524
|
+
out = _loom.run_json(["resolve", "vault"], project_dir=project_dir)
|
|
525
|
+
if out["error"] is None and isinstance(out.get("json"), dict):
|
|
526
|
+
# loom resolve returns {"peer","command":[...]|null}; null == the
|
|
527
|
+
# vault kill-switch / unresolvable, which we surface as None.
|
|
528
|
+
return out["json"].get("command") # list[str] or None
|
|
529
|
+
# loom errored -> fall through to in-process (transition fail-soft).
|
|
530
|
+
# -------------------------------------------------------------------------
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
> Implementer note: `allow_npx` guards the loom head because `vault_available()` calls `resolve_vault(allow_npx=False)` to detect a *concrete* install — that probe must stay in-process (loom's `resolve` would report the npx fallback as resolvable and corrupt the "installed" signal). Keeping the loom head behind `allow_npx` preserves `vault_available()`'s exact semantics. If a future loom `resolve --no-npx` lands, revisit; for now this is the faithful boundary.
|
|
534
|
+
|
|
535
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
536
|
+
|
|
537
|
+
Run: `cd ~/Projects/wicked-garden && python3 -m pytest tests/crew/test_loom_resolve_contract.py tests/qe/test_vault_gate.py -v`
|
|
538
|
+
Expected: PASS — the 3 new contract tests AND the existing `test_vault_gate.py` suite (the in-process path is untouched when the flag is `off`/`auto`-unresolvable, which is the default in CI).
|
|
539
|
+
|
|
540
|
+
- [ ] **Step 5: Commit**
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
cd ~/Projects/wicked-garden
|
|
544
|
+
git add scripts/qe/vault_gate.py tests/crew/test_loom_resolve_contract.py
|
|
545
|
+
git commit -m "feat(loom-cutover): resolve_vault shells to loom resolve, fail-soft to in-process"
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Task 2: Cut over `gate` — garden's vault gate shells to `loom gate`
|
|
551
|
+
|
|
552
|
+
**Files:**
|
|
553
|
+
- Modify: `~/Projects/wicked-garden/scripts/qe/vault_gate.py` (`cross_check`, lines 170–213)
|
|
554
|
+
- Test: `~/Projects/wicked-garden/tests/qe/test_loom_gate_contract.py`
|
|
555
|
+
|
|
556
|
+
Second surface (spec §7). `cross_check` grows a loom-first head: when `use_loom()` is true, shell `loom gate <phase> --scope <scope> [--with-attestations] [--verifier-spec ...]` and map its `{"gate": <verdict>}` onto `cross_check`'s existing return shape; otherwise fall through to the unchanged in-process `_run`+parse. **`gate_satisfied` is unchanged** — it calls `cross_check`, so it transparently inherits the loom path while keeping its fail-closed posture (loom gate also fails closed; if loom is unreachable in `auto`, the in-process gate runs and itself fails closed — never a vacuous pass, I2). The contract test asserts: identical `overall` for PASS and REJECT, and identical fail-closed verdict when the vault is unresolvable on both paths.
|
|
557
|
+
|
|
558
|
+
- [ ] **Step 1: Write the failing contract test**
|
|
559
|
+
|
|
560
|
+
```python
|
|
561
|
+
import os
|
|
562
|
+
import sys
|
|
563
|
+
import unittest
|
|
564
|
+
from pathlib import Path
|
|
565
|
+
from unittest.mock import patch
|
|
566
|
+
|
|
567
|
+
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
568
|
+
for _p in (_REPO_ROOT / "scripts", _REPO_ROOT / "scripts" / "qe"):
|
|
569
|
+
if str(_p) not in sys.path:
|
|
570
|
+
sys.path.insert(0, str(_p))
|
|
571
|
+
|
|
572
|
+
import _loom # noqa: E402
|
|
573
|
+
import vault_gate as vg # noqa: E402
|
|
574
|
+
|
|
575
|
+
_PROJECT = Path("/tmp/proj-loom-gate")
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class GateCutoverContract(unittest.TestCase):
|
|
579
|
+
"""Strangler safety net: the loom-shelled gate verdict must match the
|
|
580
|
+
in-process cross_check verdict (same overall + satisfied) for PASS,
|
|
581
|
+
REJECT, and the fail-closed-when-vault-absent case."""
|
|
582
|
+
|
|
583
|
+
def setUp(self):
|
|
584
|
+
for v in ("WICKED_VAULT_BIN", "WICKED_LOOM_BIN", "WICKED_LOOM_CUTOVER"):
|
|
585
|
+
os.environ.pop(v, None)
|
|
586
|
+
|
|
587
|
+
def _loom_gate_runner(self, overall):
|
|
588
|
+
def fake_run(prefix, args, timeout):
|
|
589
|
+
import json
|
|
590
|
+
verdict = {"satisfied": overall == "PASS", "overall": overall,
|
|
591
|
+
"gate": "vault-cross-check"}
|
|
592
|
+
return {"exit_code": 0 if overall == "PASS" else 1,
|
|
593
|
+
"stdout": json.dumps({"gate": verdict}), "stderr": "", "error": None}
|
|
594
|
+
return fake_run
|
|
595
|
+
|
|
596
|
+
def test_loom_pass_matches_in_process_pass(self):
|
|
597
|
+
# In-process PASS (stub the vault subprocess).
|
|
598
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "off"
|
|
599
|
+
with patch.object(vg, "resolve_vault", return_value=["wicked-vault"]), \
|
|
600
|
+
patch.object(vg, "_run", return_value={"exit_code": 0, "error": None,
|
|
601
|
+
"stdout": '{"overall":"PASS"}', "stderr": ""}):
|
|
602
|
+
in_proc = vg.cross_check("build-1", "test", project_dir=_PROJECT)
|
|
603
|
+
|
|
604
|
+
# Loom PASS.
|
|
605
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "on"
|
|
606
|
+
with patch.object(_loom, "resolve_loom", return_value=["wicked-loom"]), \
|
|
607
|
+
patch.object(_loom, "_default_run", self._loom_gate_runner("PASS")):
|
|
608
|
+
via_loom = vg.cross_check("build-1", "test", project_dir=_PROJECT)
|
|
609
|
+
|
|
610
|
+
self.assertEqual(via_loom["overall"], in_proc["overall"])
|
|
611
|
+
self.assertTrue(via_loom["available"])
|
|
612
|
+
|
|
613
|
+
def test_loom_reject_matches_in_process_reject(self):
|
|
614
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "on"
|
|
615
|
+
with patch.object(_loom, "resolve_loom", return_value=["wicked-loom"]), \
|
|
616
|
+
patch.object(_loom, "_default_run", self._loom_gate_runner("REJECT")):
|
|
617
|
+
via_loom = vg.cross_check("build-1", "test", project_dir=_PROJECT)
|
|
618
|
+
self.assertEqual(via_loom["overall"], "REJECT")
|
|
619
|
+
|
|
620
|
+
def test_gate_fails_closed_when_loom_errors_and_vault_absent(self):
|
|
621
|
+
# auto + loom unresolvable -> in-process gate runs; vault also absent
|
|
622
|
+
# -> fail closed. The loom error never invents a PASS (I2).
|
|
623
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "auto"
|
|
624
|
+
with patch.object(_loom, "resolve_loom", return_value=None), \
|
|
625
|
+
patch.object(vg, "resolve_vault", return_value=None):
|
|
626
|
+
verdict = vg.gate_satisfied(_PROJECT, "build-1", "test")
|
|
627
|
+
self.assertFalse(verdict["satisfied"])
|
|
628
|
+
self.assertEqual(verdict["gate"], "unavailable")
|
|
629
|
+
|
|
630
|
+
def test_loom_with_attestations_forwarded(self):
|
|
631
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "on"
|
|
632
|
+
seen = {}
|
|
633
|
+
|
|
634
|
+
def fake_run(prefix, args, timeout):
|
|
635
|
+
import json
|
|
636
|
+
seen["args"] = args
|
|
637
|
+
return {"exit_code": 0, "stdout": json.dumps(
|
|
638
|
+
{"gate": {"satisfied": True, "overall": "PASS"}}), "stderr": "", "error": None}
|
|
639
|
+
with patch.object(_loom, "resolve_loom", return_value=["wicked-loom"]), \
|
|
640
|
+
patch.object(_loom, "_default_run", fake_run):
|
|
641
|
+
vg.cross_check("build-1", "review", project_dir=_PROJECT, with_attestations=True)
|
|
642
|
+
self.assertIn("--with-attestations", seen["args"])
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
if __name__ == "__main__":
|
|
646
|
+
unittest.main()
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
650
|
+
|
|
651
|
+
Run: `cd ~/Projects/wicked-garden && python3 -m pytest tests/qe/test_loom_gate_contract.py -v`
|
|
652
|
+
Expected: FAIL — the loom-path tests fail because `cross_check` does not yet consult loom.
|
|
653
|
+
|
|
654
|
+
- [ ] **Step 3: Edit `vault_gate.py` — add the loom-first head to `cross_check`**
|
|
655
|
+
|
|
656
|
+
Insert at the top of `cross_check` (immediately after the docstring, before `args = ["cross-check", ...]` at line 188). Reuse the module-level `_loom` import added in Task 1:
|
|
657
|
+
|
|
658
|
+
```python
|
|
659
|
+
# --- loom cutover head (gate surface) ------------------------------------
|
|
660
|
+
# Try loom first iff the cutover flag allows AND loom is reachable. Maps
|
|
661
|
+
# loom's {"gate": <verdict>} onto cross_check's return shape. Any loom error
|
|
662
|
+
# falls through to the in-process re-derivation (which itself fails closed).
|
|
663
|
+
if _loom is not None and _loom.use_loom(project_dir=project_dir):
|
|
664
|
+
loom_args = ["gate", phase, "--scope", scope]
|
|
665
|
+
if with_attestations:
|
|
666
|
+
loom_args.append("--with-attestations")
|
|
667
|
+
out = _loom.run_json(loom_args, project_dir=project_dir, timeout=timeout)
|
|
668
|
+
if out["error"] is None and isinstance(out.get("json"), dict):
|
|
669
|
+
verdict = out["json"].get("gate") or {}
|
|
670
|
+
gate_kind = verdict.get("gate")
|
|
671
|
+
if gate_kind == "unavailable":
|
|
672
|
+
# loom reached but the vault is unresolvable behind it -> the
|
|
673
|
+
# gate is unavailable; fail closed exactly as in-process (I2).
|
|
674
|
+
return {"available": False, "overall": "ERROR",
|
|
675
|
+
"error": verdict.get("error", "vault unavailable via loom"),
|
|
676
|
+
"exit_code": out.get("exit_code")}
|
|
677
|
+
return {
|
|
678
|
+
"available": True,
|
|
679
|
+
"overall": verdict.get("overall", "ERROR"),
|
|
680
|
+
"exit_code": out.get("exit_code"),
|
|
681
|
+
"claims": verdict.get("claims", []),
|
|
682
|
+
"mode": verdict.get("mode"),
|
|
683
|
+
"contract_version": verdict.get("contract_version"),
|
|
684
|
+
"detail": verdict.get("detail"),
|
|
685
|
+
"raw": verdict,
|
|
686
|
+
}
|
|
687
|
+
# loom errored -> fall through to in-process re-derivation (fail-soft).
|
|
688
|
+
# -------------------------------------------------------------------------
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
> Implementer note: the `verifier_spec` forwarding (`--verifier-spec PATH`) is wired when the flow compiler (Task 5) starts attaching `verifier_spec_ref`; `cross_check`'s current garden signature has no `verifier_spec` parameter, so this head forwards only `--scope`/`--phase`/`--with-attestations` — the exact arguments the in-process path builds (lines 188–190). Do NOT add a `verifier_spec` parameter to `cross_check` in this task (out of scope; it belongs with the compiler handoff). The loom gate is fail-soft on a missing verifier spec by construction (I3), so omitting it is safe.
|
|
692
|
+
|
|
693
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
694
|
+
|
|
695
|
+
Run: `cd ~/Projects/wicked-garden && python3 -m pytest tests/qe/test_loom_gate_contract.py tests/qe/test_vault_gate.py -v`
|
|
696
|
+
Expected: PASS — the 4 new contract tests AND the existing `test_vault_gate.py` (in-process path unchanged when the flag is off/auto-unresolvable).
|
|
697
|
+
|
|
698
|
+
- [ ] **Step 5: Commit**
|
|
699
|
+
|
|
700
|
+
```bash
|
|
701
|
+
cd ~/Projects/wicked-garden
|
|
702
|
+
git add scripts/qe/vault_gate.py tests/qe/test_loom_gate_contract.py
|
|
703
|
+
git commit -m "feat(loom-cutover): cross_check shells to loom gate, fail-closed preserved"
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
## Task 3: Cut over `flow` — garden's phase advance/park mirrors `loom flow`
|
|
709
|
+
|
|
710
|
+
**Files:**
|
|
711
|
+
- Modify: `~/Projects/wicked-garden/scripts/crew/phase_manager.py` (`approve_phase`, lines 307–422; `_is_hard_gate`, 299–304)
|
|
712
|
+
- Test: `~/Projects/wicked-garden/tests/crew/test_loom_flow_contract.py`
|
|
713
|
+
|
|
714
|
+
Highest-risk surface, done last (spec §7). `phase_manager` keeps its DomainStore `ProjectState` authority — the `flow` cutover does NOT replace storage. Instead, `approve_phase` grows a loom **mirror head**: when `use_loom()` is true, it asks loom whether the (archetype, phase) is a hard gate by consulting the loom flow runner's verdict, and the **contract test** asserts loom's park-at-hard-gate decision matches `_HARD_GATE_PHASES` for every archetype. This proves the two phase engines agree on the load-bearing decision (where the human-in-the-loop stop is) before any later plan moves storage onto loom.
|
|
715
|
+
|
|
716
|
+
The mirror is read-only parity: the in-process hard-gate enforcement (the `ValueError` guard at lines 332–351) is unchanged and remains authoritative during the cutover. Loom is consulted to *cross-check the decision*, not to gate the write.
|
|
717
|
+
|
|
718
|
+
- [ ] **Step 1: Write the failing contract test**
|
|
719
|
+
|
|
720
|
+
```python
|
|
721
|
+
import os
|
|
722
|
+
import sys
|
|
723
|
+
import unittest
|
|
724
|
+
from pathlib import Path
|
|
725
|
+
from unittest.mock import patch
|
|
726
|
+
|
|
727
|
+
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
728
|
+
for _p in (_REPO_ROOT / "scripts", _REPO_ROOT / "scripts" / "crew"):
|
|
729
|
+
if str(_p) not in sys.path:
|
|
730
|
+
sys.path.insert(0, str(_p))
|
|
731
|
+
|
|
732
|
+
import _loom # noqa: E402
|
|
733
|
+
import phase_manager as pm # noqa: E402
|
|
734
|
+
import flow_compiler as fc # noqa: E402 — Task 5; import guarded below
|
|
735
|
+
|
|
736
|
+
# (archetype, hard-gate phase) pairs the in-process map declares.
|
|
737
|
+
_EXPECTED_HARD_GATES = {
|
|
738
|
+
"migrate": "cutover",
|
|
739
|
+
"incident": "mitigate",
|
|
740
|
+
"review": "remediate-or-accept",
|
|
741
|
+
"specify": "validate",
|
|
742
|
+
"decide": "record",
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
class FlowHardGateParityContract(unittest.TestCase):
|
|
747
|
+
"""Strangler safety net: loom's park-at-hard-gate decision (derived from
|
|
748
|
+
the compiled flow definition) must match phase_manager._HARD_GATE_PHASES
|
|
749
|
+
for every archetype. The two engines must agree on WHERE the human stops."""
|
|
750
|
+
|
|
751
|
+
def test_compiled_flow_hard_phase_matches_in_process_map(self):
|
|
752
|
+
for archetype, hard_phase in _EXPECTED_HARD_GATES.items():
|
|
753
|
+
flow_def = fc.compile_flow(archetype, flow_id=f"{archetype}-x")
|
|
754
|
+
hard_phases = [p["name"] for p in flow_def["phases"]
|
|
755
|
+
if isinstance(p.get("hitl"), str) and p["hitl"].startswith("hard:")]
|
|
756
|
+
self.assertIn(hard_phase, hard_phases,
|
|
757
|
+
f"{archetype}: compiled flow hard phase != in-process map")
|
|
758
|
+
|
|
759
|
+
def test_non_hard_archetypes_compile_no_hard_phase(self):
|
|
760
|
+
for archetype in ("triage", "explore", "build", "ship"):
|
|
761
|
+
flow_def = fc.compile_flow(archetype, flow_id=f"{archetype}-x")
|
|
762
|
+
hard_phases = [p["name"] for p in flow_def["phases"]
|
|
763
|
+
if isinstance(p.get("hitl"), str) and p["hitl"].startswith("hard:")]
|
|
764
|
+
self.assertEqual(hard_phases, [],
|
|
765
|
+
f"{archetype} should have no hard:* phase")
|
|
766
|
+
|
|
767
|
+
def test_approve_phase_in_process_authority_unchanged_when_loom_off(self):
|
|
768
|
+
# With cutover off, approve_phase behaves exactly as before: a hard gate
|
|
769
|
+
# without --confirmed-by raises ValueError (the in-process guard).
|
|
770
|
+
os.environ["WICKED_LOOM_CUTOVER"] = "off"
|
|
771
|
+
st = pm.ProjectState(name="m1", current_phase="cutover",
|
|
772
|
+
created_at=pm.get_utc_timestamp(),
|
|
773
|
+
phase_plan=["plan", "expand", "backfill", "cutover", "contract"],
|
|
774
|
+
extras={"v11_archetype": "migrate"})
|
|
775
|
+
with patch.object(pm, "save_project_state", lambda s: None):
|
|
776
|
+
with self.assertRaises(ValueError):
|
|
777
|
+
pm.approve_phase(st, "cutover")
|
|
778
|
+
|
|
779
|
+
def tearDown(self):
|
|
780
|
+
os.environ.pop("WICKED_LOOM_CUTOVER", None)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
if __name__ == "__main__":
|
|
784
|
+
unittest.main()
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
788
|
+
|
|
789
|
+
Run: `cd ~/Projects/wicked-garden && python3 -m pytest tests/crew/test_loom_flow_contract.py -v`
|
|
790
|
+
Expected: FAIL — `ModuleNotFoundError: No module named 'flow_compiler'` (the compiler is Task 5; this test pins the parity contract the flow cutover depends on, so it is written here and goes green after Task 5).
|
|
791
|
+
|
|
792
|
+
> Sequencing note: this contract test depends on `flow_compiler.compile_flow` (Task 5). It is written in Task 3 because it is the *flow cutover's* safety net, but it will not pass until Task 5 ships the compiler. Run Task 3 Step 3 (the mirror head) now; let Step 4 below confirm the in-process-authority subtest passes immediately, and the two parity subtests pass after Task 5. (Subagent-driven execution: keep this task open until Task 5 closes, then re-run.)
|
|
793
|
+
|
|
794
|
+
- [ ] **Step 3: Edit `phase_manager.py` — add the loom mirror head to `approve_phase`**
|
|
795
|
+
|
|
796
|
+
Add the module-level loom import near the existing `_bus_emit_safe` import block (after line 43, before `_bus_emit_safe`):
|
|
797
|
+
|
|
798
|
+
```python
|
|
799
|
+
# Strangler shim toward wicked-loom (cutover phase). Read-only parity mirror:
|
|
800
|
+
# the in-process hard-gate enforcement below STAYS authoritative; loom is
|
|
801
|
+
# consulted only to cross-check the park-at-hard-gate decision during cutover.
|
|
802
|
+
try:
|
|
803
|
+
import _loom # scripts/ is on sys.path
|
|
804
|
+
except ImportError: # pragma: no cover
|
|
805
|
+
_loom = None # type: ignore
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _loom_confirms_hard_gate(archetype: str, phase: str) -> "Optional[bool]":
|
|
809
|
+
"""Ask loom (via the compiled flow def) whether (archetype, phase) is a
|
|
810
|
+
hard gate. Returns True/False, or None when loom is unavailable/uncertain
|
|
811
|
+
(in which case the in-process _HARD_GATE_PHASES map remains authoritative).
|
|
812
|
+
Best-effort, fail-soft — never raises, never blocks the state write."""
|
|
813
|
+
if _loom is None or not _loom.use_loom():
|
|
814
|
+
return None
|
|
815
|
+
try:
|
|
816
|
+
from flow_compiler import compile_flow
|
|
817
|
+
flow_def = compile_flow(archetype, flow_id=f"{archetype}-parity")
|
|
818
|
+
for p in flow_def.get("phases", []):
|
|
819
|
+
if p.get("name") == phase:
|
|
820
|
+
hitl = p.get("hitl")
|
|
821
|
+
return isinstance(hitl, str) and hitl.startswith("hard:")
|
|
822
|
+
return False
|
|
823
|
+
except Exception:
|
|
824
|
+
return None # fail-soft: in-process map stays authoritative
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
Then insert the parity cross-check inside `approve_phase`, immediately after `phase = resolve_phase(phase)` (line 329) and before the hard-gate guard (line 332):
|
|
828
|
+
|
|
829
|
+
```python
|
|
830
|
+
# --- loom cutover mirror (flow surface) ----------------------------------
|
|
831
|
+
# Cross-check loom's park decision against the in-process map. A DISAGREEMENT
|
|
832
|
+
# is surfaced (stderr + bus emit) but does NOT change behavior — the
|
|
833
|
+
# in-process guard below stays authoritative during cutover (rollback-safe).
|
|
834
|
+
archetype_for_parity = (state.extras or {}).get("v11_archetype")
|
|
835
|
+
if archetype_for_parity:
|
|
836
|
+
loom_says = _loom_confirms_hard_gate(archetype_for_parity, phase)
|
|
837
|
+
in_proc_says = _is_hard_gate(state, phase)
|
|
838
|
+
if loom_says is not None and loom_says != in_proc_says:
|
|
839
|
+
print(f"[wicked-garden] loom/in-process hard-gate parity mismatch: "
|
|
840
|
+
f"archetype={archetype_for_parity} phase={phase} "
|
|
841
|
+
f"loom={loom_says} in_process={in_proc_says}", file=sys.stderr)
|
|
842
|
+
_bus_emit_safe(
|
|
843
|
+
"wicked.loom.parity_mismatch",
|
|
844
|
+
{"project_id": state.name, "archetype": archetype_for_parity,
|
|
845
|
+
"phase": phase, "loom": loom_says, "in_process": in_proc_says},
|
|
846
|
+
chain_id=f"{state.name}.{archetype_for_parity}.{phase}.parity",
|
|
847
|
+
)
|
|
848
|
+
# -------------------------------------------------------------------------
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
> Implementer note: the mirror is intentionally read-only this phase — it emits a `wicked.loom.parity_mismatch` event when the engines disagree (so production surfaces any drift before the contract phase moves storage onto loom) but never alters the write path. Replacing `phase_manager`'s storage with `loom flow run|status|resume` is the contract phase, not this one. The `Optional` type is already imported at line 35.
|
|
852
|
+
|
|
853
|
+
- [ ] **Step 4: Run test to verify it passes (in-process subtest now; parity subtests after Task 5)**
|
|
854
|
+
|
|
855
|
+
Run: `cd ~/Projects/wicked-garden && python3 -m pytest tests/crew/test_loom_flow_contract.py -v -k in_process`
|
|
856
|
+
Expected: PASS — `test_approve_phase_in_process_authority_unchanged_when_loom_off`. The two parity subtests stay red until Task 5 ships `flow_compiler`. Re-run the full file at the end of Task 5.
|
|
857
|
+
|
|
858
|
+
- [ ] **Step 5: Commit**
|
|
859
|
+
|
|
860
|
+
```bash
|
|
861
|
+
cd ~/Projects/wicked-garden
|
|
862
|
+
git add scripts/crew/phase_manager.py tests/crew/test_loom_flow_contract.py
|
|
863
|
+
git commit -m "feat(loom-cutover): approve_phase mirrors loom park-at-hard-gate decision (read-only parity)"
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
---
|
|
867
|
+
|
|
868
|
+
## Task 4: Add wicked-loom as the 5th required peer
|
|
869
|
+
|
|
870
|
+
**Files:**
|
|
871
|
+
- Modify: `~/Projects/wicked-garden/docs/required-peers.md`
|
|
872
|
+
- Modify: `~/Projects/wicked-garden/commands/setup.md`
|
|
873
|
+
- Modify: `~/Projects/wicked-garden/hooks/scripts/bootstrap.py` (add `_check_loom_dependency`, peer-detection portion only)
|
|
874
|
+
- Modify: `~/Projects/wicked-garden/.claude-plugin/plugin.json` (peer description)
|
|
875
|
+
|
|
876
|
+
Promote loom from "shimmed tool" to a declared peer — **required at install, resilient at runtime**, the same stance as the other four (§7, docs/required-peers.md). This is doc + setup + bootstrap-probe wiring; no new runtime logic (the runtime resolver is `_loom.resolve_loom` from Task 0).
|
|
877
|
+
|
|
878
|
+
- [ ] **Step 1: Edit `docs/required-peers.md`** — change "four peers" → "five peers" in the opening paragraph and the "## The four peers" heading (→ "## The five peers"), and add a row to the peer table:
|
|
879
|
+
|
|
880
|
+
```markdown
|
|
881
|
+
| **wicked-loom** | The orchestration runtime garden drives: peer resolution (`loom resolve`), synchronous fail-closed evidence gating (`loom gate`), and the archetype-agnostic flow runtime (`loom flow run/status/resume`). Garden compiles its archetype catalog into a flow definition and shells to loom to run it. | `npx wicked-loom` (npm; pinned `^0.2.0` in `plugin.json`) | `/wicked-garden:setup` verifies at install; resolved at runtime via `WICKED_LOOM_BIN` → config → `PATH` → `node_modules` → `npx wicked-loom`, with `WICKED_LOOM_BIN=""` as the kill-switch |
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
Add to the "## Why each is load-bearing" list:
|
|
885
|
+
|
|
886
|
+
```markdown
|
|
887
|
+
- **wicked-loom runs the work.** Garden classifies and steers; loom resolves
|
|
888
|
+
the peers, re-derives the gates, and advances the phases — parking at every
|
|
889
|
+
hard gate. Without it, garden has the archetype intelligence but no runtime
|
|
890
|
+
to execute it (during cutover, garden falls back to its in-process runtime;
|
|
891
|
+
after the contract phase, loom is the only runtime).
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
> Note: during the cutover phase the wording must stay honest — loom is required at install going forward, but garden still carries the in-process fallback (it is not deleted until the contract phase). The "resilient at runtime" stance already covers the transient-outage case; the bracket above states the cutover-vs-contract distinction explicitly.
|
|
895
|
+
|
|
896
|
+
- [ ] **Step 2: Edit `commands/setup.md`** — add a new verify step §2.8 after §2.7 (wicked-bus), in the exact shape of §2.5 (wicked-testing):
|
|
897
|
+
|
|
898
|
+
```markdown
|
|
899
|
+
### 2.8 Verify wicked-loom (Required)
|
|
900
|
+
|
|
901
|
+
```bash
|
|
902
|
+
npx wicked-loom doctor 2>/dev/null && npx wicked-loom resolve vault 2>/dev/null || echo "MISSING"
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
- `MISSING` → blocking. wicked-loom is the orchestration runtime garden drives — peer resolution, evidence gating, and flow execution. Show "wicked-loom is not installed. wicked-garden requires it as a peer (sibling to wicked-testing / wicked-vault / wicked-brain / wicked-bus)." **INTERACTIVE mode**: AskUserQuestion header "wicked-loom Required", options "Install now (Required)" = "Run: npm i -g wicked-loom (or use via npx)" / "Exit setup" = "Cancel — I'll install manually and re-run". **PLAIN_TEXT mode**: present numbered options and STOP. If install: run `npm i -g wicked-loom` (or confirm `npx wicked-loom doctor` resolves), then re-probe. On failure, show stderr and exit with manual instructions. If exit: "Install wicked-loom then restart with `/wicked-garden:setup`."
|
|
906
|
+
- A JSON line from `doctor` → check the version satisfies `^0.2.0` (the pin from `plugin.json`; 0.2.0 ships the gate + flow surfaces this garden shells to). Then verify the garden can resolve it: `sh "${CLAUDE_PLUGIN_ROOT}/scripts/_python.sh" -c "import sys; sys.path.insert(0,'scripts'); import _loom; print(_loom.resolve_loom())"` should print a non-null argv. Note: `WICKED_LOOM_CUTOVER=off` disables the loom path (the garden runs its in-process fallback) — used for rollback during the cutover transition.
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
- [ ] **Step 3: Edit `bootstrap.py`** — add `_check_loom_dependency` modeled on `_check_vault_dependency` (lines 640–685). Insert after `_check_vault_dependency` (after line 686):
|
|
910
|
+
|
|
911
|
+
```python
|
|
912
|
+
def _check_loom_dependency():
|
|
913
|
+
"""Return a briefing note if wicked-loom is not resolvable, else None.
|
|
914
|
+
|
|
915
|
+
wicked-loom is a required peer (the 5th, sibling to wicked-testing /
|
|
916
|
+
wicked-vault / wicked-brain / wicked-bus): garden shells to it for peer
|
|
917
|
+
resolution, evidence gating, and flow execution. During the cutover
|
|
918
|
+
transition garden falls back to its in-process runtime when loom is
|
|
919
|
+
absent, so this is a one-line install pointer, never a block.
|
|
920
|
+
|
|
921
|
+
Fast + stdlib-only (no subprocess to npx): checks PATH and loose skill
|
|
922
|
+
installs only. Always fails open — never blocks the session.
|
|
923
|
+
"""
|
|
924
|
+
try:
|
|
925
|
+
import shutil
|
|
926
|
+
|
|
927
|
+
# WICKED_LOOM_BIN explicitly set (even empty kill-switch) or the cutover
|
|
928
|
+
# flag turned off → operator is driving deliberately; don't nag.
|
|
929
|
+
if "WICKED_LOOM_BIN" in os.environ:
|
|
930
|
+
return None
|
|
931
|
+
if os.environ.get("WICKED_LOOM_CUTOVER", "").strip().lower() == "off":
|
|
932
|
+
return None
|
|
933
|
+
if shutil.which("wicked-loom"):
|
|
934
|
+
return None
|
|
935
|
+
|
|
936
|
+
skill_roots = [Path.home() / ".claude" / "skills"]
|
|
937
|
+
cfg = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
938
|
+
if cfg:
|
|
939
|
+
skill_roots.append(Path(cfg) / "skills")
|
|
940
|
+
for root in skill_roots:
|
|
941
|
+
try:
|
|
942
|
+
if root.exists():
|
|
943
|
+
for entry in root.iterdir():
|
|
944
|
+
if entry.is_dir() and entry.name.startswith("wicked-loom"):
|
|
945
|
+
return None
|
|
946
|
+
except OSError:
|
|
947
|
+
continue # fail open
|
|
948
|
+
|
|
949
|
+
return (
|
|
950
|
+
"[wicked-loom] REQUIRED but not installed.\n"
|
|
951
|
+
"Install now: npm i -g wicked-loom (or run via: npx wicked-loom)\n"
|
|
952
|
+
"wicked-loom is the orchestration runtime garden drives for peer "
|
|
953
|
+
"resolution, evidence gating, and flow execution. (Cutover phase: "
|
|
954
|
+
"garden falls back to its in-process runtime when loom is absent.)"
|
|
955
|
+
)
|
|
956
|
+
except Exception:
|
|
957
|
+
return None # Fail open — never block session start
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
Then wire it into wherever `_check_vault_dependency()`'s note is collected for the SessionStart briefing (find the call site — grep `_check_vault_dependency(` in bootstrap.py — and add a parallel `_check_loom_dependency()` call appending its note to the same briefing list).
|
|
961
|
+
|
|
962
|
+
- [ ] **Step 4: Edit `.claude-plugin/plugin.json`** — update the description's peer clause. Change "Requires four sibling peers verified at setup — wicked-testing, wicked-vault, wicked-brain, wicked-bus" to "Requires five sibling peers verified at setup — wicked-testing, wicked-vault, wicked-brain, wicked-bus, wicked-loom".
|
|
963
|
+
|
|
964
|
+
- [ ] **Step 5: Verify the docs + structural checks pass**
|
|
965
|
+
|
|
966
|
+
Run: `cd ~/Projects/wicked-garden && python3 -c "import json; json.load(open('.claude-plugin/plugin.json')); print('plugin.json valid')" && grep -c "wicked-loom" docs/required-peers.md commands/setup.md`
|
|
967
|
+
Expected: `plugin.json valid`; both files report ≥1 `wicked-loom` mention. (If the repo has a `/wg-check` structural validator, run it too: `python3 -m pytest tests/ -k "peer or required" -v` for any peer-count assertion that needs the 5th row.)
|
|
968
|
+
|
|
969
|
+
- [ ] **Step 6: Commit**
|
|
970
|
+
|
|
971
|
+
```bash
|
|
972
|
+
cd ~/Projects/wicked-garden
|
|
973
|
+
git add docs/required-peers.md commands/setup.md hooks/scripts/bootstrap.py .claude-plugin/plugin.json
|
|
974
|
+
git commit -m "feat(loom-cutover): wicked-loom is the 5th required peer (docs + setup + bootstrap probe)"
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
---
|
|
978
|
+
|
|
979
|
+
## Task 5: The archetype → flow-definition compiler (`scripts/crew/flow_compiler.py`)
|
|
980
|
+
|
|
981
|
+
**Files:**
|
|
982
|
+
- Create: `~/Projects/wicked-garden/scripts/crew/flow_compiler.py`
|
|
983
|
+
- Test: `~/Projects/wicked-garden/tests/crew/test_flow_compiler.py`
|
|
984
|
+
|
|
985
|
+
The §3.1 seam — the thin garden layer that makes loom archetype-agnostic. It reads `.claude-plugin/archetypes.json` (`phases`, `produces`, `hitl`) plus the hard-gate phase map and emits the flow-definition JSON `loom flow run` consumes. **Pure + deterministic**: catalog in, JSON out, no I/O beyond reading the catalog.
|
|
986
|
+
|
|
987
|
+
The translation rule (resolving the §3.1 ↔ catalog mismatch — documented under Ambiguities):
|
|
988
|
+
- The catalog's `hitl` is **archetype-level** (one discipline string per archetype, e.g. `hard:cutover-gate`). The flow definition needs **per-phase** `hitl`. Rule: every phase defaults to `hitl: "none"`; the archetype's hard/discrete gate is attached to its **gate phase** — the hard-gate phase from `_HARD_GATE_PHASES` (e.g. `migrate.cutover`), or for non-hard gating archetypes the catalog's last produces-bearing phase. The attached value is the catalog's `hitl` verbatim (so `hard:cutover-gate` lands on the `cutover` phase).
|
|
989
|
+
- A phase gets a `gate` iff it is the phase that produces a gated artifact. Rule: the **last phase** carries `gate: "produces:<first-produces-id>"` for gating archetypes (build/migrate/review/incident/ship/specify/decide); gateless archetypes (triage/explore) get `gate: null` on all phases. `peers_required` is `["vault", "testing"]` for archetypes whose produces include `test-report`, else `["vault"]`. `verifier_spec_ref` is `null` (wired later, spec §9 / #887 — out of scope here).
|
|
990
|
+
|
|
991
|
+
- [ ] **Step 1: Write the failing test**
|
|
992
|
+
|
|
993
|
+
```python
|
|
994
|
+
import sys
|
|
995
|
+
import unittest
|
|
996
|
+
from pathlib import Path
|
|
997
|
+
|
|
998
|
+
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
999
|
+
for _p in (_REPO_ROOT / "scripts", _REPO_ROOT / "scripts" / "crew"):
|
|
1000
|
+
if str(_p) not in sys.path:
|
|
1001
|
+
sys.path.insert(0, str(_p))
|
|
1002
|
+
|
|
1003
|
+
import flow_compiler as fc # noqa: E402
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
class CompileFlowTests(unittest.TestCase):
|
|
1007
|
+
def test_build_flow_shape(self):
|
|
1008
|
+
fd = fc.compile_flow("build", flow_id="build-1")
|
|
1009
|
+
self.assertEqual(fd["flow_id"], "build-1")
|
|
1010
|
+
self.assertEqual([p["name"] for p in fd["phases"]],
|
|
1011
|
+
["plan", "implement", "test", "review"])
|
|
1012
|
+
# build is discrete:review-gate -> no hard:* phase.
|
|
1013
|
+
self.assertFalse(any(p["hitl"].startswith("hard:") for p in fd["phases"]))
|
|
1014
|
+
|
|
1015
|
+
def test_migrate_cutover_is_the_hard_phase(self):
|
|
1016
|
+
fd = fc.compile_flow("migrate", flow_id="m-1")
|
|
1017
|
+
hard = [p for p in fd["phases"] if p["hitl"].startswith("hard:")]
|
|
1018
|
+
self.assertEqual(len(hard), 1)
|
|
1019
|
+
self.assertEqual(hard[0]["name"], "cutover")
|
|
1020
|
+
self.assertEqual(hard[0]["hitl"], "hard:cutover-gate")
|
|
1021
|
+
|
|
1022
|
+
def test_triage_is_fully_gateless(self):
|
|
1023
|
+
fd = fc.compile_flow("triage", flow_id="t-1")
|
|
1024
|
+
self.assertTrue(all(p["gate"] is None for p in fd["phases"]))
|
|
1025
|
+
self.assertTrue(all(p["hitl"] == "none" for p in fd["phases"]))
|
|
1026
|
+
|
|
1027
|
+
def test_gating_archetype_has_a_gate_on_last_phase(self):
|
|
1028
|
+
fd = fc.compile_flow("build", flow_id="b-1")
|
|
1029
|
+
self.assertIsNotNone(fd["phases"][-1]["gate"])
|
|
1030
|
+
self.assertTrue(fd["phases"][-1]["gate"].startswith("produces:"))
|
|
1031
|
+
|
|
1032
|
+
def test_peers_required_includes_testing_when_test_report_produced(self):
|
|
1033
|
+
self.assertIn("testing", fc.compile_flow("build", flow_id="b-1")["peers_required"])
|
|
1034
|
+
self.assertNotIn("testing", fc.compile_flow("decide", flow_id="d-1")["peers_required"])
|
|
1035
|
+
|
|
1036
|
+
def test_verifier_spec_ref_is_null(self):
|
|
1037
|
+
self.assertIsNone(fc.compile_flow("build", flow_id="b-1")["verifier_spec_ref"])
|
|
1038
|
+
|
|
1039
|
+
def test_unknown_archetype_raises(self):
|
|
1040
|
+
with self.assertRaises(ValueError):
|
|
1041
|
+
fc.compile_flow("frobnicate", flow_id="x-1")
|
|
1042
|
+
|
|
1043
|
+
def test_unsafe_flow_id_raises(self):
|
|
1044
|
+
with self.assertRaises(ValueError):
|
|
1045
|
+
fc.compile_flow("build", flow_id="../etc/passwd")
|
|
1046
|
+
|
|
1047
|
+
def test_output_is_json_serializable(self):
|
|
1048
|
+
import json
|
|
1049
|
+
json.dumps(fc.compile_flow("incident", flow_id="i-1")) # must not raise
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
if __name__ == "__main__":
|
|
1053
|
+
unittest.main()
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
1057
|
+
|
|
1058
|
+
Run: `cd ~/Projects/wicked-garden && python3 -m pytest tests/crew/test_flow_compiler.py -v`
|
|
1059
|
+
Expected: FAIL — `ModuleNotFoundError: No module named 'flow_compiler'`
|
|
1060
|
+
|
|
1061
|
+
- [ ] **Step 3: Write `scripts/crew/flow_compiler.py`**
|
|
1062
|
+
|
|
1063
|
+
```python
|
|
1064
|
+
#!/usr/bin/env python3
|
|
1065
|
+
"""flow_compiler.py — compile a garden archetype into a loom flow definition.
|
|
1066
|
+
|
|
1067
|
+
The §3.1 seam: garden owns the archetype catalog; loom is archetype-agnostic
|
|
1068
|
+
and runs a generic flow definition. This module is the thin handoff contract —
|
|
1069
|
+
it reads .claude-plugin/archetypes.json (phases, produces, hitl) plus the
|
|
1070
|
+
hard-gate phase map and emits the flow-definition JSON `loom flow run` consumes.
|
|
1071
|
+
|
|
1072
|
+
Pure + deterministic: catalog in, dict out. The only I/O is reading the catalog.
|
|
1073
|
+
|
|
1074
|
+
Flow-definition shape (consumed verbatim by wicked-loom `flow run`):
|
|
1075
|
+
{
|
|
1076
|
+
"flow_id": str,
|
|
1077
|
+
"phases": [{ "name", "gate": "produces:<id>"|null, "hitl": str, "produces": [...] }],
|
|
1078
|
+
"peers_required": [str],
|
|
1079
|
+
"verifier_spec_ref": str|null,
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
Translation rules (see the cutover plan's Ambiguities section):
|
|
1083
|
+
- The catalog's `hitl` is archetype-level; the flow def needs it per-phase.
|
|
1084
|
+
Every phase defaults to "none"; the archetype's gate discipline is attached
|
|
1085
|
+
to its gate phase — the hard-gate phase from _HARD_GATE_PHASES when present,
|
|
1086
|
+
else the last phase — verbatim (so "hard:cutover-gate" lands on "cutover").
|
|
1087
|
+
- The last phase of a GATING archetype carries gate="produces:<first-produces>";
|
|
1088
|
+
gateless archetypes (triage/explore) carry gate=null on every phase.
|
|
1089
|
+
- peers_required = ["vault","testing"] iff produces includes "test-report",
|
|
1090
|
+
else ["vault"]. verifier_spec_ref is null (wired later, spec §9 / #887).
|
|
1091
|
+
"""
|
|
1092
|
+
|
|
1093
|
+
from __future__ import annotations
|
|
1094
|
+
|
|
1095
|
+
import json
|
|
1096
|
+
import re
|
|
1097
|
+
import sys
|
|
1098
|
+
from pathlib import Path
|
|
1099
|
+
from typing import Any, Dict, List, Optional
|
|
1100
|
+
|
|
1101
|
+
# scripts/crew/ is on sys.path; import the authoritative hard-gate map so the
|
|
1102
|
+
# compiler and phase_manager agree by construction (the parity contract).
|
|
1103
|
+
from phase_manager import _HARD_GATE_PHASES # noqa: E402
|
|
1104
|
+
|
|
1105
|
+
_FLOW_ID_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
|
|
1106
|
+
# Archetypes whose `hitl` is "continuous"/"none" do not produce a re-derivable
|
|
1107
|
+
# gated artifact -> the flow is gateless (no per-phase produces gate).
|
|
1108
|
+
_GATELESS_HITL = ("none", "continuous")
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def _catalog_path() -> Path:
|
|
1112
|
+
"""Resolve .claude-plugin/archetypes.json relative to the repo root."""
|
|
1113
|
+
root = Path(os.environ["CLAUDE_PLUGIN_ROOT"]) if os.environ.get("CLAUDE_PLUGIN_ROOT") \
|
|
1114
|
+
else Path(__file__).resolve().parents[2]
|
|
1115
|
+
return root / ".claude-plugin" / "archetypes.json"
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _load_catalog() -> Dict[str, Any]:
|
|
1119
|
+
return json.loads(_catalog_path().read_text(encoding="utf-8")).get("archetypes", {})
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _hard_phase_for(archetype: str) -> Optional[str]:
|
|
1123
|
+
"""The single hard-gate phase for an archetype, or None."""
|
|
1124
|
+
pair = _HARD_GATE_PHASES.get(archetype, ())
|
|
1125
|
+
return pair[0] if pair else None
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def compile_flow(archetype: str, *, flow_id: str,
|
|
1129
|
+
catalog: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
1130
|
+
"""Compile one archetype into a loom flow definition (a JSON-able dict)."""
|
|
1131
|
+
if not isinstance(flow_id, str) or not _FLOW_ID_RE.match(flow_id):
|
|
1132
|
+
raise ValueError(f"unsafe flow_id: {flow_id!r} (kebab/snake/alnum, max 64)")
|
|
1133
|
+
|
|
1134
|
+
cat = catalog if catalog is not None else _load_catalog()
|
|
1135
|
+
arch = cat.get(archetype)
|
|
1136
|
+
if not arch:
|
|
1137
|
+
raise ValueError(f"unknown archetype: {archetype!r} (known: {sorted(cat)})")
|
|
1138
|
+
|
|
1139
|
+
phase_names: List[str] = list(arch.get("phases", []))
|
|
1140
|
+
produces: List[str] = list(arch.get("produces", []))
|
|
1141
|
+
hitl: str = arch.get("hitl", "none")
|
|
1142
|
+
gateless = hitl in _GATELESS_HITL or not phase_names
|
|
1143
|
+
|
|
1144
|
+
hard_phase = _hard_phase_for(archetype)
|
|
1145
|
+
# The gate phase: the hard-gate phase if declared, else the last phase.
|
|
1146
|
+
gate_phase = hard_phase if hard_phase in phase_names else (
|
|
1147
|
+
phase_names[-1] if phase_names else None)
|
|
1148
|
+
|
|
1149
|
+
first_produces = produces[0] if produces else None
|
|
1150
|
+
|
|
1151
|
+
phases: List[Dict[str, Any]] = []
|
|
1152
|
+
for name in phase_names:
|
|
1153
|
+
is_gate_phase = (name == gate_phase) and not gateless
|
|
1154
|
+
phases.append({
|
|
1155
|
+
"name": name,
|
|
1156
|
+
"gate": f"produces:{first_produces}" if (is_gate_phase and first_produces) else None,
|
|
1157
|
+
"hitl": hitl if (name == gate_phase and not gateless) else "none",
|
|
1158
|
+
"produces": list(produces) if is_gate_phase else [],
|
|
1159
|
+
})
|
|
1160
|
+
|
|
1161
|
+
peers = ["vault", "testing"] if "test-report" in produces else ["vault"]
|
|
1162
|
+
return {
|
|
1163
|
+
"flow_id": flow_id,
|
|
1164
|
+
"phases": phases,
|
|
1165
|
+
"peers_required": peers,
|
|
1166
|
+
"verifier_spec_ref": None,
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
__all__ = ["compile_flow"]
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
if __name__ == "__main__":
|
|
1174
|
+
import argparse
|
|
1175
|
+
parser = argparse.ArgumentParser(description="Compile an archetype to a loom flow definition.")
|
|
1176
|
+
parser.add_argument("archetype")
|
|
1177
|
+
parser.add_argument("--flow-id", required=True)
|
|
1178
|
+
a = parser.parse_args()
|
|
1179
|
+
print(json.dumps(compile_flow(a.archetype, flow_id=a.flow_id), indent=2))
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
> Implementer note: add `import os` to the imports (used by `_catalog_path`). The `from phase_manager import _HARD_GATE_PHASES` import is the deliberate single source of truth — the compiler and the phase manager must agree on the hard-gate phase set by construction, which is exactly what Task 3's parity contract test asserts.
|
|
1183
|
+
|
|
1184
|
+
- [ ] **Step 4: Run test to verify it passes (compiler + the deferred Task 3 parity subtests)**
|
|
1185
|
+
|
|
1186
|
+
Run: `cd ~/Projects/wicked-garden && python3 -m pytest tests/crew/test_flow_compiler.py tests/crew/test_loom_flow_contract.py -v`
|
|
1187
|
+
Expected: PASS — 9 compiler tests AND now all of `test_loom_flow_contract.py` (the two parity subtests deferred from Task 3 go green because `flow_compiler` exists).
|
|
1188
|
+
|
|
1189
|
+
- [ ] **Step 5: Smoke the compiler CLI**
|
|
1190
|
+
|
|
1191
|
+
Run: `cd ~/Projects/wicked-garden && python3 scripts/crew/flow_compiler.py migrate --flow-id migrate-smoke`
|
|
1192
|
+
Expected: a JSON flow definition with 5 phases; the `cutover` phase has `"hitl": "hard:cutover-gate"` and the last phase carries a `produces:shape-change` gate.
|
|
1193
|
+
|
|
1194
|
+
- [ ] **Step 6: Commit**
|
|
1195
|
+
|
|
1196
|
+
```bash
|
|
1197
|
+
cd ~/Projects/wicked-garden
|
|
1198
|
+
git add scripts/crew/flow_compiler.py tests/crew/test_flow_compiler.py
|
|
1199
|
+
git commit -m "feat(loom-cutover): archetype->flow-definition compiler (the §3.1 seam)"
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
---
|
|
1203
|
+
|
|
1204
|
+
## Task 6: Full-suite green + end-to-end smoke + handoff note
|
|
1205
|
+
|
|
1206
|
+
**Files:** none (verification only).
|
|
1207
|
+
|
|
1208
|
+
- [ ] **Step 1: Run the full garden suite**
|
|
1209
|
+
|
|
1210
|
+
Run: `cd ~/Projects/wicked-garden && python3 -m pytest tests/ -q`
|
|
1211
|
+
Expected: PASS — no regressions. The cutover defaults to `auto`, and in CI loom is unresolvable, so every existing test exercises the unchanged in-process path. New tests: 14 (resolver) + 3 (resolve contract) + 4 (gate contract) + 3 (flow contract) + 9 (compiler) = **33 new tests**, all green.
|
|
1212
|
+
|
|
1213
|
+
- [ ] **Step 2: End-to-end smoke with loom forced on (requires a published `npx wicked-loom` ≥0.2.0)**
|
|
1214
|
+
|
|
1215
|
+
Run:
|
|
1216
|
+
```bash
|
|
1217
|
+
cd ~/Projects/wicked-garden
|
|
1218
|
+
WICKED_LOOM_CUTOVER=on sh scripts/_python.sh -c "import sys; sys.path.insert(0,'scripts'); sys.path.insert(0,'scripts/qe'); import vault_gate as vg; print(vg.resolve_vault())"
|
|
1219
|
+
```
|
|
1220
|
+
Expected: a non-null argv printed by the **loom** path (proving the resolve cutover reaches the real CLI). If `npx wicked-loom` is not yet published, this smoke is SKIPPED — note it in the handoff and rely on the injected-runner contract tests (Step 1), which prove parity without the network.
|
|
1221
|
+
|
|
1222
|
+
- [ ] **Step 3: Confirm rollback works**
|
|
1223
|
+
|
|
1224
|
+
Run: `cd ~/Projects/wicked-garden && WICKED_LOOM_CUTOVER=off python3 -m pytest tests/qe/test_vault_gate.py tests/crew/test_loom_flow_contract.py -q`
|
|
1225
|
+
Expected: PASS — with the flag off, every surface runs the in-process path; the cutover is fully reversible by a single env var (no code revert needed).
|
|
1226
|
+
|
|
1227
|
+
- [ ] **Step 4: Report (no autonomous push)**
|
|
1228
|
+
|
|
1229
|
+
Report to the operator: the three surfaces (`resolve`/`gate`/`flow`) are cut over behind contract tests + the `WICKED_LOOM_CUTOVER` flag; loom is the declared 5th peer; the flow compiler exists. The in-process code is intact behind the shim (rollback = `WICKED_LOOM_CUTOVER=off`). The CONTRACT phase (deleting the in-process runtime) is the next plan — run it only after this cutover is proven in production. Do NOT run `npm publish`, `/wg-release`, or delete any in-process code in this plan.
|
|
1230
|
+
|
|
1231
|
+
---
|
|
1232
|
+
|
|
1233
|
+
## Self-Review
|
|
1234
|
+
|
|
1235
|
+
**1. Spec coverage** — checked against §6 (extraction inventory: which files cut over) and §7 step 2 (the cutover):
|
|
1236
|
+
|
|
1237
|
+
| Spec item (§6 / §7) | Where realized | Status |
|
|
1238
|
+
|---|---|---|
|
|
1239
|
+
| §7 cutover order `resolve` → `gate` → `flow`, one surface at a time | Tasks 1 → 2 → 3, in that order | ✓ |
|
|
1240
|
+
| §6 `_integration_resolver.py` / `_capability_resolver.py` / `_capability_registry.py` → loom (compose/resolution) | The **resolution** they back is cut over via Task 1 (`resolve_vault` → `loom resolve`) + the `_loom.resolve_loom` ladder (Task 0). These files are NOT deleted (contract phase); the runtime *peer resolution path* is what cuts over. | ✓ (cutover, not deletion) |
|
|
1241
|
+
| §6 `scripts/qe/vault_gate.py` → loom (conduct/gate) | Task 2 — `cross_check` shells to `loom gate`; `gate_satisfied` inherits it unchanged | ✓ |
|
|
1242
|
+
| §6 `scripts/crew/phase_manager.py` → loom (conduct/state) | Task 3 — `approve_phase` mirrors `loom flow` park-at-hard-gate (read-only parity; storage move is the contract phase) | ✓ (parity mirror, not storage replacement — deliberate, stated) |
|
|
1243
|
+
| §6 `bootstrap.py` PARTIAL — peer detect portion → loom compose | Task 4 — `_check_loom_dependency` probe added; the existing detect functions STAY (PARTIAL boundary confirmed empirically in contract phase per §6) | ✓ |
|
|
1244
|
+
| §7 add wicked-loom as the 5th required peer (docs/required-peers.md + setup) | Task 4 — required-peers.md row + setup.md §2.8 + plugin.json + bootstrap probe | ✓ |
|
|
1245
|
+
| §7 introduce the archetype → flow-definition compiler (§3.1 seam) | Task 5 — `flow_compiler.compile_flow` reads the catalog + hard-gate map, emits the §3.1 JSON | ✓ |
|
|
1246
|
+
| §7 / §10 each cutover fail-soft during transition (loom unresolvable → fall back / degrade, never break a session) | Every cutover head guards on `_loom.use_loom()` and falls through to the unchanged in-process body; default flag `auto` | ✓ |
|
|
1247
|
+
| §10 / I2 gates still fail-closed | Task 2 — loom `gate: "unavailable"` maps to `available: False`; loom error in `auto` falls to in-process gate which itself fails closed; `test_gate_fails_closed_when_loom_errors_and_vault_absent` | ✓ |
|
|
1248
|
+
| **House requirement: contract test per cutover (strangler safety net, identical results)** | Task 1 `test_loom_resolve_matches_in_process_for_npx_case`; Task 2 `test_loom_pass_matches_in_process_pass` / `_reject_matches_`; Task 3 `test_compiled_flow_hard_phase_matches_in_process_map` | ✓ |
|
|
1249
|
+
| §11 success #3 no regression: gate behavior identical pre/post | Contract tests + in-process path untouched (flag default leaves it active in CI) | ✓ |
|
|
1250
|
+
| §11 success #6 loom is archetype-agnostic | Task 5 emits a generic flow def; loom branches only on `gate`/`hitl` (Plan B I6) — garden never hands loom an archetype name | ✓ |
|
|
1251
|
+
|
|
1252
|
+
**Explicitly out of scope (stated up front, intentional gaps):** the CONTRACT phase (deleting the `→ loom` rows in §6 — a later plan); the headless daemon (D3, §9). These are NOT covered here by design — the cutover leaves the in-process code in place behind the shim for trivial rollback. No unintended gap.
|
|
1253
|
+
|
|
1254
|
+
**2. Placeholder scan** — No `TBD`/`TODO`/"add error handling"/"similar to Task N". Every code step shows complete, runnable code; every run step shows the exact `python3 -m pytest …` command + expected pass/fail. The four implementer notes (Task 1 `allow_npx` boundary, Task 2 `verifier_spec` deferral, Task 3 read-only-parity rationale, Task 5 `import os` + single-source-of-truth import) are concrete clarifications, not deferred work. The Task 3↔5 sequencing dependency is called out explicitly (the parity subtests go green after Task 5). ✓
|
|
1255
|
+
|
|
1256
|
+
**3. Type consistency** —
|
|
1257
|
+
- `_loom.resolve_loom() -> list[str] | None` is consumed identically by `vault_gate.resolve_vault`'s loom head (returns the `command` array) and `_loom.use_loom`. ✓
|
|
1258
|
+
- `_loom.run_json(args, *, timeout, project_dir, _run) -> {exit_code, json, stdout, stderr, error}` — every caller (resolve head, gate head) reads `out["error"]` then `out["json"]`. ✓
|
|
1259
|
+
- `cross_check`'s return shape `{available, overall, exit_code, claims, mode, contract_version, detail, raw}` is produced identically by the in-process body (lines 204–213) and the loom head's mapping; `gate_satisfied` reads `available` + `overall` from both. ✓
|
|
1260
|
+
- `compile_flow(archetype, *, flow_id, catalog=None) -> dict` with phase dicts `{name, gate, hitl, produces}` — consumed by Task 3's parity test (`p["name"]`, `p["hitl"]`) and matches Plan B's flow-def field semantics (`gate`: `null`|`"produces:<id>"`; `hitl`: `"hard:*"`|else) exactly. ✓
|
|
1261
|
+
- `_HARD_GATE_PHASES` is imported by `flow_compiler` from `phase_manager` (single source of truth) and asserted-against in Task 3's contract test — the compiler and the phase manager agree by construction. ✓
|
|
1262
|
+
- `cutover_mode() -> str ∈ {auto,on,off}` and `use_loom() -> bool` consistent across `_loom`, the two gate/resolve heads, and the phase-manager mirror. ✓
|
|
1263
|
+
|
|
1264
|
+
**One self-review fix applied inline:** Task 3's contract test depends on Task 5's `flow_compiler`; rather than reorder (which would put the highest-risk surface's safety net after its shim), the plan keeps the spec's `resolve→gate→flow` order and explicitly flags that two of Task 3's subtests go green only after Task 5, with the in-process-authority subtest passing immediately. This keeps the cutover ordering faithful to §7 while being honest about the cross-task test dependency.
|
|
1265
|
+
|
|
1266
|
+
---
|
|
1267
|
+
|
|
1268
|
+
## Ambiguities resolved (and how)
|
|
1269
|
+
|
|
1270
|
+
1. **How does garden invoke the loom CLI cross-platform?** Garden already uses `sh "${CLAUDE_PLUGIN_ROOT}/scripts/_python.sh"` for Python and `npx <peer>` for peers. Resolution: `scripts/_loom.py` mirrors `vault_gate.resolve_vault`'s ladder verbatim — `WICKED_LOOM_BIN` (empty = kill-switch) → `tool_preferences.wicked-loom` in `config.json` → `wicked-loom` on `PATH` → `node_modules/.bin/wicked-loom` → `npx --yes wicked-loom`. A `.mjs`/`.js` override is invoked via `node` (same `_argv_for` rule). Argv lists, `shutil.which`, no shell — cross-platform by construction.
|
|
1271
|
+
|
|
1272
|
+
2. **The loom CLI surface targeted.** The shipped loom repo at `~/Projects/wicked-loom` is at **0.1.0 (compose only)** — it ships `resolve`/`doctor`/`compose`. The `gate` and `flow` surfaces are Plan B (conduct), landing as **0.2.0**. Resolution: Task 1 (`resolve`) works against 0.1.0; Tasks 2–3 (`gate`/`flow`) require 0.2.0. The plan pins `^0.2.0` and the end-to-end smoke (Task 6 Step 2) is SKIPPED-with-note if 0.2.0 is not yet published — the injected-runner contract tests prove parity without it. Execution Handoff states conduct must be published first.
|
|
1273
|
+
|
|
1274
|
+
3. **§3.1 flow-def `hitl` is per-phase, but the catalog's `hitl` is archetype-level.** The catalog gives one `hitl` string per archetype (e.g. `migrate` → `hard:cutover-gate`) and names hard-gate phases only in `phase_manager._HARD_GATE_PHASES`. Resolution (the compiler's translation rule): every phase defaults to `hitl: "none"`; the archetype's discipline is attached to its gate phase — the hard-gate phase from `_HARD_GATE_PHASES` when present, else the last phase — verbatim. The compiler imports `_HARD_GATE_PHASES` from `phase_manager` so the two engines agree by construction (asserted by Task 3's parity contract).
|
|
1275
|
+
|
|
1276
|
+
4. **Which phase carries the `gate`?** The catalog lists `produces` at the archetype level, not per phase. Resolution: the gate phase (above) carries `gate: "produces:<first-produces-id>"`; gateless archetypes (`hitl` ∈ {none, continuous}: triage, explore) get `gate: null` everywhere. This matches Plan B's flow-def semantics (`null` → advance freely; `produces:<id>` → re-derive that contract).
|
|
1277
|
+
|
|
1278
|
+
5. **Does the `flow` cutover replace `phase_manager`'s storage?** No — that would be a high-risk storage migration, and §7 contracts (deletes) are the *next* phase. Resolution: the `flow` cutover is a **read-only parity mirror** — `approve_phase` cross-checks loom's park-at-hard-gate decision against `_HARD_GATE_PHASES` and emits `wicked.loom.parity_mismatch` on disagreement, but the in-process guard stays authoritative. This surfaces drift in production before the contract phase moves storage onto `loom flow`.
|
|
1279
|
+
|
|
1280
|
+
6. **How is the gate fail-closed posture preserved across the shim?** Both paths fail closed. The loom `gate` returns `gate: "unavailable"` when the vault is unresolvable behind it → mapped to `available: False` → `gate_satisfied` returns the unavailable verdict. If loom *itself* is unreachable in `auto` mode, the head falls through to the in-process `cross_check`, which fails closed on a missing vault exactly as today. A loom error never invents a PASS (I2), proven by `test_gate_fails_closed_when_loom_errors_and_vault_absent`.
|
|
1281
|
+
|
|
1282
|
+
7. **`vault_available()` must not be corrupted by loom's npx fallback.** `vault_available()` calls `resolve_vault(allow_npx=False)` to detect a *concrete* install. Loom's `resolve` would report the npx fallback as resolvable. Resolution: the loom head in `resolve_vault` is guarded by `allow_npx` — the concrete-install probe stays in-process, preserving the exact "installed" signal setup and bootstrap rely on.
|
|
1283
|
+
|
|
1284
|
+
---
|
|
1285
|
+
|
|
1286
|
+
## Execution Handoff
|
|
1287
|
+
|
|
1288
|
+
Plan complete and saved to `docs/plans/2026-06-08-wicked-loom-cutover.md`. Two execution options:
|
|
1289
|
+
|
|
1290
|
+
1. **Subagent-Driven (recommended)** — fresh subagent per task, two-stage review between tasks. REQUIRED SUB-SKILL: `superpowers:subagent-driven-development`. Note the Task 3 ↔ Task 5 dependency: Task 3's two parity subtests go green only after Task 5 ships the compiler — keep Task 3 open and re-run its full file at the end of Task 5.
|
|
1291
|
+
2. **Inline Execution** — execute in this session with checkpoints. REQUIRED SUB-SKILL: `superpowers:executing-plans`.
|
|
1292
|
+
|
|
1293
|
+
Notes for the executor:
|
|
1294
|
+
- Execution happens in **wicked-garden** (`~/Projects/wicked-garden`), editing existing files. The in-process runtime code is **left in place behind the shim** — do NOT delete it; that is the contract phase.
|
|
1295
|
+
- **Prerequisite for Tasks 2–3:** `wicked-loom@0.2.0` (the conduct surface — `gate`, `flow`) must be published and reachable via `npx wicked-loom`. Plan B (`docs/plans/2026-06-08-wicked-loom-conduct.md`) is its deliverable. Task 1 (`resolve`) works against the already-shipped 0.1.0. The contract tests are hermetic (loom subprocess injected) and prove parity without a published loom; only the Task 6 end-to-end smoke needs the real CLI.
|
|
1296
|
+
- Garden imports nothing from loom's Python — it shells `npx wicked-loom` via `scripts/_loom.py`. Loom stays a sibling primitive.
|
|
1297
|
+
- Rollback during transition is a single env var: `WICKED_LOOM_CUTOVER=off` runs every surface in-process. No code revert needed.
|
|
1298
|
+
- The CONTRACT phase (deleting the `→ loom` rows in spec §6, re-measuring footprint, absorbing #843) is the **next** plan — run it only after this cutover is proven in production.
|
|
1299
|
+
|
|
1300
|
+
**Which approach?**
|