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,1337 @@
|
|
|
1
|
+
# wicked-loom — Conduct Module, Plan B (gate + flow runtime) — 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:** Add the `wicked-loom` **conduct** surface — a synchronous, fail-closed vault re-derivation gate and an archetype-agnostic flow runtime that advances declarative flow definitions, executes per-phase gates, parks at hard gates, and persists deterministic per-flow state — built on Plan A's `resolve()`/`compose` core, with garden untouched.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Four new stdlib-only Python modules added to the existing `python/loom/` package: `gate.py` (sync vault `cross-check`, ported from garden's `scripts/qe/vault_gate.py`), `flowstate.py` (local-JSON per-flow state, phase_manager-style), `busemit.py` (thin best-effort fire-and-forget bus emit — emission only; no consumer), and `flow.py` (the generic phase-loop that wires gate + state + emit together). Subprocess execution is injected everywhere (matching `compose.py`'s `RunResult`/`Runner` pattern) so no test ever spawns a real vault or touches the network. `cli.py` gains a `gate` command and a `flow` command group. **Conduct only** — compose was Plan A.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python 3.9+ (stdlib only — no third-party deps, mirroring Plan A and garden's hook discipline), `pytest` for tests, the existing `bin/loom.mjs` Node shim (unchanged). Reuses `loom.resolve.resolve("vault")` for vault resolution and `loom.compose.RunResult`/`Runner` for the injected subprocess seam.
|
|
10
|
+
|
|
11
|
+
**Decisions locked for this plan (override in review):**
|
|
12
|
+
- **Scope = conduct, synchronous core only.** `loom gate`, `loom flow run|status|resume`. The headless daemon / bus-consumer / projector is **DEFERRED per spec decision D3 and §9 (D-headless)** — explicitly OUT OF SCOPE here (see "Out of scope" below). Only the *contract* (park-at-hard-gate, event names) is realized, not unattended execution.
|
|
13
|
+
- **File layout:** `gate.py`, `flow.py`, `flowstate.py`, `busemit.py` — one responsibility per file (justified in File Structure). Vault subprocess and bus subprocess are both injected `Runner`s so flow/gate tests are hermetic.
|
|
14
|
+
- **Target repo:** `~/Projects/wicked-loom` (the package Plan A created). Plan B adds files to it; garden (`~/Projects/wicked-garden`) is untouched.
|
|
15
|
+
|
|
16
|
+
**Source material to read in wicked-garden before Task 1 (grounding only — do not import from garden):**
|
|
17
|
+
- `scripts/qe/vault_gate.py` — the exact `cross-check` invocation (`["cross-check", "--scope", scope, "--phase", phase, ("--with-attestations")]`), how it parses the `overall` PASS/REJECT/ERROR field, and the fail-closed posture when the vault is unresolvable or unrunnable. `gate.py` is a faithful, injected-runner port of this.
|
|
18
|
+
- `scripts/crew/phase_manager.py` — the `ProjectState` dataclass + `save_project_state`/`load_project_state` JSON pattern and the `_bus_emit_safe` fire-and-forget wrapper. `flowstate.py` and `busemit.py` mirror these, slimmed.
|
|
19
|
+
- Plan A (`docs/plans/2026-06-08-wicked-loom-expand-compose.md`) — the code style, the `RunResult(returncode, stdout, stderr)` injected-runner pattern, and the `cli.py` dispatch shape this plan extends.
|
|
20
|
+
|
|
21
|
+
**Out of scope (DEFERRED — do NOT build in Plan B):**
|
|
22
|
+
- The bus **consumer / projector** and headless unattended execution (spec §4.2 "Project", §4.3 surface #2). `busemit.py` is emission ONLY — best-effort, fail-soft, never blocking; an emitted event NEVER satisfies a gate (invariant I4). No subscriber, no event-sourced state rebuild.
|
|
23
|
+
- `loom compose` changes (Plan A is frozen).
|
|
24
|
+
- The garden-side archetype→flow-definition compiler (§3.1, §7 step 2) and the garden `gate`/`flow` cutover shims (§7) — those are the cutover plan, not Plan B. Loom only *consumes* a hand-authored flow definition (success criterion #6).
|
|
25
|
+
|
|
26
|
+
**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 gate must be synchronous + fully mockable (never hit the network or a real vault in a unit test); flow state must be deterministic (no wall-clock-dependent assertions — timestamps are written but never asserted-on for equality).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## File Structure
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
wicked-loom/
|
|
34
|
+
├── python/loom/
|
|
35
|
+
│ ├── gate.py # NEW — synchronous vault cross-check (re-derive). Ported
|
|
36
|
+
│ │ # from garden scripts/qe/vault_gate.py, runner injected.
|
|
37
|
+
│ ├── flowstate.py # NEW — per-flow JSON state: load/save/new, one file per flow_id.
|
|
38
|
+
│ ├── busemit.py # NEW — thin best-effort fire-and-forget bus emit (emission only).
|
|
39
|
+
│ ├── flow.py # NEW — the generic phase-loop: run/status/resume; wires
|
|
40
|
+
│ │ # gate + flowstate + busemit. Archetype-agnostic.
|
|
41
|
+
│ ├── resolve.py # (Plan A) reused by gate.py for vault resolution.
|
|
42
|
+
│ ├── compose.py # (Plan A) RunResult/Runner reused by gate.py + busemit.py.
|
|
43
|
+
│ ├── manifest.py # (Plan A) unchanged.
|
|
44
|
+
│ └── cli.py # MODIFY — add `gate` + `flow` (run|status|resume) dispatch.
|
|
45
|
+
├── tests/
|
|
46
|
+
│ ├── test_gate.py # NEW
|
|
47
|
+
│ ├── test_flowstate.py # NEW
|
|
48
|
+
│ ├── test_busemit.py # NEW
|
|
49
|
+
│ ├── test_flow.py # NEW
|
|
50
|
+
│ └── test_cli.py # MODIFY — add gate + flow CLI cases.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Responsibilities (one per file, justifying the layout):**
|
|
54
|
+
- `gate.py` = *re-derive evidence for one produces* (the honest-evidence engine). Pure of state/flow concerns; takes an injected runner; returns a status dict. This is the §4.2 "Execute a gate" bullet and invariants I1/I2/I3.
|
|
55
|
+
- `flowstate.py` = *persist one flow's progress to disk* (data + I/O only — no gate, no bus). The §4.2 "Run a flow → persist phase state" storage half. Deterministic, no network (spec §10).
|
|
56
|
+
- `busemit.py` = *announce a transition, best-effort* (emission only — the §4.3 #3 event contract producer side; the consumer is deferred per D3/I4). Isolated so flow logic never couples to bus availability.
|
|
57
|
+
- `flow.py` = *advance phases, calling gate + state + emit* (the §4.2 "Run a flow" + "Park at hard gate" orchestration; invariants I5/I6). It is the only file that knows the flow-definition shape; it is archetype-agnostic (I6 — it must not branch on archetype names).
|
|
58
|
+
- `cli.py` = *argument dispatch only* (unchanged philosophy from Plan A — no business logic).
|
|
59
|
+
|
|
60
|
+
Why `busemit.py` is separate from `flowstate.py`: state is the **source of truth** (deterministic, must persist); the bus is **optional infrastructure** (fail-open). Mixing them would let a bus failure threaten a state write (it must not — I4 / spec §10). Why `gate.py` is separate from `flow.py`: the gate is independently shippable as `loom gate` (a garden cutover target on its own, §7) and must be unit-testable without any flow.
|
|
61
|
+
|
|
62
|
+
**The flow-definition shape this plan consumes** (spec §3.1 — loom executes it, garden authors it):
|
|
63
|
+
|
|
64
|
+
```jsonc
|
|
65
|
+
{
|
|
66
|
+
"flow_id": "build-1234",
|
|
67
|
+
"phases": [
|
|
68
|
+
{ "name": "plan", "gate": null, "hitl": "none", "produces": [] },
|
|
69
|
+
{ "name": "implement", "gate": null, "hitl": "none", "produces": [] },
|
|
70
|
+
{ "name": "test", "gate": "produces:test-report", "hitl": "discrete:review", "produces": ["test-report"] },
|
|
71
|
+
{ "name": "review", "gate": "produces:verdict", "hitl": "hard:final-verdict","produces": ["verdict"] }
|
|
72
|
+
],
|
|
73
|
+
"peers_required": ["vault", "testing"],
|
|
74
|
+
"verifier_spec_ref": null
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Field semantics loom relies on (and nothing else — I6):
|
|
79
|
+
- `gate`: `null` → no gate, advance freely. `"produces:<name>"` → run a vault cross-check whose `--phase` is `<name>` (the produces contract id). Any non-null, non-`produces:` value is treated as a generic gate on the literal string (still re-derived; fail-soft only on an unknown verifier spec, never a vacuous pass).
|
|
80
|
+
- `hitl`: `"hard:*"` → a **hard gate**: even on a PASS, the flow PARKS for human verdict (I5); loom never self-approves a hard gate. `"discrete:*"`/`"continuous"`/`"none"` → not hard; a satisfied gate advances automatically.
|
|
81
|
+
- `verifier_spec_ref`: optional path passed through to the gate as `--verifier-spec`; absent/unreadable → generic detection, never blocks (I3).
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Task 1: Synchronous vault gate (`gate.py`)
|
|
86
|
+
|
|
87
|
+
**Files:**
|
|
88
|
+
- Create: `~/Projects/wicked-loom/python/loom/gate.py`
|
|
89
|
+
- Test: `~/Projects/wicked-loom/tests/test_gate.py`
|
|
90
|
+
|
|
91
|
+
Faithful injected-runner port of garden's `scripts/qe/vault_gate.cross_check` + the fail-closed posture of `gate_satisfied`. The vault command prefix comes from `loom.resolve.resolve("vault")`; the subprocess is run through an injected `Runner` (the same `RunResult` shape as `compose.py`) so tests never spawn the vault. Invariants: **I1** synchronous/in-line (one blocking call per invocation, no events), **I2** fail-closed (vault unresolvable OR unrunnable → `gate: "unavailable"`, `satisfied: False` — never a pass), **I3** fail-soft on the verifier spec only (a `verifier_spec` arg is passed through when present; its absence never blocks). Hard gates require `with_attestations=True` (the independent attester), forwarded to the vault as `--with-attestations`.
|
|
92
|
+
|
|
93
|
+
- [ ] **Step 1: Write the failing test**
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from unittest.mock import patch
|
|
97
|
+
|
|
98
|
+
from loom import gate
|
|
99
|
+
from loom.compose import RunResult
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _runner(stdout="", code=0):
|
|
103
|
+
def run(cmd, timeout=None):
|
|
104
|
+
return RunResult(returncode=code, stdout=stdout, stderr="")
|
|
105
|
+
return run
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_gate_passes_when_vault_reports_pass():
|
|
109
|
+
with patch.object(gate, "resolve", return_value=["wicked-vault"]):
|
|
110
|
+
r = gate.run_gate("test-report", scope="build-1",
|
|
111
|
+
run=_runner(stdout='{"overall":"PASS"}'))
|
|
112
|
+
assert r["satisfied"] is True
|
|
113
|
+
assert r["gate"] == "vault-cross-check"
|
|
114
|
+
assert r["re_derived"] is True
|
|
115
|
+
assert r["overall"] == "PASS"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_gate_rejects_when_vault_reports_reject():
|
|
119
|
+
with patch.object(gate, "resolve", return_value=["wicked-vault"]):
|
|
120
|
+
r = gate.run_gate("test-report", scope="build-1",
|
|
121
|
+
run=_runner(stdout='{"overall":"REJECT"}'))
|
|
122
|
+
assert r["satisfied"] is False
|
|
123
|
+
assert r["overall"] == "REJECT"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_gate_fails_closed_when_vault_unresolvable():
|
|
127
|
+
with patch.object(gate, "resolve", return_value=None):
|
|
128
|
+
r = gate.run_gate("test-report", scope="build-1", run=_runner())
|
|
129
|
+
assert r["satisfied"] is False
|
|
130
|
+
assert r["gate"] == "unavailable"
|
|
131
|
+
assert r["re_derived"] is False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_gate_fails_closed_when_vault_unrunnable():
|
|
135
|
+
def boom(cmd, timeout=None):
|
|
136
|
+
raise FileNotFoundError("vault gone")
|
|
137
|
+
with patch.object(gate, "resolve", return_value=["wicked-vault"]):
|
|
138
|
+
r = gate.run_gate("test-report", scope="build-1", run=boom)
|
|
139
|
+
assert r["satisfied"] is False
|
|
140
|
+
assert r["gate"] == "unavailable"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_gate_fails_closed_on_non_json_output():
|
|
144
|
+
with patch.object(gate, "resolve", return_value=["wicked-vault"]):
|
|
145
|
+
r = gate.run_gate("test-report", scope="build-1",
|
|
146
|
+
run=_runner(stdout="not json"))
|
|
147
|
+
assert r["satisfied"] is False
|
|
148
|
+
assert r["overall"] == "ERROR"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_with_attestations_forwarded_to_vault():
|
|
152
|
+
seen = {}
|
|
153
|
+
|
|
154
|
+
def run(cmd, timeout=None):
|
|
155
|
+
seen["cmd"] = cmd
|
|
156
|
+
return RunResult(returncode=0, stdout='{"overall":"PASS"}', stderr="")
|
|
157
|
+
|
|
158
|
+
with patch.object(gate, "resolve", return_value=["wicked-vault"]):
|
|
159
|
+
gate.run_gate("verdict", scope="build-1", with_attestations=True, run=run)
|
|
160
|
+
assert "--with-attestations" in seen["cmd"]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_verifier_spec_forwarded_when_present():
|
|
164
|
+
seen = {}
|
|
165
|
+
|
|
166
|
+
def run(cmd, timeout=None):
|
|
167
|
+
seen["cmd"] = cmd
|
|
168
|
+
return RunResult(returncode=0, stdout='{"overall":"PASS"}', stderr="")
|
|
169
|
+
|
|
170
|
+
with patch.object(gate, "resolve", return_value=["wicked-vault"]):
|
|
171
|
+
gate.run_gate("verdict", scope="build-1",
|
|
172
|
+
verifier_spec="/tmp/verify.json", run=run)
|
|
173
|
+
assert "--verifier-spec" in seen["cmd"]
|
|
174
|
+
assert "/tmp/verify.json" in seen["cmd"]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_verifier_spec_absent_is_fail_soft_not_blocking():
|
|
178
|
+
# No verifier_spec given -> the gate still runs and can PASS (I3).
|
|
179
|
+
with patch.object(gate, "resolve", return_value=["wicked-vault"]):
|
|
180
|
+
r = gate.run_gate("verdict", scope="build-1",
|
|
181
|
+
run=_runner(stdout='{"overall":"PASS"}'))
|
|
182
|
+
assert r["satisfied"] is True
|
|
183
|
+
assert "--verifier-spec" not in r.get("argv", [])
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
187
|
+
|
|
188
|
+
Run: `cd ~/Projects/wicked-loom && PYTHONPATH=python python3 -m pytest tests/test_gate.py -v`
|
|
189
|
+
Expected: FAIL — `ModuleNotFoundError: No module named 'loom.gate'`
|
|
190
|
+
|
|
191
|
+
- [ ] **Step 3: Write `gate.py`**
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
"""gate.py — the synchronous, fail-closed produces gate (vault re-derivation).
|
|
195
|
+
|
|
196
|
+
Ported from wicked-garden scripts/qe/vault_gate.py, with the subprocess
|
|
197
|
+
execution injected (the ``run`` parameter, same RunResult shape as compose.py)
|
|
198
|
+
so callers and tests control side effects and no real vault is ever spawned in
|
|
199
|
+
a unit test.
|
|
200
|
+
|
|
201
|
+
Invariants (spec §5):
|
|
202
|
+
I1 — synchronous + in-line: exactly one blocking vault call per invocation;
|
|
203
|
+
no events are consulted. An event NEVER satisfies a gate.
|
|
204
|
+
I2 — fail-closed: vault unresolvable OR unrunnable OR non-JSON ->
|
|
205
|
+
gate "unavailable" / overall "ERROR", satisfied False. Never a pass.
|
|
206
|
+
I3 — fail-soft on the verifier spec only: a present ``verifier_spec`` is
|
|
207
|
+
forwarded to the vault; its ABSENCE never blocks and never vacates the
|
|
208
|
+
gate — the vault falls back to generic detection.
|
|
209
|
+
|
|
210
|
+
Hard gates additionally require an independent attestation: pass
|
|
211
|
+
``with_attestations=True`` -> ``--with-attestations`` forwarded to the vault.
|
|
212
|
+
|
|
213
|
+
The return is a status dict (R4 — surface as data, never raise):
|
|
214
|
+
{satisfied, re_derived, gate, overall, exit_code, argv, detail, error}
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
from __future__ import annotations
|
|
218
|
+
|
|
219
|
+
import json
|
|
220
|
+
from typing import Callable, Optional
|
|
221
|
+
|
|
222
|
+
from loom.compose import RunResult, _default_run
|
|
223
|
+
from loom.resolve import resolve
|
|
224
|
+
|
|
225
|
+
Runner = Callable[..., RunResult]
|
|
226
|
+
|
|
227
|
+
_DEFAULT_TIMEOUT = 120 # seconds; bounds the blocking re-derivation (R5).
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _build_argv(vault_prefix: list, produces: str, scope: str, *,
|
|
231
|
+
with_attestations: bool,
|
|
232
|
+
verifier_spec: Optional[str]) -> list:
|
|
233
|
+
"""The full vault argv: <prefix> cross-check --scope S --phase <produces> ...
|
|
234
|
+
|
|
235
|
+
``produces`` maps to the vault's ``--phase`` (the produces-contract id, e.g.
|
|
236
|
+
"test-report"); ``scope`` maps to ``--scope`` (the project/flow scope).
|
|
237
|
+
"""
|
|
238
|
+
argv = list(vault_prefix) + ["cross-check", "--scope", scope, "--phase", produces]
|
|
239
|
+
if with_attestations:
|
|
240
|
+
argv.append("--with-attestations")
|
|
241
|
+
if verifier_spec:
|
|
242
|
+
argv += ["--verifier-spec", verifier_spec]
|
|
243
|
+
return argv
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def run_gate(produces: str, *, scope: str,
|
|
247
|
+
with_attestations: bool = False,
|
|
248
|
+
verifier_spec: Optional[str] = None,
|
|
249
|
+
run: Runner = _default_run,
|
|
250
|
+
timeout: int = _DEFAULT_TIMEOUT) -> dict:
|
|
251
|
+
"""Re-derive ``produces`` via ``vault cross-check`` and return the verdict.
|
|
252
|
+
|
|
253
|
+
Fail-closed (I2): no resolvable vault, an unrunnable vault, or non-JSON
|
|
254
|
+
output all yield ``satisfied: False`` with ``gate: "unavailable"`` (or
|
|
255
|
+
overall "ERROR") — never an invented pass.
|
|
256
|
+
"""
|
|
257
|
+
vault_prefix = resolve("vault")
|
|
258
|
+
if vault_prefix is None:
|
|
259
|
+
return {
|
|
260
|
+
"satisfied": False,
|
|
261
|
+
"re_derived": False,
|
|
262
|
+
"gate": "unavailable",
|
|
263
|
+
"overall": "ERROR",
|
|
264
|
+
"argv": [],
|
|
265
|
+
"error": "wicked-vault not resolvable",
|
|
266
|
+
"detail": "Gate fails closed — 'done' cannot be self-asserted (I2).",
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
argv = _build_argv(vault_prefix, produces, scope,
|
|
270
|
+
with_attestations=with_attestations,
|
|
271
|
+
verifier_spec=verifier_spec)
|
|
272
|
+
try:
|
|
273
|
+
result = run(argv, timeout=timeout)
|
|
274
|
+
except Exception as e: # noqa: BLE001 — surface as data, fail closed (R4/I2)
|
|
275
|
+
return {
|
|
276
|
+
"satisfied": False,
|
|
277
|
+
"re_derived": False,
|
|
278
|
+
"gate": "unavailable",
|
|
279
|
+
"overall": "ERROR",
|
|
280
|
+
"argv": argv,
|
|
281
|
+
"error": str(e),
|
|
282
|
+
"detail": "vault resolvable but not runnable; gate fails closed (I2).",
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
parsed = json.loads(result.stdout) if (result.stdout or "").strip() else {}
|
|
287
|
+
except json.JSONDecodeError:
|
|
288
|
+
return {
|
|
289
|
+
"satisfied": False,
|
|
290
|
+
"re_derived": True,
|
|
291
|
+
"gate": "vault-cross-check",
|
|
292
|
+
"overall": "ERROR",
|
|
293
|
+
"exit_code": result.returncode,
|
|
294
|
+
"argv": argv,
|
|
295
|
+
"error": "vault returned non-JSON output",
|
|
296
|
+
"detail": result.stderr.strip()[:500],
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
overall = parsed.get("overall", "ERROR")
|
|
300
|
+
return {
|
|
301
|
+
"satisfied": overall == "PASS",
|
|
302
|
+
"re_derived": True,
|
|
303
|
+
"gate": "vault-cross-check",
|
|
304
|
+
"overall": overall,
|
|
305
|
+
"exit_code": result.returncode,
|
|
306
|
+
"argv": argv,
|
|
307
|
+
"claims": parsed.get("claims", []),
|
|
308
|
+
"contract_version": parsed.get("contract_version"),
|
|
309
|
+
"detail": parsed.get("detail"),
|
|
310
|
+
"error": None,
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
315
|
+
|
|
316
|
+
Run: `cd ~/Projects/wicked-loom && PYTHONPATH=python python3 -m pytest tests/test_gate.py -v`
|
|
317
|
+
Expected: PASS (8 tests)
|
|
318
|
+
|
|
319
|
+
- [ ] **Step 5: Commit**
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
cd ~/Projects/wicked-loom
|
|
323
|
+
git add python/loom/gate.py tests/test_gate.py
|
|
324
|
+
git commit -m "feat: synchronous fail-closed vault gate (conduct)"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Task 2: Per-flow state persistence (`flowstate.py`)
|
|
330
|
+
|
|
331
|
+
**Files:**
|
|
332
|
+
- Create: `~/Projects/wicked-loom/python/loom/flowstate.py`
|
|
333
|
+
- Test: `~/Projects/wicked-loom/tests/test_flowstate.py`
|
|
334
|
+
|
|
335
|
+
One JSON file per `flow_id`, phase_manager-style. The state directory is injected (a `Path`) so tests use `tmp_path` and never touch a real home dir — deterministic, no network (spec §10). State records: `flow_id`, the original `phases` list, `current_phase` (index into `phases`), per-phase `gate_verdicts`, `parked` (bool) + `parked_reason`, `status` (`running`|`parked`|`completed`), and `created_at`/`updated_at` timestamps (written, never asserted-on for equality — T1 determinism). Nothing here raises on a normal path; a missing file on load returns `None`.
|
|
336
|
+
|
|
337
|
+
- [ ] **Step 1: Write the failing test**
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
from loom import flowstate
|
|
341
|
+
|
|
342
|
+
_FLOW = {
|
|
343
|
+
"flow_id": "build-1",
|
|
344
|
+
"phases": [
|
|
345
|
+
{"name": "plan", "gate": None, "hitl": "none"},
|
|
346
|
+
{"name": "test", "gate": "produces:test-report", "hitl": "discrete:review"},
|
|
347
|
+
],
|
|
348
|
+
"peers_required": ["vault"],
|
|
349
|
+
"verifier_spec_ref": None,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_new_state_starts_at_phase_zero_running(tmp_path):
|
|
354
|
+
st = flowstate.new_state(_FLOW, state_dir=tmp_path)
|
|
355
|
+
assert st["flow_id"] == "build-1"
|
|
356
|
+
assert st["current_phase"] == 0
|
|
357
|
+
assert st["status"] == "running"
|
|
358
|
+
assert st["parked"] is False
|
|
359
|
+
assert st["gate_verdicts"] == {}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_save_then_load_roundtrips(tmp_path):
|
|
363
|
+
st = flowstate.new_state(_FLOW, state_dir=tmp_path)
|
|
364
|
+
flowstate.save_state(st, state_dir=tmp_path)
|
|
365
|
+
loaded = flowstate.load_state("build-1", state_dir=tmp_path)
|
|
366
|
+
assert loaded["flow_id"] == "build-1"
|
|
367
|
+
assert loaded["phases"] == _FLOW["phases"]
|
|
368
|
+
assert loaded["current_phase"] == 0
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_load_missing_flow_returns_none(tmp_path):
|
|
372
|
+
assert flowstate.load_state("nope", state_dir=tmp_path) is None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def test_state_file_path_is_one_file_per_flow_id(tmp_path):
|
|
376
|
+
flowstate.save_state(flowstate.new_state(_FLOW, state_dir=tmp_path),
|
|
377
|
+
state_dir=tmp_path)
|
|
378
|
+
expected = tmp_path / "build-1.json"
|
|
379
|
+
assert expected.exists()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def test_record_verdict_is_persisted(tmp_path):
|
|
383
|
+
st = flowstate.new_state(_FLOW, state_dir=tmp_path)
|
|
384
|
+
st["gate_verdicts"]["test"] = {"satisfied": True, "overall": "PASS"}
|
|
385
|
+
flowstate.save_state(st, state_dir=tmp_path)
|
|
386
|
+
loaded = flowstate.load_state("build-1", state_dir=tmp_path)
|
|
387
|
+
assert loaded["gate_verdicts"]["test"]["satisfied"] is True
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_unsafe_flow_id_is_rejected(tmp_path):
|
|
391
|
+
bad = dict(_FLOW, flow_id="../etc/passwd")
|
|
392
|
+
try:
|
|
393
|
+
flowstate.new_state(bad, state_dir=tmp_path)
|
|
394
|
+
raised = False
|
|
395
|
+
except ValueError:
|
|
396
|
+
raised = True
|
|
397
|
+
assert raised is True
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
401
|
+
|
|
402
|
+
Run: `cd ~/Projects/wicked-loom && PYTHONPATH=python python3 -m pytest tests/test_flowstate.py -v`
|
|
403
|
+
Expected: FAIL — `ModuleNotFoundError: No module named 'loom.flowstate'`
|
|
404
|
+
|
|
405
|
+
- [ ] **Step 3: Write `flowstate.py`**
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
"""flowstate.py — per-flow JSON state. One file per flow_id.
|
|
409
|
+
|
|
410
|
+
phase_manager-style local state (spec §4.2 "persist phase state", §10 "local
|
|
411
|
+
JSON, deterministic, no network"). The state directory is a parameter so tests
|
|
412
|
+
inject a tmp dir and production injects a project-scoped path; this module never
|
|
413
|
+
hardcodes a home path and never spawns a process.
|
|
414
|
+
|
|
415
|
+
State schema:
|
|
416
|
+
flow_id : str (validated kebab/snake/alphanumeric, max 64 — no path sep)
|
|
417
|
+
phases : list[dict] (the original flow-def phases, verbatim)
|
|
418
|
+
current_phase : int (index into phases; == len(phases) when completed)
|
|
419
|
+
gate_verdicts : dict[phase_name -> verdict dict]
|
|
420
|
+
parked : bool
|
|
421
|
+
parked_reason : str | None
|
|
422
|
+
status : "running" | "parked" | "completed"
|
|
423
|
+
created_at : ISO8601 str (written, never asserted-on)
|
|
424
|
+
updated_at : ISO8601 str (written, never asserted-on)
|
|
425
|
+
|
|
426
|
+
Nothing here raises except on an unsafe flow_id (ValueError — a guard, not a
|
|
427
|
+
swallowed error, R4). A missing state file on load returns None.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
from __future__ import annotations
|
|
431
|
+
|
|
432
|
+
import json
|
|
433
|
+
import re
|
|
434
|
+
from datetime import datetime, timezone
|
|
435
|
+
from pathlib import Path
|
|
436
|
+
from typing import Optional
|
|
437
|
+
|
|
438
|
+
_FLOW_ID_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
|
|
439
|
+
|
|
440
|
+
STATUS_RUNNING = "running"
|
|
441
|
+
STATUS_PARKED = "parked"
|
|
442
|
+
STATUS_COMPLETED = "completed"
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _now() -> str:
|
|
446
|
+
return datetime.now(timezone.utc).isoformat()
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _validate_flow_id(flow_id: str) -> str:
|
|
450
|
+
if not isinstance(flow_id, str) or not _FLOW_ID_RE.match(flow_id):
|
|
451
|
+
raise ValueError(f"unsafe flow_id: {flow_id!r} (kebab/snake/alnum, max 64)")
|
|
452
|
+
return flow_id
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _state_path(flow_id: str, state_dir: Path) -> Path:
|
|
456
|
+
return Path(state_dir) / f"{_validate_flow_id(flow_id)}.json"
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def new_state(flow_def: dict, *, state_dir: Path) -> dict:
|
|
460
|
+
"""Build a fresh running state from a flow definition. Does not persist."""
|
|
461
|
+
flow_id = _validate_flow_id(flow_def["flow_id"])
|
|
462
|
+
now = _now()
|
|
463
|
+
return {
|
|
464
|
+
"flow_id": flow_id,
|
|
465
|
+
"phases": list(flow_def.get("phases", [])),
|
|
466
|
+
"peers_required": list(flow_def.get("peers_required", [])),
|
|
467
|
+
"verifier_spec_ref": flow_def.get("verifier_spec_ref"),
|
|
468
|
+
"current_phase": 0,
|
|
469
|
+
"gate_verdicts": {},
|
|
470
|
+
"parked": False,
|
|
471
|
+
"parked_reason": None,
|
|
472
|
+
"status": STATUS_RUNNING,
|
|
473
|
+
"created_at": now,
|
|
474
|
+
"updated_at": now,
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def save_state(state: dict, *, state_dir: Path) -> Path:
|
|
479
|
+
"""Atomically write the state file for ``state['flow_id']`` and return its path."""
|
|
480
|
+
Path(state_dir).mkdir(parents=True, exist_ok=True)
|
|
481
|
+
state["updated_at"] = _now()
|
|
482
|
+
path = _state_path(state["flow_id"], state_dir)
|
|
483
|
+
tmp = path.with_suffix(".json.tmp")
|
|
484
|
+
tmp.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
485
|
+
tmp.replace(path)
|
|
486
|
+
return path
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def load_state(flow_id: str, *, state_dir: Path) -> Optional[dict]:
|
|
490
|
+
"""Load the state for ``flow_id``; None if no file exists."""
|
|
491
|
+
path = _state_path(flow_id, state_dir)
|
|
492
|
+
if not path.exists():
|
|
493
|
+
return None
|
|
494
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
498
|
+
|
|
499
|
+
Run: `cd ~/Projects/wicked-loom && PYTHONPATH=python python3 -m pytest tests/test_flowstate.py -v`
|
|
500
|
+
Expected: PASS (6 tests)
|
|
501
|
+
|
|
502
|
+
- [ ] **Step 5: Commit**
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
cd ~/Projects/wicked-loom
|
|
506
|
+
git add python/loom/flowstate.py tests/test_flowstate.py
|
|
507
|
+
git commit -m "feat: per-flow JSON state persistence (conduct)"
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Task 3: Best-effort bus emission (`busemit.py`)
|
|
513
|
+
|
|
514
|
+
**Files:**
|
|
515
|
+
- Create: `~/Projects/wicked-loom/python/loom/busemit.py`
|
|
516
|
+
- Test: `~/Projects/wicked-loom/tests/test_busemit.py`
|
|
517
|
+
|
|
518
|
+
Emission ONLY (the §4.3 #3 event-contract producer side). Fire-and-forget, fail-soft: a bus that is unresolvable, errors, or times out NEVER raises and NEVER blocks the caller (invariant I4 — the bus is the spine for everything that is *not* a gate verdict, and an event never satisfies a gate). The bus command resolves via `loom.resolve.resolve("bus")` and runs through an injected `Runner`. Event names are the stable set from spec §4.3: `loom:flow:started|phase-advanced|gate-passed|gate-failed|needs-human|completed`. **No consumer/projector is built here — deferred per D3.**
|
|
519
|
+
|
|
520
|
+
- [ ] **Step 1: Write the failing test**
|
|
521
|
+
|
|
522
|
+
```python
|
|
523
|
+
from unittest.mock import patch
|
|
524
|
+
|
|
525
|
+
from loom import busemit
|
|
526
|
+
from loom.compose import RunResult
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def test_event_names_are_the_spec_set():
|
|
530
|
+
assert busemit.EVENTS["started"] == "loom:flow:started"
|
|
531
|
+
assert busemit.EVENTS["phase-advanced"] == "loom:flow:phase-advanced"
|
|
532
|
+
assert busemit.EVENTS["gate-passed"] == "loom:flow:gate-passed"
|
|
533
|
+
assert busemit.EVENTS["gate-failed"] == "loom:flow:gate-failed"
|
|
534
|
+
assert busemit.EVENTS["needs-human"] == "loom:flow:needs-human"
|
|
535
|
+
assert busemit.EVENTS["completed"] == "loom:flow:completed"
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def test_emit_invokes_bus_with_event_and_payload():
|
|
539
|
+
seen = {}
|
|
540
|
+
|
|
541
|
+
def run(cmd, timeout=None):
|
|
542
|
+
seen["cmd"] = cmd
|
|
543
|
+
return RunResult(returncode=0, stdout="", stderr="")
|
|
544
|
+
|
|
545
|
+
with patch.object(busemit, "resolve", return_value=["wicked-bus"]):
|
|
546
|
+
ok = busemit.emit("started", {"flow_id": "build-1"}, run=run)
|
|
547
|
+
assert ok is True
|
|
548
|
+
assert "loom:flow:started" in seen["cmd"]
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def test_emit_is_fail_soft_when_bus_unresolvable():
|
|
552
|
+
with patch.object(busemit, "resolve", return_value=None):
|
|
553
|
+
ok = busemit.emit("started", {"flow_id": "build-1"})
|
|
554
|
+
assert ok is False # best-effort: reports, never raises
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def test_emit_never_raises_when_bus_errors():
|
|
558
|
+
def boom(cmd, timeout=None):
|
|
559
|
+
raise FileNotFoundError("bus gone")
|
|
560
|
+
|
|
561
|
+
with patch.object(busemit, "resolve", return_value=["wicked-bus"]):
|
|
562
|
+
ok = busemit.emit("needs-human", {"flow_id": "build-1"}, run=boom)
|
|
563
|
+
assert ok is False # swallowed at the boundary by design (I4), reported as data
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def test_emit_unknown_event_is_false_not_raise():
|
|
567
|
+
with patch.object(busemit, "resolve", return_value=["wicked-bus"]):
|
|
568
|
+
ok = busemit.emit("frobnicate", {"flow_id": "build-1"})
|
|
569
|
+
assert ok is False
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
573
|
+
|
|
574
|
+
Run: `cd ~/Projects/wicked-loom && PYTHONPATH=python python3 -m pytest tests/test_busemit.py -v`
|
|
575
|
+
Expected: FAIL — `ModuleNotFoundError: No module named 'loom.busemit'`
|
|
576
|
+
|
|
577
|
+
- [ ] **Step 3: Write `busemit.py`**
|
|
578
|
+
|
|
579
|
+
```python
|
|
580
|
+
"""busemit.py — best-effort fire-and-forget bus emission. Emission ONLY.
|
|
581
|
+
|
|
582
|
+
The bus is the spine for everything that is NOT a gate verdict (spec §4.4/I4):
|
|
583
|
+
phase transitions, needs-human parks, audit. Emission is fire-and-forget — an
|
|
584
|
+
unresolvable, erroring, or slow bus must NEVER raise and NEVER block the flow.
|
|
585
|
+
An emitted event NEVER satisfies a gate (I4) — gates are synchronous and direct
|
|
586
|
+
(gate.py); this module only announces.
|
|
587
|
+
|
|
588
|
+
The bus consumer / projector and any headless reaction to these events are
|
|
589
|
+
DEFERRED (spec D3 / §9 D-headless). This file is the producer side only.
|
|
590
|
+
|
|
591
|
+
Stable event names (spec §4.3 #3):
|
|
592
|
+
loom:flow:started | phase-advanced | gate-passed | gate-failed
|
|
593
|
+
| needs-human | completed
|
|
594
|
+
"""
|
|
595
|
+
|
|
596
|
+
from __future__ import annotations
|
|
597
|
+
|
|
598
|
+
import json
|
|
599
|
+
from typing import Callable
|
|
600
|
+
|
|
601
|
+
from loom.compose import RunResult, _default_run
|
|
602
|
+
from loom.resolve import resolve
|
|
603
|
+
|
|
604
|
+
Runner = Callable[..., RunResult]
|
|
605
|
+
|
|
606
|
+
_EMIT_TIMEOUT = 5 # seconds; emission is best-effort, keep it short (R5).
|
|
607
|
+
|
|
608
|
+
EVENTS: dict = {
|
|
609
|
+
"started": "loom:flow:started",
|
|
610
|
+
"phase-advanced": "loom:flow:phase-advanced",
|
|
611
|
+
"gate-passed": "loom:flow:gate-passed",
|
|
612
|
+
"gate-failed": "loom:flow:gate-failed",
|
|
613
|
+
"needs-human": "loom:flow:needs-human",
|
|
614
|
+
"completed": "loom:flow:completed",
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def emit(event_key: str, payload: dict, *, run: Runner = _default_run) -> bool:
|
|
619
|
+
"""Fire-and-forget emit. Returns True iff the bus accepted it (exit 0).
|
|
620
|
+
|
|
621
|
+
Never raises: an unknown event, an unresolvable bus, or any subprocess
|
|
622
|
+
failure is reported as ``False`` (I4 — the bus is optional infrastructure).
|
|
623
|
+
"""
|
|
624
|
+
event_type = EVENTS.get(event_key)
|
|
625
|
+
if event_type is None:
|
|
626
|
+
return False
|
|
627
|
+
|
|
628
|
+
bus_prefix = resolve("bus")
|
|
629
|
+
if bus_prefix is None:
|
|
630
|
+
return False
|
|
631
|
+
|
|
632
|
+
argv = list(bus_prefix) + ["emit", event_type, json.dumps(payload)]
|
|
633
|
+
try:
|
|
634
|
+
result = run(argv, timeout=_EMIT_TIMEOUT)
|
|
635
|
+
except Exception: # noqa: BLE001 — fire-and-forget; bus is optional (I4)
|
|
636
|
+
return False
|
|
637
|
+
return result.returncode == 0
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
641
|
+
|
|
642
|
+
Run: `cd ~/Projects/wicked-loom && PYTHONPATH=python python3 -m pytest tests/test_busemit.py -v`
|
|
643
|
+
Expected: PASS (5 tests)
|
|
644
|
+
|
|
645
|
+
- [ ] **Step 5: Commit**
|
|
646
|
+
|
|
647
|
+
```bash
|
|
648
|
+
cd ~/Projects/wicked-loom
|
|
649
|
+
git add python/loom/busemit.py tests/test_busemit.py
|
|
650
|
+
git commit -m "feat: best-effort fire-and-forget bus emission (conduct)"
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## Task 4: The flow runner (`flow.py`)
|
|
656
|
+
|
|
657
|
+
**Files:**
|
|
658
|
+
- Create: `~/Projects/wicked-loom/python/loom/flow.py`
|
|
659
|
+
- Test: `~/Projects/wicked-loom/tests/test_flow.py`
|
|
660
|
+
|
|
661
|
+
The generic, **archetype-agnostic** phase-loop (invariant I6 — it branches on the flow definition's `gate`/`hitl` fields, NEVER on archetype names). It wires Task 1 (gate), Task 2 (state), Task 3 (emit). The vault runner and bus runner are injected separately so a flow test never spawns either. Three operations:
|
|
662
|
+
|
|
663
|
+
- `run_flow(flow_def, …)` — create state, emit `started`, then advance.
|
|
664
|
+
- `status(flow_id, …)` — read persisted state (no advance, no side effects beyond read).
|
|
665
|
+
- `resume(flow_id, …)` — reload a parked/running flow and advance again (the human approves a hard gate out-of-band; resume moves past it).
|
|
666
|
+
|
|
667
|
+
**Advance loop (the heart of conduct, spec §3.1 + §4.2):** from `current_phase`, for each phase:
|
|
668
|
+
1. If the phase has no `gate` → emit `phase-advanced`, increment, continue.
|
|
669
|
+
2. Else run the gate (Task 1) → record the verdict in state.
|
|
670
|
+
- Gate **not satisfied** → emit `gate-failed`, persist, STOP (status stays `running`; the flow is blocked on real evidence, not parked-for-human).
|
|
671
|
+
- Gate **satisfied** + `hitl` is `hard:*` → emit `gate-passed` then `needs-human`, set `parked=True`, status `parked`, persist, STOP. **Loom never self-approves a hard gate (I5).**
|
|
672
|
+
- Gate **satisfied** + not hard → emit `gate-passed` + `phase-advanced`, increment, continue.
|
|
673
|
+
3. When `current_phase == len(phases)` → status `completed`, emit `completed`, persist, STOP.
|
|
674
|
+
|
|
675
|
+
`resume` clears `parked` for the phase the human just approved (it advances PAST the parked hard-gate phase without re-running it — the human verdict is the authority there, per I5), then runs the same loop.
|
|
676
|
+
|
|
677
|
+
- [ ] **Step 1: Write the failing test**
|
|
678
|
+
|
|
679
|
+
```python
|
|
680
|
+
from unittest.mock import patch
|
|
681
|
+
|
|
682
|
+
from loom import flow
|
|
683
|
+
from loom.compose import RunResult
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _vault(overall="PASS", code=0):
|
|
687
|
+
def run(cmd, timeout=None):
|
|
688
|
+
import json
|
|
689
|
+
return RunResult(returncode=code, stdout=json.dumps({"overall": overall}),
|
|
690
|
+
stderr="")
|
|
691
|
+
return run
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _silent_bus():
|
|
695
|
+
def run(cmd, timeout=None):
|
|
696
|
+
return RunResult(returncode=0, stdout="", stderr="")
|
|
697
|
+
return run
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _flow_def(flow_id="build-1"):
|
|
701
|
+
return {
|
|
702
|
+
"flow_id": flow_id,
|
|
703
|
+
"phases": [
|
|
704
|
+
{"name": "plan", "gate": None, "hitl": "none"},
|
|
705
|
+
{"name": "implement", "gate": None, "hitl": "none"},
|
|
706
|
+
{"name": "test", "gate": "produces:test-report", "hitl": "discrete:review"},
|
|
707
|
+
{"name": "review", "gate": "produces:verdict", "hitl": "hard:final-verdict"},
|
|
708
|
+
],
|
|
709
|
+
"peers_required": ["vault"],
|
|
710
|
+
"verifier_spec_ref": None,
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def test_gateless_phases_advance_freely(tmp_path):
|
|
715
|
+
fd = {"flow_id": "g-1",
|
|
716
|
+
"phases": [{"name": "a", "gate": None, "hitl": "none"},
|
|
717
|
+
{"name": "b", "gate": None, "hitl": "none"}],
|
|
718
|
+
"peers_required": [], "verifier_spec_ref": None}
|
|
719
|
+
with patch.object(flow, "resolve", return_value=["wicked-vault"]):
|
|
720
|
+
st = flow.run_flow(fd, state_dir=tmp_path,
|
|
721
|
+
vault_run=_vault(), bus_run=_silent_bus())
|
|
722
|
+
assert st["status"] == "completed"
|
|
723
|
+
assert st["current_phase"] == 2
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def test_flow_parks_at_hard_gate_after_pass(tmp_path):
|
|
727
|
+
with patch.object(flow, "resolve", return_value=["wicked-vault"]):
|
|
728
|
+
st = flow.run_flow(_flow_def(), state_dir=tmp_path,
|
|
729
|
+
vault_run=_vault("PASS"), bus_run=_silent_bus())
|
|
730
|
+
assert st["status"] == "parked"
|
|
731
|
+
assert st["parked"] is True
|
|
732
|
+
# parks AT the hard-gate phase ("review", index 3), not past it (I5).
|
|
733
|
+
assert st["current_phase"] == 3
|
|
734
|
+
assert st["gate_verdicts"]["review"]["satisfied"] is True
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def test_flow_stops_unparked_when_gate_fails(tmp_path):
|
|
738
|
+
with patch.object(flow, "resolve", return_value=["wicked-vault"]):
|
|
739
|
+
st = flow.run_flow(_flow_def(), state_dir=tmp_path,
|
|
740
|
+
vault_run=_vault("REJECT"), bus_run=_silent_bus())
|
|
741
|
+
# stops at the first gated phase ("test", index 2) — not parked, just blocked.
|
|
742
|
+
assert st["status"] == "running"
|
|
743
|
+
assert st["parked"] is False
|
|
744
|
+
assert st["current_phase"] == 2
|
|
745
|
+
assert st["gate_verdicts"]["test"]["satisfied"] is False
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def test_flow_fails_closed_when_vault_unresolvable(tmp_path):
|
|
749
|
+
with patch.object(flow, "resolve", return_value=None):
|
|
750
|
+
st = flow.run_flow(_flow_def(), state_dir=tmp_path,
|
|
751
|
+
vault_run=_vault(), bus_run=_silent_bus())
|
|
752
|
+
assert st["status"] == "running" # blocked at the gate, not advanced
|
|
753
|
+
assert st["current_phase"] == 2
|
|
754
|
+
assert st["gate_verdicts"]["test"]["gate"] == "unavailable"
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def test_status_reads_without_advancing(tmp_path):
|
|
758
|
+
with patch.object(flow, "resolve", return_value=["wicked-vault"]):
|
|
759
|
+
flow.run_flow(_flow_def("s-1"), state_dir=tmp_path,
|
|
760
|
+
vault_run=_vault("PASS"), bus_run=_silent_bus())
|
|
761
|
+
st = flow.status("s-1", state_dir=tmp_path)
|
|
762
|
+
assert st["flow_id"] == "s-1"
|
|
763
|
+
assert st["status"] == "parked"
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def test_status_unknown_flow_is_none(tmp_path):
|
|
767
|
+
assert flow.status("nope", state_dir=tmp_path) is None
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def test_resume_advances_past_an_approved_hard_gate(tmp_path):
|
|
771
|
+
with patch.object(flow, "resolve", return_value=["wicked-vault"]):
|
|
772
|
+
flow.run_flow(_flow_def("r-1"), state_dir=tmp_path,
|
|
773
|
+
vault_run=_vault("PASS"), bus_run=_silent_bus())
|
|
774
|
+
# human approved the parked hard gate out-of-band -> resume.
|
|
775
|
+
st = flow.resume("r-1", state_dir=tmp_path,
|
|
776
|
+
vault_run=_vault("PASS"), bus_run=_silent_bus())
|
|
777
|
+
assert st["status"] == "completed"
|
|
778
|
+
assert st["parked"] is False
|
|
779
|
+
assert st["current_phase"] == 4
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def test_resume_unknown_flow_returns_none(tmp_path):
|
|
783
|
+
assert flow.resume("nope", state_dir=tmp_path,
|
|
784
|
+
vault_run=_silent_bus(), bus_run=_silent_bus()) is None
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def test_flow_is_archetype_agnostic(tmp_path):
|
|
788
|
+
# A flow def with a made-up archetype-shaped name still runs purely off
|
|
789
|
+
# gate/hitl fields — loom must not branch on archetype names (I6).
|
|
790
|
+
fd = {"flow_id": "x-1",
|
|
791
|
+
"phases": [{"name": "totally-made-up-phase", "gate": None, "hitl": "none"}],
|
|
792
|
+
"peers_required": [], "verifier_spec_ref": None}
|
|
793
|
+
with patch.object(flow, "resolve", return_value=["wicked-vault"]):
|
|
794
|
+
st = flow.run_flow(fd, state_dir=tmp_path,
|
|
795
|
+
vault_run=_vault(), bus_run=_silent_bus())
|
|
796
|
+
assert st["status"] == "completed"
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
800
|
+
|
|
801
|
+
Run: `cd ~/Projects/wicked-loom && PYTHONPATH=python python3 -m pytest tests/test_flow.py -v`
|
|
802
|
+
Expected: FAIL — `ModuleNotFoundError: No module named 'loom.flow'`
|
|
803
|
+
|
|
804
|
+
- [ ] **Step 3: Write `flow.py`**
|
|
805
|
+
|
|
806
|
+
```python
|
|
807
|
+
"""flow.py — the archetype-agnostic flow runner (conduct orchestration).
|
|
808
|
+
|
|
809
|
+
Executes a declarative flow definition (spec §3.1): advance phases; on a gated
|
|
810
|
+
phase, re-derive synchronously via gate.py; park at a hard gate; persist state
|
|
811
|
+
via flowstate.py; announce transitions via busemit.py (best-effort).
|
|
812
|
+
|
|
813
|
+
Invariants (spec §5):
|
|
814
|
+
I1/I2/I3 — inherited from gate.py (synchronous, fail-closed, fail-soft spec).
|
|
815
|
+
I4 — bus emission is best-effort and never gates; an event never advances a
|
|
816
|
+
phase. Only a re-derived gate verdict advances.
|
|
817
|
+
I5 — autonomy never overrides a hard gate: on a satisfied hard:* gate the flow
|
|
818
|
+
PARKS and emits needs-human; it never self-approves. ``resume`` advances
|
|
819
|
+
past an approved hard gate only because a human acted out-of-band.
|
|
820
|
+
I6 — archetype-agnostic: this module branches ONLY on the flow definition's
|
|
821
|
+
``gate`` / ``hitl`` fields, never on archetype names.
|
|
822
|
+
|
|
823
|
+
The vault runner and bus runner are injected (``vault_run`` / ``bus_run``) so a
|
|
824
|
+
flow test never spawns a real vault or bus.
|
|
825
|
+
"""
|
|
826
|
+
|
|
827
|
+
from __future__ import annotations
|
|
828
|
+
|
|
829
|
+
from pathlib import Path
|
|
830
|
+
from typing import Callable, Optional
|
|
831
|
+
|
|
832
|
+
from loom import busemit, flowstate
|
|
833
|
+
from loom.compose import RunResult, _default_run
|
|
834
|
+
from loom.gate import run_gate
|
|
835
|
+
from loom.resolve import resolve # re-exported so tests can patch flow.resolve
|
|
836
|
+
|
|
837
|
+
Runner = Callable[..., RunResult]
|
|
838
|
+
|
|
839
|
+
_GATE_PREFIX = "produces:"
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _is_hard(hitl: Optional[str]) -> bool:
|
|
843
|
+
"""A hard gate is any hitl discipline of the form ``hard:*`` (spec §3.1)."""
|
|
844
|
+
return isinstance(hitl, str) and hitl.startswith("hard:")
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _produces_of(gate_spec: str) -> str:
|
|
848
|
+
"""Map a flow-def gate string to the vault produces id.
|
|
849
|
+
|
|
850
|
+
"produces:test-report" -> "test-report". A non-"produces:" gate string is
|
|
851
|
+
passed through verbatim (still re-derived; never a vacuous pass — I2).
|
|
852
|
+
"""
|
|
853
|
+
if gate_spec.startswith(_GATE_PREFIX):
|
|
854
|
+
return gate_spec[len(_GATE_PREFIX):]
|
|
855
|
+
return gate_spec
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _advance(state: dict, *, state_dir: Path,
|
|
859
|
+
vault_run: Runner, bus_run: Runner) -> dict:
|
|
860
|
+
"""Run the phase loop from state['current_phase']; persist + return state."""
|
|
861
|
+
phases = state["phases"]
|
|
862
|
+
flow_id = state["flow_id"]
|
|
863
|
+
verifier_spec = state.get("verifier_spec_ref")
|
|
864
|
+
|
|
865
|
+
while state["current_phase"] < len(phases):
|
|
866
|
+
phase = phases[state["current_phase"]]
|
|
867
|
+
name = phase.get("name", str(state["current_phase"]))
|
|
868
|
+
gate_spec = phase.get("gate")
|
|
869
|
+
hitl = phase.get("hitl")
|
|
870
|
+
|
|
871
|
+
if not gate_spec:
|
|
872
|
+
busemit.emit("phase-advanced",
|
|
873
|
+
{"flow_id": flow_id, "phase": name}, run=bus_run)
|
|
874
|
+
state["current_phase"] += 1
|
|
875
|
+
continue
|
|
876
|
+
|
|
877
|
+
verdict = run_gate(_produces_of(gate_spec), scope=flow_id,
|
|
878
|
+
with_attestations=_is_hard(hitl),
|
|
879
|
+
verifier_spec=verifier_spec, run=vault_run)
|
|
880
|
+
state["gate_verdicts"][name] = verdict
|
|
881
|
+
|
|
882
|
+
if not verdict.get("satisfied"):
|
|
883
|
+
busemit.emit("gate-failed",
|
|
884
|
+
{"flow_id": flow_id, "phase": name,
|
|
885
|
+
"overall": verdict.get("overall")}, run=bus_run)
|
|
886
|
+
flowstate.save_state(state, state_dir=state_dir)
|
|
887
|
+
return state # blocked on evidence; status stays "running" (I2)
|
|
888
|
+
|
|
889
|
+
busemit.emit("gate-passed",
|
|
890
|
+
{"flow_id": flow_id, "phase": name}, run=bus_run)
|
|
891
|
+
|
|
892
|
+
if _is_hard(hitl):
|
|
893
|
+
state["parked"] = True
|
|
894
|
+
state["parked_reason"] = f"hard gate at phase '{name}' ({hitl})"
|
|
895
|
+
state["status"] = flowstate.STATUS_PARKED
|
|
896
|
+
busemit.emit("needs-human",
|
|
897
|
+
{"flow_id": flow_id, "phase": name, "hitl": hitl},
|
|
898
|
+
run=bus_run)
|
|
899
|
+
flowstate.save_state(state, state_dir=state_dir)
|
|
900
|
+
return state # PARK — loom never self-approves a hard gate (I5)
|
|
901
|
+
|
|
902
|
+
busemit.emit("phase-advanced",
|
|
903
|
+
{"flow_id": flow_id, "phase": name}, run=bus_run)
|
|
904
|
+
state["current_phase"] += 1
|
|
905
|
+
|
|
906
|
+
state["status"] = flowstate.STATUS_COMPLETED
|
|
907
|
+
busemit.emit("completed", {"flow_id": flow_id}, run=bus_run)
|
|
908
|
+
flowstate.save_state(state, state_dir=state_dir)
|
|
909
|
+
return state
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def run_flow(flow_def: dict, *, state_dir: Path,
|
|
913
|
+
vault_run: Runner = _default_run,
|
|
914
|
+
bus_run: Runner = _default_run) -> dict:
|
|
915
|
+
"""Start a new flow: build state, emit started, advance through phases."""
|
|
916
|
+
state = flowstate.new_state(flow_def, state_dir=state_dir)
|
|
917
|
+
flowstate.save_state(state, state_dir=state_dir)
|
|
918
|
+
busemit.emit("started", {"flow_id": state["flow_id"]}, run=bus_run)
|
|
919
|
+
return _advance(state, state_dir=state_dir,
|
|
920
|
+
vault_run=vault_run, bus_run=bus_run)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def status(flow_id: str, *, state_dir: Path) -> Optional[dict]:
|
|
924
|
+
"""Read persisted flow state without advancing or any side effect."""
|
|
925
|
+
return flowstate.load_state(flow_id, state_dir=state_dir)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def resume(flow_id: str, *, state_dir: Path,
|
|
929
|
+
vault_run: Runner = _default_run,
|
|
930
|
+
bus_run: Runner = _default_run) -> Optional[dict]:
|
|
931
|
+
"""Resume a parked/running flow. If parked at a hard gate, a human has
|
|
932
|
+
approved it out-of-band (I5) — advance PAST that phase without re-running
|
|
933
|
+
its gate, then continue the normal loop."""
|
|
934
|
+
state = flowstate.load_state(flow_id, state_dir=state_dir)
|
|
935
|
+
if state is None:
|
|
936
|
+
return None
|
|
937
|
+
if state.get("parked"):
|
|
938
|
+
state["parked"] = False
|
|
939
|
+
state["parked_reason"] = None
|
|
940
|
+
state["status"] = flowstate.STATUS_RUNNING
|
|
941
|
+
state["current_phase"] += 1 # human-approved hard gate: step past it
|
|
942
|
+
return _advance(state, state_dir=state_dir,
|
|
943
|
+
vault_run=vault_run, bus_run=bus_run)
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
947
|
+
|
|
948
|
+
Run: `cd ~/Projects/wicked-loom && PYTHONPATH=python python3 -m pytest tests/test_flow.py -v`
|
|
949
|
+
Expected: PASS (9 tests)
|
|
950
|
+
|
|
951
|
+
- [ ] **Step 5: Commit**
|
|
952
|
+
|
|
953
|
+
```bash
|
|
954
|
+
cd ~/Projects/wicked-loom
|
|
955
|
+
git add python/loom/flow.py tests/test_flow.py
|
|
956
|
+
git commit -m "feat: archetype-agnostic flow runner with park-at-hard-gate (conduct)"
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
---
|
|
960
|
+
|
|
961
|
+
## Task 5: CLI surface — `gate` + `flow` (run|status|resume)
|
|
962
|
+
|
|
963
|
+
**Files:**
|
|
964
|
+
- Modify: `~/Projects/wicked-loom/python/loom/cli.py`
|
|
965
|
+
- Modify: `~/Projects/wicked-loom/tests/test_cli.py`
|
|
966
|
+
|
|
967
|
+
Extend Plan A's dispatch with two commands. Logic stays in `gate.py`/`flow.py` (R6 — `cli.py` only parses args + formats JSON). State directory defaults to a project-scoped local path; a `--state-dir` flag overrides it (tests pass `--state-dir`).
|
|
968
|
+
|
|
969
|
+
- `loom gate <produces> [--scope S] [--verifier-spec PATH] [--with-attestations]` → `{"gate": <verdict>}`; exit 0 iff satisfied.
|
|
970
|
+
- `loom flow run <flow-def.json> [--state-dir D]` → `{"flow": <state>}`; exit 0 iff status `completed`, 2 if parked/blocked.
|
|
971
|
+
- `loom flow status <flow-id> [--state-dir D]` → `{"flow": <state>}`; exit 0 if found, 1 if not.
|
|
972
|
+
- `loom flow resume <flow-id> [--state-dir D]` → `{"flow": <state>}`; exit 0 iff `completed`, 2 if parked/blocked, 1 if not found.
|
|
973
|
+
|
|
974
|
+
- [ ] **Step 1: Write the failing test (append to `tests/test_cli.py`)**
|
|
975
|
+
|
|
976
|
+
```python
|
|
977
|
+
import json as _json
|
|
978
|
+
import os
|
|
979
|
+
from unittest.mock import patch
|
|
980
|
+
|
|
981
|
+
# (re-uses the _run helper + imports already at the top of test_cli.py)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def test_gate_command_satisfied_exits_zero():
|
|
985
|
+
verdict = {"satisfied": True, "overall": "PASS", "gate": "vault-cross-check"}
|
|
986
|
+
with patch("loom.cli.run_gate", return_value=verdict) as m:
|
|
987
|
+
code, out = _run(["gate", "test-report", "--scope", "build-1"])
|
|
988
|
+
assert code == 0
|
|
989
|
+
assert _json.loads(out)["gate"]["satisfied"] is True
|
|
990
|
+
m.assert_called_once()
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def test_gate_command_unsatisfied_exits_one():
|
|
994
|
+
verdict = {"satisfied": False, "overall": "REJECT", "gate": "vault-cross-check"}
|
|
995
|
+
with patch("loom.cli.run_gate", return_value=verdict):
|
|
996
|
+
code, _ = _run(["gate", "test-report", "--scope", "build-1"])
|
|
997
|
+
assert code == 1
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def test_gate_command_forwards_flags():
|
|
1001
|
+
with patch("loom.cli.run_gate", return_value={"satisfied": True}) as m:
|
|
1002
|
+
_run(["gate", "verdict", "--scope", "b1",
|
|
1003
|
+
"--verifier-spec", "/tmp/v.json", "--with-attestations"])
|
|
1004
|
+
_, kwargs = m.call_args
|
|
1005
|
+
assert kwargs["scope"] == "b1"
|
|
1006
|
+
assert kwargs["verifier_spec"] == "/tmp/v.json"
|
|
1007
|
+
assert kwargs["with_attestations"] is True
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def test_gate_requires_produces_arg():
|
|
1011
|
+
code, _ = _run(["gate"])
|
|
1012
|
+
assert code == 2
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def test_flow_run_completed_exits_zero(tmp_path):
|
|
1016
|
+
fd = {"flow_id": "cli-1",
|
|
1017
|
+
"phases": [{"name": "a", "gate": None, "hitl": "none"}],
|
|
1018
|
+
"peers_required": [], "verifier_spec_ref": None}
|
|
1019
|
+
p = tmp_path / "flow.json"
|
|
1020
|
+
p.write_text(_json.dumps(fd), encoding="utf-8")
|
|
1021
|
+
completed = {"flow_id": "cli-1", "status": "completed"}
|
|
1022
|
+
with patch("loom.cli.run_flow", return_value=completed) as m:
|
|
1023
|
+
code, out = _run(["flow", "run", str(p), "--state-dir", str(tmp_path)])
|
|
1024
|
+
assert code == 0
|
|
1025
|
+
assert _json.loads(out)["flow"]["status"] == "completed"
|
|
1026
|
+
m.assert_called_once()
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def test_flow_run_parked_exits_two(tmp_path):
|
|
1030
|
+
fd = {"flow_id": "cli-2", "phases": [], "peers_required": [],
|
|
1031
|
+
"verifier_spec_ref": None}
|
|
1032
|
+
p = tmp_path / "flow.json"
|
|
1033
|
+
p.write_text(_json.dumps(fd), encoding="utf-8")
|
|
1034
|
+
with patch("loom.cli.run_flow", return_value={"status": "parked"}):
|
|
1035
|
+
code, _ = _run(["flow", "run", str(p), "--state-dir", str(tmp_path)])
|
|
1036
|
+
assert code == 2
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def test_flow_status_found_exits_zero(tmp_path):
|
|
1040
|
+
with patch("loom.cli.flow_status", return_value={"flow_id": "x", "status": "running"}):
|
|
1041
|
+
code, out = _run(["flow", "status", "x", "--state-dir", str(tmp_path)])
|
|
1042
|
+
assert code == 0
|
|
1043
|
+
assert _json.loads(out)["flow"]["flow_id"] == "x"
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def test_flow_status_missing_exits_one(tmp_path):
|
|
1047
|
+
with patch("loom.cli.flow_status", return_value=None):
|
|
1048
|
+
code, _ = _run(["flow", "status", "nope", "--state-dir", str(tmp_path)])
|
|
1049
|
+
assert code == 1
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def test_flow_resume_completed_exits_zero(tmp_path):
|
|
1053
|
+
with patch("loom.cli.flow_resume", return_value={"status": "completed"}):
|
|
1054
|
+
code, _ = _run(["flow", "resume", "x", "--state-dir", str(tmp_path)])
|
|
1055
|
+
assert code == 0
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def test_flow_resume_missing_exits_one(tmp_path):
|
|
1059
|
+
with patch("loom.cli.flow_resume", return_value=None):
|
|
1060
|
+
code, _ = _run(["flow", "resume", "nope", "--state-dir", str(tmp_path)])
|
|
1061
|
+
assert code == 1
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def test_flow_unknown_subcommand_exits_two():
|
|
1065
|
+
code, _ = _run(["flow", "frobnicate"])
|
|
1066
|
+
assert code == 2
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
1070
|
+
|
|
1071
|
+
Run: `cd ~/Projects/wicked-loom && PYTHONPATH=python python3 -m pytest tests/test_cli.py -v`
|
|
1072
|
+
Expected: FAIL — the new tests error (`AttributeError: <module 'loom.cli'> does not have the attribute 'run_gate'` / unknown command `gate`). The 6 Plan A cli tests still PASS.
|
|
1073
|
+
|
|
1074
|
+
- [ ] **Step 3: Edit `cli.py` — add imports**
|
|
1075
|
+
|
|
1076
|
+
Replace the import block (currently importing only `manifest`, `check_all`, `install_peer`, `resolve`) so it also brings in the conduct entry points. The full new import block:
|
|
1077
|
+
|
|
1078
|
+
```python
|
|
1079
|
+
from __future__ import annotations
|
|
1080
|
+
|
|
1081
|
+
import json
|
|
1082
|
+
import sys
|
|
1083
|
+
from pathlib import Path
|
|
1084
|
+
|
|
1085
|
+
from loom import manifest
|
|
1086
|
+
from loom.compose import check_all, install_peer
|
|
1087
|
+
from loom.resolve import resolve
|
|
1088
|
+
from loom.gate import run_gate
|
|
1089
|
+
from loom.flow import run_flow, status as flow_status, resume as flow_resume
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
- [ ] **Step 4: Edit `cli.py` — add the state-dir default + `gate` handler**
|
|
1093
|
+
|
|
1094
|
+
Add this helper and handler above the `_DISPATCH` table:
|
|
1095
|
+
|
|
1096
|
+
```python
|
|
1097
|
+
def _default_state_dir() -> Path:
|
|
1098
|
+
"""Project-scoped local state dir. Honors WICKED_LOOM_STATE_DIR; else a
|
|
1099
|
+
cwd-anchored .wicked-loom/flows dir (deterministic, no network — spec §10)."""
|
|
1100
|
+
import os
|
|
1101
|
+
override = os.environ.get("WICKED_LOOM_STATE_DIR", "").strip()
|
|
1102
|
+
if override:
|
|
1103
|
+
return Path(override)
|
|
1104
|
+
return Path.cwd() / ".wicked-loom" / "flows"
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def _opt(args: list, name: str) -> "str | None":
|
|
1108
|
+
"""Return the value following ``--name`` in args, or None."""
|
|
1109
|
+
if name in args:
|
|
1110
|
+
i = args.index(name)
|
|
1111
|
+
if i + 1 < len(args):
|
|
1112
|
+
return args[i + 1]
|
|
1113
|
+
return None
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def _cmd_gate(args: list) -> int:
|
|
1117
|
+
positional = [a for a in args if not a.startswith("--")]
|
|
1118
|
+
# Strip values that belong to --scope / --verifier-spec from positionals.
|
|
1119
|
+
scope = _opt(args, "--scope") or "default"
|
|
1120
|
+
verifier_spec = _opt(args, "--verifier-spec")
|
|
1121
|
+
consumed = {scope, verifier_spec}
|
|
1122
|
+
produces = next((a for a in positional if a not in consumed), None)
|
|
1123
|
+
if produces is None:
|
|
1124
|
+
_emit({"error": "usage: loom gate <produces> [--scope S] "
|
|
1125
|
+
"[--verifier-spec PATH] [--with-attestations]"})
|
|
1126
|
+
return 2
|
|
1127
|
+
verdict = run_gate(produces, scope=scope, verifier_spec=verifier_spec,
|
|
1128
|
+
with_attestations="--with-attestations" in args)
|
|
1129
|
+
_emit({"gate": verdict})
|
|
1130
|
+
return 0 if verdict.get("satisfied") else 1
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
- [ ] **Step 5: Edit `cli.py` — add the `flow` handler**
|
|
1134
|
+
|
|
1135
|
+
Add this handler above the `_DISPATCH` table:
|
|
1136
|
+
|
|
1137
|
+
```python
|
|
1138
|
+
def _cmd_flow(args: list) -> int:
|
|
1139
|
+
if not args:
|
|
1140
|
+
_emit({"error": "usage: loom flow <run|status|resume> ..."})
|
|
1141
|
+
return 2
|
|
1142
|
+
sub, rest = args[0], args[1:]
|
|
1143
|
+
state_dir = Path(_opt(rest, "--state-dir") or _default_state_dir())
|
|
1144
|
+
positional = [a for a in rest if not a.startswith("--") and a != str(state_dir)]
|
|
1145
|
+
|
|
1146
|
+
if sub == "run":
|
|
1147
|
+
if not positional:
|
|
1148
|
+
_emit({"error": "usage: loom flow run <flow-def.json> [--state-dir D]"})
|
|
1149
|
+
return 2
|
|
1150
|
+
flow_def = json.loads(Path(positional[0]).read_text(encoding="utf-8"))
|
|
1151
|
+
st = run_flow(flow_def, state_dir=state_dir)
|
|
1152
|
+
_emit({"flow": st})
|
|
1153
|
+
return 0 if st.get("status") == "completed" else 2
|
|
1154
|
+
|
|
1155
|
+
if sub == "status":
|
|
1156
|
+
if not positional:
|
|
1157
|
+
_emit({"error": "usage: loom flow status <flow-id> [--state-dir D]"})
|
|
1158
|
+
return 2
|
|
1159
|
+
st = flow_status(positional[0], state_dir=state_dir)
|
|
1160
|
+
_emit({"flow": st})
|
|
1161
|
+
return 0 if st is not None else 1
|
|
1162
|
+
|
|
1163
|
+
if sub == "resume":
|
|
1164
|
+
if not positional:
|
|
1165
|
+
_emit({"error": "usage: loom flow resume <flow-id> [--state-dir D]"})
|
|
1166
|
+
return 2
|
|
1167
|
+
st = flow_resume(positional[0], state_dir=state_dir)
|
|
1168
|
+
_emit({"flow": st})
|
|
1169
|
+
if st is None:
|
|
1170
|
+
return 1
|
|
1171
|
+
return 0 if st.get("status") == "completed" else 2
|
|
1172
|
+
|
|
1173
|
+
_emit({"error": f"unknown flow subcommand: {sub}",
|
|
1174
|
+
"subcommands": ["run", "status", "resume"]})
|
|
1175
|
+
return 2
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
- [ ] **Step 6: Edit `cli.py` — register both in `_DISPATCH`**
|
|
1179
|
+
|
|
1180
|
+
Change the dispatch table to include the conduct commands:
|
|
1181
|
+
|
|
1182
|
+
```python
|
|
1183
|
+
_DISPATCH = {
|
|
1184
|
+
"resolve": _cmd_resolve,
|
|
1185
|
+
"doctor": _cmd_doctor,
|
|
1186
|
+
"compose": _cmd_compose,
|
|
1187
|
+
"gate": _cmd_gate,
|
|
1188
|
+
"flow": _cmd_flow,
|
|
1189
|
+
}
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
- [ ] **Step 7: Run test to verify it passes**
|
|
1193
|
+
|
|
1194
|
+
Run: `cd ~/Projects/wicked-loom && PYTHONPATH=python python3 -m pytest tests/test_cli.py -v`
|
|
1195
|
+
Expected: PASS — 17 tests (6 Plan A + 11 new conduct cases).
|
|
1196
|
+
|
|
1197
|
+
- [ ] **Step 8: Smoke the full path through the Node shim (fail-closed, no vault)**
|
|
1198
|
+
|
|
1199
|
+
Run:
|
|
1200
|
+
```bash
|
|
1201
|
+
cd ~/Projects/wicked-loom && WICKED_VAULT_BIN="" node bin/loom.mjs gate test-report --scope smoke
|
|
1202
|
+
```
|
|
1203
|
+
Expected: a JSON line whose `gate.gate == "unavailable"` and `gate.satisfied == false`, exit code 1 — proving the shim → python3 → conduct path works AND the gate fails closed when the vault is killed (I2). (`echo $?` → `1`.)
|
|
1204
|
+
|
|
1205
|
+
- [ ] **Step 9: Commit**
|
|
1206
|
+
|
|
1207
|
+
```bash
|
|
1208
|
+
cd ~/Projects/wicked-loom
|
|
1209
|
+
git add python/loom/cli.py tests/test_cli.py
|
|
1210
|
+
git commit -m "feat: conduct CLI — gate + flow run/status/resume"
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
---
|
|
1214
|
+
|
|
1215
|
+
## Task 6: README conduct section + full suite + publish-readiness
|
|
1216
|
+
|
|
1217
|
+
**Files:**
|
|
1218
|
+
- Modify: `~/Projects/wicked-loom/README.md`
|
|
1219
|
+
|
|
1220
|
+
- [ ] **Step 1: Append a conduct section to `README.md`**
|
|
1221
|
+
|
|
1222
|
+
```markdown
|
|
1223
|
+
## Conduct (gate + flow)
|
|
1224
|
+
|
|
1225
|
+
Synchronous, fail-closed evidence gating and an archetype-agnostic flow runtime.
|
|
1226
|
+
|
|
1227
|
+
npx wicked-loom gate test-report --scope build-1 # re-derive one produces via the vault
|
|
1228
|
+
npx wicked-loom gate verdict --scope b1 --with-attestations
|
|
1229
|
+
npx wicked-loom flow run ./flow-def.json # run a flow definition
|
|
1230
|
+
npx wicked-loom flow status build-1 # read a flow's state
|
|
1231
|
+
npx wicked-loom flow resume build-1 # continue past an approved hard gate
|
|
1232
|
+
|
|
1233
|
+
**Invariants:** gates are synchronous and re-derive every call (an event never
|
|
1234
|
+
satisfies a gate); a missing vault fails **closed** (`gate: "unavailable"`,
|
|
1235
|
+
never a pass); the verifier spec is fail-**soft** (absent → generic detection,
|
|
1236
|
+
never blocks). The runner is archetype-agnostic — it executes any flow
|
|
1237
|
+
definition (`phases[]` with optional `gate`/`hitl`, `peers_required`,
|
|
1238
|
+
`verifier_spec_ref`) and parks at any `hard:*` gate, never self-approving.
|
|
1239
|
+
|
|
1240
|
+
The headless bus-consumer / unattended execution mode is **deferred** — this
|
|
1241
|
+
release emits transition events best-effort but does not react to them.
|
|
1242
|
+
|
|
1243
|
+
## Flow definition
|
|
1244
|
+
|
|
1245
|
+
{
|
|
1246
|
+
"flow_id": "build-1",
|
|
1247
|
+
"phases": [
|
|
1248
|
+
{ "name": "plan", "gate": null, "hitl": "none" },
|
|
1249
|
+
{ "name": "test", "gate": "produces:test-report", "hitl": "discrete:review" },
|
|
1250
|
+
{ "name": "review", "gate": "produces:verdict", "hitl": "hard:final-verdict" }
|
|
1251
|
+
],
|
|
1252
|
+
"peers_required": ["vault", "testing"],
|
|
1253
|
+
"verifier_spec_ref": null
|
|
1254
|
+
}
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
- [ ] **Step 2: Run the full suite**
|
|
1258
|
+
|
|
1259
|
+
Run: `cd ~/Projects/wicked-loom && PYTHONPATH=python python3 -m pytest -v`
|
|
1260
|
+
Expected: PASS — 50 tests total: 22 Plan A (4 manifest + 5 resolve + 7 compose + 6 cli)… now cli is 17, so the count is **22 Plan A non-cli + new** → exact tally: 4 manifest + 5 resolve + 7 compose + 17 cli + 8 gate + 6 flowstate + 5 busemit + 9 flow = **61 tests**.
|
|
1261
|
+
|
|
1262
|
+
- [ ] **Step 3: Dry-run the npm package contents**
|
|
1263
|
+
|
|
1264
|
+
Run: `cd ~/Projects/wicked-loom && npm pack --dry-run`
|
|
1265
|
+
Expected: the tarball lists `bin/`, `python/` (now including `gate.py`, `flow.py`, `flowstate.py`, `busemit.py`), `README.md`, `package.json` — and NOT `tests/` or `__pycache__/`.
|
|
1266
|
+
|
|
1267
|
+
- [ ] **Step 4: Commit**
|
|
1268
|
+
|
|
1269
|
+
```bash
|
|
1270
|
+
cd ~/Projects/wicked-loom
|
|
1271
|
+
git add README.md
|
|
1272
|
+
git commit -m "docs: README conduct section + flow-def reference"
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
- [ ] **Step 5: STOP — version bump + publish are human-gated**
|
|
1276
|
+
|
|
1277
|
+
Do **not** run `npm publish` autonomously. Conduct adds a feature surface → this is a **minor** bump (`0.1.0` → `0.2.0`); report to the operator that conduct is implemented (suite green, `npm pack` clean) and let them bump `package.json`/`__init__.py`, publish, and tag. The garden-side cutover (shelling garden's `gate`/`flow` shims to loom + the archetype→flow compiler) is a separate plan.
|
|
1278
|
+
|
|
1279
|
+
---
|
|
1280
|
+
|
|
1281
|
+
## Self-Review
|
|
1282
|
+
|
|
1283
|
+
**1. Spec coverage** — checked against §4.2 (conduct module), §4.3 (surfaces/CLI/events), §5 (invariants I1–I6):
|
|
1284
|
+
|
|
1285
|
+
| Spec item | Where realized |
|
|
1286
|
+
|---|---|
|
|
1287
|
+
| §4.2 Execute a gate — sync vault re-derive via `cross-check` | Task 1 `gate.run_gate` (ported from `vault_gate.cross_check`). ✓ |
|
|
1288
|
+
| §4.2 Missing vault → `unavailable`, fail-closed, never vacuous pass (**I2**) | Task 1 `test_gate_fails_closed_when_vault_unresolvable` / `_unrunnable` / `_on_non_json_output`. ✓ |
|
|
1289
|
+
| §4.2 Hard gates require independent attestation (`--with-attestations`) | Task 1 `test_with_attestations_forwarded`; Task 4 forwards `with_attestations=_is_hard(hitl)`. ✓ |
|
|
1290
|
+
| §4.2 `verifier_spec_ref` present → read it; absent/stale → generic, never blocks (**I3**) | Task 1 `test_verifier_spec_forwarded` + `_absent_is_fail_soft`; Task 4 threads `verifier_spec_ref` through. ✓ |
|
|
1291
|
+
| §4.2 Run a flow — advance phases, persist state | Task 4 `run_flow`/`_advance` + Task 2 `flowstate`. ✓ |
|
|
1292
|
+
| §4.2 Park at hard gate — stop + emit `needs-human`, do not self-approve (**I5**) | Task 4 `test_flow_parks_at_hard_gate_after_pass`. ✓ |
|
|
1293
|
+
| §4.2 Project (bus consumer / headless) | **DEFERRED** — explicitly out of scope (D3 / §9). Stated up-front + in README. ✓ (intentional gap) |
|
|
1294
|
+
| §4.3 CLI `loom gate <produces> [--verifier-spec] [--with-attestations]` | Task 5 `_cmd_gate`. ✓ |
|
|
1295
|
+
| §4.3 CLI `loom flow run|status|resume` | Task 5 `_cmd_flow`. ✓ |
|
|
1296
|
+
| §4.3 Event contract `loom:flow:started\|phase-advanced\|gate-passed\|gate-failed\|needs-human\|completed` | Task 3 `busemit.EVENTS` (exact strings asserted in `test_event_names_are_the_spec_set`); emitted in Task 4. ✓ |
|
|
1297
|
+
| §4.3 Bus-consumer / daemon surface | **DEFERRED** (D3). ✓ (intentional gap) |
|
|
1298
|
+
| **I1** gate synchronous + in-line, event never satisfies | Task 1 (one blocking call, no events read); reinforced by Task 3/4 separation (bus is emission-only). ✓ |
|
|
1299
|
+
| **I2** fail-closed | Task 1 + Task 4 `test_flow_fails_closed_when_vault_unresolvable`. ✓ |
|
|
1300
|
+
| **I3** fail-soft on verifier spec only | Task 1 (absence never blocks). ✓ |
|
|
1301
|
+
| **I4** bus = spine for non-verdict; event never satisfies a gate | Task 3 `busemit` is fire-and-forget, returns bool, never feeds gate logic; Task 4 ignores emit results. ✓ |
|
|
1302
|
+
| **I5** autonomy never overrides a hard gate | Task 4 park-at-hard-gate; `resume` only advances because a human acted out-of-band. ✓ |
|
|
1303
|
+
| **I6** archetype-agnostic | Task 4 `test_flow_is_archetype_agnostic`; `flow.py` branches only on `gate`/`hitl`. ✓ |
|
|
1304
|
+
| §5/§10 state deterministic, no network | Task 2 `flowstate` (injected `state_dir`, atomic write, no subprocess); flow/gate runners injected. ✓ |
|
|
1305
|
+
| Reuse `resolve.resolve()` for vault resolution | Task 1 `gate.py` imports `from loom.resolve import resolve`. ✓ |
|
|
1306
|
+
| Match compose.py runner-injection pattern | Tasks 1/3/4 use `RunResult`/`Runner`/`_default_run` from `compose.py`. ✓ |
|
|
1307
|
+
|
|
1308
|
+
No unintended gaps. The only uncovered spec bullets (§4.2 "Project", §4.3 surface #2 daemon) are the **deferred** headless mode — explicitly excluded by D3 and flagged in three places (header "Out of scope", README, this table).
|
|
1309
|
+
|
|
1310
|
+
**2. Placeholder scan** — No `TBD`/`TODO`/"add error handling"/"handle edge cases"/"similar to Task N". Every code step shows complete, runnable code. Every run step shows the exact `PYTHONPATH=python python3 -m pytest …` command and expected pass/fail. Error handling is concrete everywhere (gate fail-closed branches written out; busemit `except Exception` documented as the I4 boundary; flowstate `ValueError` on unsafe id). ✓
|
|
1311
|
+
|
|
1312
|
+
**3. Type consistency** —
|
|
1313
|
+
- `RunResult(returncode, stdout, stderr)` and the `Runner = Callable[..., RunResult]` alias are imported from `compose.py` and used identically in `gate.py`, `busemit.py`, `flow.py`, and every test (`_runner`/`_vault`/`_silent_bus` all build `RunResult`). ✓
|
|
1314
|
+
- `run_gate(produces, *, scope, with_attestations=False, verifier_spec=None, run=…, timeout=…) -> dict`: the signature in Task 1 matches every call site — Task 4 (`run_gate(_produces_of(gate_spec), scope=flow_id, with_attestations=_is_hard(hitl), verifier_spec=verifier_spec, run=vault_run)`) and Task 5 (`run_gate(produces, scope=scope, verifier_spec=verifier_spec, with_attestations=…)`). The CLI test asserts kwargs `scope`/`verifier_spec`/`with_attestations` — all present. ✓
|
|
1315
|
+
- `flowstate`: `new_state(flow_def, *, state_dir)`, `save_state(state, *, state_dir)`, `load_state(flow_id, *, state_dir)` — used with identical keyword `state_dir` in `flow.py` and the tests. Status constants `STATUS_RUNNING/PARKED/COMPLETED` referenced from `flow.py` consistently. ✓
|
|
1316
|
+
- `flow`: `run_flow(flow_def, *, state_dir, vault_run, bus_run)`, `status(flow_id, *, state_dir)`, `resume(flow_id, *, state_dir, vault_run, bus_run)` — `cli.py` imports them as `run_flow`, `status as flow_status`, `resume as flow_resume` and calls with matching kwargs. ✓
|
|
1317
|
+
- `busemit.emit(event_key, payload, *, run) -> bool` — called from `flow.py` with the event keys `"started"`/`"phase-advanced"`/`"gate-passed"`/`"gate-failed"`/`"needs-human"`/`"completed"`, all of which are keys in `EVENTS`. ✓
|
|
1318
|
+
- State dict keys (`flow_id`, `phases`, `current_phase`, `gate_verdicts`, `parked`, `parked_reason`, `status`, `verifier_spec_ref`) are written by `flowstate.new_state` and read by `flow._advance`/`resume` with the same names. ✓
|
|
1319
|
+
|
|
1320
|
+
One self-review fix applied inline: Task 6 Step 2's expected count was first written as "50" then corrected to the exact tally **61** (4+5+7+17+8+6+5+9), because cli grows from 6 → 17 tests in Task 5. The per-task expected counts (8/6/5/9/17) are the authoritative figures.
|
|
1321
|
+
|
|
1322
|
+
---
|
|
1323
|
+
|
|
1324
|
+
## Execution Handoff
|
|
1325
|
+
|
|
1326
|
+
Plan complete and saved to `docs/plans/2026-06-08-wicked-loom-conduct.md`. Two execution options:
|
|
1327
|
+
|
|
1328
|
+
1. **Subagent-Driven (recommended)** — fresh subagent per task, two-stage review between tasks, fast iteration. REQUIRED SUB-SKILL: `superpowers:subagent-driven-development`.
|
|
1329
|
+
2. **Inline Execution** — execute tasks in this session with checkpoints. REQUIRED SUB-SKILL: `superpowers:executing-plans`.
|
|
1330
|
+
|
|
1331
|
+
Notes for the executor:
|
|
1332
|
+
- Execution happens in the **existing** `~/Projects/wicked-loom` repo (the package Plan A created), NOT in wicked-garden. Plan A (compose) must already be merged there.
|
|
1333
|
+
- Read garden's `scripts/qe/vault_gate.py` and `scripts/crew/phase_manager.py` for grounding before Task 1/Task 2, but **import nothing from garden** — loom is a standalone primitive (success criterion #1).
|
|
1334
|
+
- Every test is hermetic: vault and bus subprocesses are injected; `state_dir` is a `tmp_path`. No test may spawn a real vault/bus or touch the network (T1/T3).
|
|
1335
|
+
- The headless daemon / bus-consumer is DEFERRED (D3). Do not build it. If a need surfaces, it is a separate plan that consumes the event contract `busemit.py` already produces.
|
|
1336
|
+
|
|
1337
|
+
**Which approach?**
|