godpowers 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +37 -0
- package/CHANGELOG.md +639 -0
- package/INSPIRATION.md +52 -0
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/SKILL.md +500 -0
- package/agents/god-archaeologist.md +139 -0
- package/agents/god-architect.md +92 -0
- package/agents/god-auditor.md +150 -0
- package/agents/god-browser-tester.md +144 -0
- package/agents/god-context-writer.md +137 -0
- package/agents/god-coordinator.md +138 -0
- package/agents/god-debt-assessor.md +132 -0
- package/agents/god-debugger.md +77 -0
- package/agents/god-deploy-engineer.md +87 -0
- package/agents/god-deps-auditor.md +111 -0
- package/agents/god-design-reviewer.md +137 -0
- package/agents/god-designer.md +171 -0
- package/agents/god-docs-writer.md +102 -0
- package/agents/god-executor.md +76 -0
- package/agents/god-explorer.md +110 -0
- package/agents/god-harden-auditor.md +163 -0
- package/agents/god-incident-investigator.md +144 -0
- package/agents/god-launch-strategist.md +103 -0
- package/agents/god-migration-strategist.md +126 -0
- package/agents/god-observability-engineer.md +76 -0
- package/agents/god-orchestrator.md +728 -0
- package/agents/god-org-context-loader.md +124 -0
- package/agents/god-planner.md +73 -0
- package/agents/god-pm.md +105 -0
- package/agents/god-quality-reviewer.md +74 -0
- package/agents/god-reconciler.md +230 -0
- package/agents/god-reconstructor.md +124 -0
- package/agents/god-repo-scaffolder.md +60 -0
- package/agents/god-retrospective.md +109 -0
- package/agents/god-roadmap-reconciler.md +123 -0
- package/agents/god-roadmap-updater.md +89 -0
- package/agents/god-roadmapper.md +82 -0
- package/agents/god-spec-reviewer.md +70 -0
- package/agents/god-spike-runner.md +119 -0
- package/agents/god-stack-selector.md +93 -0
- package/agents/god-standards-check.md +132 -0
- package/agents/god-storyteller.md +116 -0
- package/agents/god-updater.md +174 -0
- package/bin/install.js +514 -0
- package/extensions/data-pack/README.md +33 -0
- package/extensions/data-pack/agents/god-dashboard-builder.md +66 -0
- package/extensions/data-pack/agents/god-etl-engineer.md +64 -0
- package/extensions/data-pack/agents/god-ml-feature-engineer.md +66 -0
- package/extensions/data-pack/manifest.yaml +39 -0
- package/extensions/data-pack/package.json +42 -0
- package/extensions/data-pack/skills/god-dashboard.md +28 -0
- package/extensions/data-pack/skills/god-etl.md +28 -0
- package/extensions/data-pack/skills/god-ml-feature.md +28 -0
- package/extensions/data-pack/workflows/dashboard-arc.yaml +13 -0
- package/extensions/data-pack/workflows/etl-arc.yaml +13 -0
- package/extensions/data-pack/workflows/ml-feature-arc.yaml +13 -0
- package/extensions/launch-pack/README.md +36 -0
- package/extensions/launch-pack/agents/god-indie-hackers-strategist.md +128 -0
- package/extensions/launch-pack/agents/god-oss-release-strategist.md +125 -0
- package/extensions/launch-pack/agents/god-product-hunt-strategist.md +118 -0
- package/extensions/launch-pack/agents/god-show-hn-strategist.md +113 -0
- package/extensions/launch-pack/manifest.yaml +45 -0
- package/extensions/launch-pack/package.json +41 -0
- package/extensions/launch-pack/skills/god-indie-hackers.md +39 -0
- package/extensions/launch-pack/skills/god-oss-release.md +43 -0
- package/extensions/launch-pack/skills/god-product-hunt.md +41 -0
- package/extensions/launch-pack/skills/god-show-hn.md +40 -0
- package/extensions/launch-pack/workflows/indie-hackers.yaml +13 -0
- package/extensions/launch-pack/workflows/oss-release.yaml +13 -0
- package/extensions/launch-pack/workflows/product-hunt.yaml +13 -0
- package/extensions/launch-pack/workflows/show-hn.yaml +13 -0
- package/extensions/security-pack/README.md +48 -0
- package/extensions/security-pack/agents/god-hipaa-auditor.md +117 -0
- package/extensions/security-pack/agents/god-pci-auditor.md +100 -0
- package/extensions/security-pack/agents/god-soc2-auditor.md +107 -0
- package/extensions/security-pack/manifest.yaml +39 -0
- package/extensions/security-pack/package.json +42 -0
- package/extensions/security-pack/skills/god-hipaa-audit.md +41 -0
- package/extensions/security-pack/skills/god-pci-audit.md +40 -0
- package/extensions/security-pack/skills/god-soc2-audit.md +42 -0
- package/extensions/security-pack/workflows/hipaa-arc.yaml +15 -0
- package/extensions/security-pack/workflows/pci-arc.yaml +15 -0
- package/extensions/security-pack/workflows/soc2-arc.yaml +15 -0
- package/hooks/pre-tool-use.sh +40 -0
- package/hooks/session-start.sh +74 -0
- package/lib/README.md +28 -0
- package/lib/agent-browser-driver.js +215 -0
- package/lib/agent-cache.js +194 -0
- package/lib/agent-validator.js +275 -0
- package/lib/artifact-diff.js +168 -0
- package/lib/artifact-linter.js +142 -0
- package/lib/awesome-design.js +312 -0
- package/lib/browser-bridge.js +209 -0
- package/lib/budget.js +215 -0
- package/lib/checkpoint.js +390 -0
- package/lib/code-scanner.js +262 -0
- package/lib/context-budget.js +170 -0
- package/lib/context-writer.js +348 -0
- package/lib/cost-tracker.js +325 -0
- package/lib/cross-artifact-impact.js +162 -0
- package/lib/cross-repo-linkage.js +150 -0
- package/lib/design-detector.js +167 -0
- package/lib/design-spec.js +348 -0
- package/lib/drift-detector.js +212 -0
- package/lib/event-reader.js +174 -0
- package/lib/events.js +183 -0
- package/lib/extensions.js +257 -0
- package/lib/have-nots-validator.js +647 -0
- package/lib/impact.js +314 -0
- package/lib/impeccable-bridge.js +139 -0
- package/lib/intent.js +177 -0
- package/lib/linkage.js +232 -0
- package/lib/meta-linter.js +263 -0
- package/lib/multi-repo-detector.js +182 -0
- package/lib/otel-exporter.js +308 -0
- package/lib/recipes.js +186 -0
- package/lib/reverse-sync.js +332 -0
- package/lib/review-required.js +224 -0
- package/lib/router.js +278 -0
- package/lib/runtime-audit.js +455 -0
- package/lib/runtime-test.js +309 -0
- package/lib/skillui-bridge.js +216 -0
- package/lib/state-lock.js +201 -0
- package/lib/state.js +142 -0
- package/lib/story-validator.js +301 -0
- package/lib/suite-state.js +220 -0
- package/lib/workflow-parser.js +109 -0
- package/lib/workflow-runner.js +221 -0
- package/package.json +63 -0
- package/references/HAVE-NOTS.md +573 -0
- package/references/building/BUILD-ANTIPATTERNS.md +102 -0
- package/references/building/BUILD-VERTICAL-SLICES.md +75 -0
- package/references/building/BUILD-WAVES.md +61 -0
- package/references/building/README.md +17 -0
- package/references/design/COLOR.md +122 -0
- package/references/design/DESIGN-ANATOMY.md +121 -0
- package/references/design/DESIGN-ANTIPATTERNS.md +108 -0
- package/references/design/INTERACTION.md +148 -0
- package/references/design/MOTION.md +120 -0
- package/references/design/RESPONSIVE.md +157 -0
- package/references/design/SPATIAL.md +109 -0
- package/references/design/TYPOGRAPHY.md +121 -0
- package/references/design/UX-WRITING.md +135 -0
- package/references/orchestration/MODE-DETECTION.md +74 -0
- package/references/orchestration/README.md +18 -0
- package/references/orchestration/SCALE-DETECTION.md +81 -0
- package/references/planning/ARCH-ANATOMY.md +143 -0
- package/references/planning/ARCH-ANTIPATTERNS.md +52 -0
- package/references/planning/PRD-ANATOMY.md +117 -0
- package/references/planning/PRD-ANTIPATTERNS.md +138 -0
- package/references/planning/README.md +16 -0
- package/references/planning/ROADMAP-ANATOMY.md +43 -0
- package/references/planning/ROADMAP-ANTIPATTERNS.md +94 -0
- package/references/planning/STACK-ANATOMY.md +60 -0
- package/references/planning/STACK-ANTIPATTERNS.md +95 -0
- package/references/shared/GLOSSARY.md +80 -0
- package/references/shared/ORCHESTRATORS.md +76 -0
- package/references/shared/README.md +14 -0
- package/references/shipping/DEPLOY-ANTIPATTERNS.md +64 -0
- package/references/shipping/DEPLOY-PATTERNS.md +110 -0
- package/references/shipping/HARDEN-ANTIPATTERNS.md +66 -0
- package/references/shipping/HARDEN-OWASP-WORKSHEETS.md +89 -0
- package/references/shipping/LAUNCH-ANTIPATTERNS.md +68 -0
- package/references/shipping/OBSERVE-ANTIPATTERNS.md +62 -0
- package/references/shipping/OBSERVE-SLO-EXAMPLES.md +107 -0
- package/references/shipping/README.md +18 -0
- package/routing/god-add-backlog.yaml +24 -0
- package/routing/god-add-tests.yaml +27 -0
- package/routing/god-add-todo.yaml +24 -0
- package/routing/god-agent-audit.yaml +24 -0
- package/routing/god-arch.yaml +46 -0
- package/routing/god-archaeology.yaml +28 -0
- package/routing/god-audit.yaml +32 -0
- package/routing/god-budget.yaml +24 -0
- package/routing/god-build-agent.yaml +24 -0
- package/routing/god-build.yaml +46 -0
- package/routing/god-cache-clear.yaml +24 -0
- package/routing/god-check-todos.yaml +24 -0
- package/routing/god-context-scan.yaml +24 -0
- package/routing/god-context.yaml +44 -0
- package/routing/god-cost.yaml +24 -0
- package/routing/god-debug.yaml +28 -0
- package/routing/god-deploy.yaml +34 -0
- package/routing/god-design-impact.yaml +25 -0
- package/routing/god-design.yaml +67 -0
- package/routing/god-discuss.yaml +27 -0
- package/routing/god-docs.yaml +33 -0
- package/routing/god-doctor.yaml +27 -0
- package/routing/god-explore.yaml +27 -0
- package/routing/god-extension-add.yaml +24 -0
- package/routing/god-extension-info.yaml +24 -0
- package/routing/god-extension-list.yaml +24 -0
- package/routing/god-extension-remove.yaml +24 -0
- package/routing/god-extract-learnings.yaml +24 -0
- package/routing/god-fast.yaml +27 -0
- package/routing/god-feature.yaml +34 -0
- package/routing/god-graph.yaml +24 -0
- package/routing/god-harden.yaml +41 -0
- package/routing/god-help.yaml +27 -0
- package/routing/god-hotfix.yaml +34 -0
- package/routing/god-hygiene.yaml +28 -0
- package/routing/god-init.yaml +37 -0
- package/routing/god-intel.yaml +24 -0
- package/routing/god-launch.yaml +41 -0
- package/routing/god-lifecycle.yaml +27 -0
- package/routing/god-link.yaml +24 -0
- package/routing/god-lint.yaml +24 -0
- package/routing/god-list-assumptions.yaml +27 -0
- package/routing/god-locate.yaml +24 -0
- package/routing/god-logs.yaml +24 -0
- package/routing/god-map-codebase.yaml +24 -0
- package/routing/god-metrics.yaml +24 -0
- package/routing/god-mode.yaml +31 -0
- package/routing/god-next.yaml +27 -0
- package/routing/god-note.yaml +24 -0
- package/routing/god-observe.yaml +34 -0
- package/routing/god-org-context.yaml +28 -0
- package/routing/god-party.yaml +24 -0
- package/routing/god-pause-work.yaml +27 -0
- package/routing/god-plant-seed.yaml +24 -0
- package/routing/god-postmortem.yaml +34 -0
- package/routing/god-pr-branch.yaml +25 -0
- package/routing/god-prd.yaml +49 -0
- package/routing/god-quick.yaml +28 -0
- package/routing/god-reconcile.yaml +48 -0
- package/routing/god-reconstruct.yaml +36 -0
- package/routing/god-redo.yaml +27 -0
- package/routing/god-refactor.yaml +36 -0
- package/routing/god-repair.yaml +27 -0
- package/routing/god-repo.yaml +35 -0
- package/routing/god-restore.yaml +27 -0
- package/routing/god-resume-work.yaml +27 -0
- package/routing/god-review-changes.yaml +25 -0
- package/routing/god-review.yaml +28 -0
- package/routing/god-roadmap-check.yaml +39 -0
- package/routing/god-roadmap-update.yaml +37 -0
- package/routing/god-roadmap.yaml +42 -0
- package/routing/god-rollback.yaml +27 -0
- package/routing/god-scan.yaml +24 -0
- package/routing/god-set-profile.yaml +24 -0
- package/routing/god-settings.yaml +24 -0
- package/routing/god-skip.yaml +27 -0
- package/routing/god-smite.yaml +29 -0
- package/routing/god-spike.yaml +35 -0
- package/routing/god-sprint.yaml +25 -0
- package/routing/god-stack.yaml +41 -0
- package/routing/god-standards.yaml +24 -0
- package/routing/god-status.yaml +27 -0
- package/routing/god-stories.yaml +24 -0
- package/routing/god-story-build.yaml +25 -0
- package/routing/god-story-close.yaml +25 -0
- package/routing/god-story-verify.yaml +25 -0
- package/routing/god-story.yaml +24 -0
- package/routing/god-suite-init.yaml +24 -0
- package/routing/god-suite-patch.yaml +25 -0
- package/routing/god-suite-release.yaml +25 -0
- package/routing/god-suite-status.yaml +25 -0
- package/routing/god-suite-sync.yaml +25 -0
- package/routing/god-sync.yaml +33 -0
- package/routing/god-tech-debt.yaml +32 -0
- package/routing/god-test-extension.yaml +24 -0
- package/routing/god-test-runtime.yaml +25 -0
- package/routing/god-thread.yaml +24 -0
- package/routing/god-trace.yaml +24 -0
- package/routing/god-undo.yaml +27 -0
- package/routing/god-update-deps.yaml +39 -0
- package/routing/god-upgrade.yaml +33 -0
- package/routing/god-version.yaml +24 -0
- package/routing/god-workstream.yaml +24 -0
- package/routing/god.yaml +24 -0
- package/routing/recipes/add-feature-defer-current-milestone.yaml +21 -0
- package/routing/recipes/add-feature-future-conditional.yaml +21 -0
- package/routing/recipes/add-feature-mid-arc-pause.yaml +33 -0
- package/routing/recipes/add-feature-next-milestone.yaml +23 -0
- package/routing/recipes/add-feature-parallel.yaml +29 -0
- package/routing/recipes/add-feature-prd-update.yaml +21 -0
- package/routing/recipes/add-feature-small.yaml +24 -0
- package/routing/recipes/add-feature-tiny.yaml +24 -0
- package/routing/recipes/bluefield-org-aware.yaml +27 -0
- package/routing/recipes/broken-install.yaml +22 -0
- package/routing/recipes/brownfield-onboarding.yaml +32 -0
- package/routing/recipes/bug-no-urgency.yaml +21 -0
- package/routing/recipes/capture-idea.yaml +22 -0
- package/routing/recipes/capture-todo.yaml +21 -0
- package/routing/recipes/clean-pr.yaml +21 -0
- package/routing/recipes/code-cleanup.yaml +23 -0
- package/routing/recipes/docs-drift.yaml +21 -0
- package/routing/recipes/existing-codebase-onboarding.yaml +32 -0
- package/routing/recipes/extract-learnings.yaml +22 -0
- package/routing/recipes/greenfield-fast.yaml +25 -0
- package/routing/recipes/greenfield-manual.yaml +32 -0
- package/routing/recipes/greenfield-with-ideation.yaml +29 -0
- package/routing/recipes/incident-postmortem.yaml +24 -0
- package/routing/recipes/major-framework-upgrade.yaml +23 -0
- package/routing/recipes/monthly-deps.yaml +22 -0
- package/routing/recipes/multi-repo-suite.yaml +56 -0
- package/routing/recipes/parallel-engineers.yaml +26 -0
- package/routing/recipes/pause-handoff.yaml +21 -0
- package/routing/recipes/production-broken.yaml +26 -0
- package/routing/recipes/rerun-tier.yaml +21 -0
- package/routing/recipes/returning-after-break.yaml +31 -0
- package/routing/recipes/state-drift.yaml +21 -0
- package/routing/recipes/undo-last.yaml +21 -0
- package/routing/recipes/weekly-health-check.yaml +24 -0
- package/routing/recipes/whats-next.yaml +22 -0
- package/routing/recipes/where-am-i.yaml +21 -0
- package/schema/events.v1.json +63 -0
- package/schema/extension-manifest.v1.json +84 -0
- package/schema/intent.v1.yaml.json +116 -0
- package/schema/recipe.v1.json +120 -0
- package/schema/routing.v1.json +163 -0
- package/schema/state.v1.json +146 -0
- package/schema/workflow.v1.json +96 -0
- package/skills/god-add-backlog.md +40 -0
- package/skills/god-add-tests.md +53 -0
- package/skills/god-add-todo.md +32 -0
- package/skills/god-agent-audit.md +87 -0
- package/skills/god-arch.md +81 -0
- package/skills/god-archaeology.md +48 -0
- package/skills/god-audit.md +65 -0
- package/skills/god-budget.md +103 -0
- package/skills/god-build-agent.md +91 -0
- package/skills/god-build.md +90 -0
- package/skills/god-cache-clear.md +75 -0
- package/skills/god-check-todos.md +42 -0
- package/skills/god-context-scan.md +125 -0
- package/skills/god-context.md +147 -0
- package/skills/god-cost.md +118 -0
- package/skills/god-debug.md +30 -0
- package/skills/god-deploy.md +76 -0
- package/skills/god-design-impact.md +86 -0
- package/skills/god-design.md +275 -0
- package/skills/god-discuss.md +46 -0
- package/skills/god-docs.md +81 -0
- package/skills/god-doctor.md +94 -0
- package/skills/god-explore.md +50 -0
- package/skills/god-export-otel.md +87 -0
- package/skills/god-extension-add.md +79 -0
- package/skills/god-extension-info.md +75 -0
- package/skills/god-extension-list.md +55 -0
- package/skills/god-extension-remove.md +66 -0
- package/skills/god-extract-learnings.md +60 -0
- package/skills/god-fast.md +47 -0
- package/skills/god-feature.md +114 -0
- package/skills/god-graph.md +56 -0
- package/skills/god-harden.md +106 -0
- package/skills/god-help.md +66 -0
- package/skills/god-hotfix.md +139 -0
- package/skills/god-hygiene.md +104 -0
- package/skills/god-init.md +161 -0
- package/skills/god-intel.md +36 -0
- package/skills/god-launch.md +86 -0
- package/skills/god-lifecycle.md +119 -0
- package/skills/god-link.md +90 -0
- package/skills/god-lint.md +128 -0
- package/skills/god-list-assumptions.md +56 -0
- package/skills/god-locate.md +97 -0
- package/skills/god-logs.md +57 -0
- package/skills/god-map-codebase.md +45 -0
- package/skills/god-metrics.md +51 -0
- package/skills/god-mode.md +159 -0
- package/skills/god-next.md +257 -0
- package/skills/god-note.md +39 -0
- package/skills/god-observe.md +76 -0
- package/skills/god-org-context.md +81 -0
- package/skills/god-party.md +87 -0
- package/skills/god-pause-work.md +64 -0
- package/skills/god-plant-seed.md +59 -0
- package/skills/god-postmortem.md +103 -0
- package/skills/god-pr-branch.md +50 -0
- package/skills/god-prd.md +90 -0
- package/skills/god-quick.md +50 -0
- package/skills/god-reconcile.md +90 -0
- package/skills/god-reconstruct.md +72 -0
- package/skills/god-redo.md +73 -0
- package/skills/god-refactor.md +137 -0
- package/skills/god-repair.md +82 -0
- package/skills/god-repo.md +49 -0
- package/skills/god-restore.md +91 -0
- package/skills/god-resume-work.md +42 -0
- package/skills/god-review-changes.md +93 -0
- package/skills/god-review.md +52 -0
- package/skills/god-roadmap-check.md +66 -0
- package/skills/god-roadmap-update.md +64 -0
- package/skills/god-roadmap.md +77 -0
- package/skills/god-rollback.md +88 -0
- package/skills/god-scan.md +106 -0
- package/skills/god-set-profile.md +58 -0
- package/skills/god-settings.md +44 -0
- package/skills/god-skip.md +78 -0
- package/skills/god-smite.md +86 -0
- package/skills/god-spike.md +120 -0
- package/skills/god-sprint.md +77 -0
- package/skills/god-stack.md +74 -0
- package/skills/god-standards.md +62 -0
- package/skills/god-status.md +99 -0
- package/skills/god-stories.md +60 -0
- package/skills/god-story-build.md +76 -0
- package/skills/god-story-close.md +82 -0
- package/skills/god-story-verify.md +71 -0
- package/skills/god-story.md +55 -0
- package/skills/god-suite-init.md +75 -0
- package/skills/god-suite-patch.md +64 -0
- package/skills/god-suite-release.md +58 -0
- package/skills/god-suite-status.md +63 -0
- package/skills/god-suite-sync.md +49 -0
- package/skills/god-sync.md +102 -0
- package/skills/god-tech-debt.md +56 -0
- package/skills/god-test-extension.md +87 -0
- package/skills/god-test-runtime.md +144 -0
- package/skills/god-thread.md +39 -0
- package/skills/god-trace.md +50 -0
- package/skills/god-undo.md +68 -0
- package/skills/god-update-deps.md +134 -0
- package/skills/god-upgrade.md +139 -0
- package/skills/god-version.md +37 -0
- package/skills/god-workstream.md +61 -0
- package/skills/god.md +207 -0
- package/templates/ARCH.md +99 -0
- package/templates/DEPS-AUDIT.md +66 -0
- package/templates/DESIGN.md +71 -0
- package/templates/DOCS-UPDATE-LOG.md +64 -0
- package/templates/HARDEN-FINDINGS.md +69 -0
- package/templates/MIGRATION.md +86 -0
- package/templates/POSTMORTEM.md +88 -0
- package/templates/PRD.md +80 -0
- package/templates/PROGRESS.md +49 -0
- package/templates/ROADMAP.md +47 -0
- package/templates/SPIKE.md +72 -0
- package/templates/STACK-DECISION.md +61 -0
- package/workflows/audit-only.yaml +22 -0
- package/workflows/bluefield-arc.yaml +87 -0
- package/workflows/brownfield-arc.yaml +44 -0
- package/workflows/deps-audit.yaml +56 -0
- package/workflows/docs-arc.yaml +22 -0
- package/workflows/feature-arc.yaml +59 -0
- package/workflows/full-arc.yaml +84 -0
- package/workflows/hotfix-arc.yaml +59 -0
- package/workflows/hygiene.yaml +43 -0
- package/workflows/migration-arc.yaml +73 -0
- package/workflows/postmortem.yaml +31 -0
- package/workflows/refactor-arc.yaml +59 -0
- package/workflows/spike.yaml +23 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Lock - cooperative advisory locking for state.json mutations.
|
|
3
|
+
*
|
|
4
|
+
* Contract (from ARCHITECTURE.md "Concurrency contract"):
|
|
5
|
+
* - Reads are lock-free.
|
|
6
|
+
* - Writes acquire a lock before mutating, release on completion.
|
|
7
|
+
* - Lock has a `scope` (e.g. `tier-1.arch`, `linkage`, `all`).
|
|
8
|
+
* Concurrent writers with non-overlapping scopes may run.
|
|
9
|
+
* - Stale locks (past `expires`) are reclaimable by any actor;
|
|
10
|
+
* reclaim emits state.repair with previous holder recorded.
|
|
11
|
+
*
|
|
12
|
+
* The lock lives in state.json under the `lock` key. Acquiring writes
|
|
13
|
+
* { holder, acquired, expires, scope }. Releasing sets lock to null.
|
|
14
|
+
*
|
|
15
|
+
* Scope conflict rules:
|
|
16
|
+
* - 'all' conflicts with everything.
|
|
17
|
+
* - 'tier-N.substep' conflicts with another 'tier-N.substep' on same
|
|
18
|
+
* substep AND with 'all'. Does NOT conflict with 'tier-M.substep'
|
|
19
|
+
* where M != N.
|
|
20
|
+
* - 'linkage' conflicts with 'linkage' and 'all'.
|
|
21
|
+
* - Any custom scope: exact-match conflict only (plus 'all').
|
|
22
|
+
*
|
|
23
|
+
* Public API:
|
|
24
|
+
* acquire(projectRoot, opts) -> { acquired: true, lock } | { acquired: false, reason, holder }
|
|
25
|
+
* release(projectRoot, holder) -> { released: bool }
|
|
26
|
+
* peek(projectRoot) -> lock | null
|
|
27
|
+
* isStale(lock, nowMs?) -> bool
|
|
28
|
+
* reclaim(projectRoot, holder) -> { reclaimed: bool, previousHolder? }
|
|
29
|
+
* scopesConflict(a, b) -> bool
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
|
|
35
|
+
const state = require('./state');
|
|
36
|
+
|
|
37
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
38
|
+
|
|
39
|
+
function nowIso(offsetMs) {
|
|
40
|
+
const d = offsetMs ? new Date(Date.now() + offsetMs) : new Date();
|
|
41
|
+
return d.toISOString();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function peek(projectRoot) {
|
|
45
|
+
const s = state.read(projectRoot);
|
|
46
|
+
return s ? (s.lock || null) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Return true if the lock is past its `expires` time (or null/malformed).
|
|
51
|
+
*/
|
|
52
|
+
function isStale(lock, nowMs) {
|
|
53
|
+
if (!lock || !lock.expires) return true;
|
|
54
|
+
const t = (nowMs != null ? nowMs : Date.now());
|
|
55
|
+
const exp = Date.parse(lock.expires);
|
|
56
|
+
return Number.isFinite(exp) ? t > exp : true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Determine whether two scopes conflict.
|
|
61
|
+
*/
|
|
62
|
+
function scopesConflict(a, b) {
|
|
63
|
+
if (!a || !b) return false;
|
|
64
|
+
if (a === 'all' || b === 'all') return true;
|
|
65
|
+
return a === b;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Try to acquire a lock.
|
|
70
|
+
*
|
|
71
|
+
* opts: { holder (required), scope (default 'all'), ttlMs (default 5min), force }
|
|
72
|
+
*
|
|
73
|
+
* Returns { acquired: true, lock } on success.
|
|
74
|
+
* Returns { acquired: false, reason, holder, scope } on conflict.
|
|
75
|
+
*
|
|
76
|
+
* Reentrant: same holder can re-acquire. If the existing lock is stale,
|
|
77
|
+
* it is silently reclaimed.
|
|
78
|
+
*/
|
|
79
|
+
function acquire(projectRoot, opts = {}) {
|
|
80
|
+
if (!opts.holder) throw new Error('holder is required');
|
|
81
|
+
const scope = opts.scope || 'all';
|
|
82
|
+
const ttlMs = opts.ttlMs || DEFAULT_TTL_MS;
|
|
83
|
+
|
|
84
|
+
const s = state.read(projectRoot);
|
|
85
|
+
if (!s) throw new Error('state.json not initialized');
|
|
86
|
+
|
|
87
|
+
const existing = s.lock || null;
|
|
88
|
+
|
|
89
|
+
if (existing && !isStale(existing) && !opts.force) {
|
|
90
|
+
// Reentrant: same holder + compatible scope -> refresh expiration
|
|
91
|
+
if (existing.holder === opts.holder &&
|
|
92
|
+
(existing.scope === scope || existing.scope === 'all' || scope === 'all')) {
|
|
93
|
+
existing.expires = nowIso(ttlMs);
|
|
94
|
+
s.lock = existing;
|
|
95
|
+
state.write(projectRoot, s);
|
|
96
|
+
return { acquired: true, lock: existing, reentrant: true };
|
|
97
|
+
}
|
|
98
|
+
if (scopesConflict(existing.scope || 'all', scope)) {
|
|
99
|
+
return {
|
|
100
|
+
acquired: false,
|
|
101
|
+
reason: 'held',
|
|
102
|
+
holder: existing.holder,
|
|
103
|
+
scope: existing.scope,
|
|
104
|
+
expires: existing.expires
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Compatible scopes: layered lock. We support a simple single-lock
|
|
108
|
+
// slot for now, so reject and tell the caller. Multi-lock support
|
|
109
|
+
// can be added when there's a real second writer.
|
|
110
|
+
return {
|
|
111
|
+
acquired: false,
|
|
112
|
+
reason: 'scope-coexistence-not-supported',
|
|
113
|
+
holder: existing.holder,
|
|
114
|
+
scope: existing.scope
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// existing is null OR stale OR force -> take it
|
|
119
|
+
const lock = {
|
|
120
|
+
holder: opts.holder,
|
|
121
|
+
acquired: nowIso(),
|
|
122
|
+
expires: nowIso(ttlMs),
|
|
123
|
+
scope
|
|
124
|
+
};
|
|
125
|
+
const reclaimedFrom = existing && isStale(existing) ? existing.holder : null;
|
|
126
|
+
s.lock = lock;
|
|
127
|
+
state.write(projectRoot, s);
|
|
128
|
+
return {
|
|
129
|
+
acquired: true,
|
|
130
|
+
lock,
|
|
131
|
+
reclaimed: reclaimedFrom != null,
|
|
132
|
+
reclaimedFrom
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Release a held lock. Only the current holder can release.
|
|
138
|
+
*
|
|
139
|
+
* Returns { released: true } on success, { released: false, reason } otherwise.
|
|
140
|
+
*/
|
|
141
|
+
function release(projectRoot, holder) {
|
|
142
|
+
if (!holder) throw new Error('holder is required');
|
|
143
|
+
const s = state.read(projectRoot);
|
|
144
|
+
if (!s) return { released: false, reason: 'no-state' };
|
|
145
|
+
const lock = s.lock;
|
|
146
|
+
if (!lock) return { released: false, reason: 'no-lock' };
|
|
147
|
+
if (lock.holder !== holder) {
|
|
148
|
+
return { released: false, reason: 'wrong-holder', heldBy: lock.holder };
|
|
149
|
+
}
|
|
150
|
+
s.lock = null;
|
|
151
|
+
state.write(projectRoot, s);
|
|
152
|
+
return { released: true, releasedAt: nowIso() };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Force-reclaim a stale lock. Used by /god-repair.
|
|
157
|
+
* Returns { reclaimed, previousHolder? }.
|
|
158
|
+
*/
|
|
159
|
+
function reclaim(projectRoot, holder) {
|
|
160
|
+
const s = state.read(projectRoot);
|
|
161
|
+
if (!s) return { reclaimed: false, reason: 'no-state' };
|
|
162
|
+
const lock = s.lock;
|
|
163
|
+
if (!lock) return { reclaimed: false, reason: 'no-lock' };
|
|
164
|
+
if (!isStale(lock)) {
|
|
165
|
+
return { reclaimed: false, reason: 'lock-not-stale', expires: lock.expires };
|
|
166
|
+
}
|
|
167
|
+
const prev = lock.holder;
|
|
168
|
+
s.lock = null;
|
|
169
|
+
state.write(projectRoot, s);
|
|
170
|
+
return { reclaimed: true, previousHolder: prev };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Convenience wrapper: run an async function under a lock.
|
|
175
|
+
* Acquires, runs, releases.
|
|
176
|
+
*/
|
|
177
|
+
async function withLock(projectRoot, opts, fn) {
|
|
178
|
+
const r = acquire(projectRoot, opts);
|
|
179
|
+
if (!r.acquired) {
|
|
180
|
+
const err = new Error(`lock unavailable: held by ${r.holder} (scope=${r.scope})`);
|
|
181
|
+
err.code = 'LOCK_UNAVAILABLE';
|
|
182
|
+
err.detail = r;
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
return await fn(r.lock);
|
|
187
|
+
} finally {
|
|
188
|
+
release(projectRoot, opts.holder);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
acquire,
|
|
194
|
+
release,
|
|
195
|
+
peek,
|
|
196
|
+
isStale,
|
|
197
|
+
reclaim,
|
|
198
|
+
withLock,
|
|
199
|
+
scopesConflict,
|
|
200
|
+
DEFAULT_TTL_MS
|
|
201
|
+
};
|
package/lib/state.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Manager
|
|
3
|
+
*
|
|
4
|
+
* Read/write .godpowers/state.json with schema validation.
|
|
5
|
+
* Source of truth for tier statuses and artifact hashes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
const STATE_VERSION = '1.0.0';
|
|
13
|
+
|
|
14
|
+
function statePath(projectRoot) {
|
|
15
|
+
return path.join(projectRoot, '.godpowers', 'state.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read state.json from a project. Returns null if not initialized.
|
|
20
|
+
*/
|
|
21
|
+
function read(projectRoot) {
|
|
22
|
+
const file = statePath(projectRoot);
|
|
23
|
+
if (!fs.existsSync(file)) return null;
|
|
24
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Write state.json to a project. Validates basic structure.
|
|
29
|
+
*/
|
|
30
|
+
function write(projectRoot, state) {
|
|
31
|
+
if (!state || typeof state !== 'object') {
|
|
32
|
+
throw new Error('state must be an object');
|
|
33
|
+
}
|
|
34
|
+
if (!state.version) state.version = STATE_VERSION;
|
|
35
|
+
if (!state.$schema) state.$schema = 'https://godpowers.dev/schema/state.v1.json';
|
|
36
|
+
if (!state.project || !state.project.name) {
|
|
37
|
+
throw new Error('state.project.name is required');
|
|
38
|
+
}
|
|
39
|
+
if (!state.tiers) state.tiers = {};
|
|
40
|
+
|
|
41
|
+
const file = statePath(projectRoot);
|
|
42
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
43
|
+
fs.writeFileSync(file, JSON.stringify(state, null, 2) + '\n');
|
|
44
|
+
return state;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize a new state.json for a project.
|
|
49
|
+
*/
|
|
50
|
+
function init(projectRoot, projectName, opts = {}) {
|
|
51
|
+
const state = {
|
|
52
|
+
$schema: 'https://godpowers.dev/schema/state.v1.json',
|
|
53
|
+
version: STATE_VERSION,
|
|
54
|
+
project: {
|
|
55
|
+
name: projectName,
|
|
56
|
+
started: new Date().toISOString()
|
|
57
|
+
},
|
|
58
|
+
'active-workstream': 'main',
|
|
59
|
+
tiers: {
|
|
60
|
+
'tier-0': { orchestration: { status: 'in-flight', updated: new Date().toISOString() } },
|
|
61
|
+
'tier-1': {
|
|
62
|
+
prd: { status: 'pending' },
|
|
63
|
+
arch: { status: 'pending' },
|
|
64
|
+
roadmap: { status: 'pending' },
|
|
65
|
+
stack: { status: 'pending' },
|
|
66
|
+
design: { status: 'pending' }, // conditional: not-required for backend-only
|
|
67
|
+
product: { status: 'pending' } // populated only when impeccable installed
|
|
68
|
+
},
|
|
69
|
+
'tier-2': {
|
|
70
|
+
repo: { status: 'pending' },
|
|
71
|
+
build: { status: 'pending' }
|
|
72
|
+
},
|
|
73
|
+
'tier-3': {
|
|
74
|
+
deploy: { status: 'pending' },
|
|
75
|
+
observe: { status: 'pending' },
|
|
76
|
+
launch: { status: 'pending' },
|
|
77
|
+
harden: { status: 'pending' }
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
'lifecycle-phase': 'in-arc',
|
|
81
|
+
linkage: {
|
|
82
|
+
'coverage-pct': 0,
|
|
83
|
+
'orphan-count': 0,
|
|
84
|
+
'drift-count': 0,
|
|
85
|
+
'review-required-items': 0
|
|
86
|
+
},
|
|
87
|
+
'yolo-decisions': [],
|
|
88
|
+
...opts
|
|
89
|
+
};
|
|
90
|
+
return write(projectRoot, state);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Update a single sub-step's status.
|
|
95
|
+
*/
|
|
96
|
+
function updateSubStep(projectRoot, tierKey, subStepKey, updates) {
|
|
97
|
+
const state = read(projectRoot);
|
|
98
|
+
if (!state) throw new Error('state.json not found');
|
|
99
|
+
if (!state.tiers[tierKey]) throw new Error(`Tier not found: ${tierKey}`);
|
|
100
|
+
state.tiers[tierKey][subStepKey] = {
|
|
101
|
+
...(state.tiers[tierKey][subStepKey] || {}),
|
|
102
|
+
...updates,
|
|
103
|
+
updated: new Date().toISOString()
|
|
104
|
+
};
|
|
105
|
+
write(projectRoot, state);
|
|
106
|
+
return state.tiers[tierKey][subStepKey];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Hash a file. Used for artifact-hash tracking.
|
|
111
|
+
*/
|
|
112
|
+
function hashFile(filePath) {
|
|
113
|
+
if (!fs.existsSync(filePath)) return null;
|
|
114
|
+
const content = fs.readFileSync(filePath);
|
|
115
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
116
|
+
return `sha256:${hash}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Detect drift: for each sub-step with an artifact, rehash and compare.
|
|
121
|
+
* Returns a list of drift entries.
|
|
122
|
+
*/
|
|
123
|
+
function detectDrift(projectRoot) {
|
|
124
|
+
const state = read(projectRoot);
|
|
125
|
+
if (!state) return [];
|
|
126
|
+
const drift = [];
|
|
127
|
+
for (const [tierKey, tier] of Object.entries(state.tiers)) {
|
|
128
|
+
for (const [subStepKey, subStep] of Object.entries(tier)) {
|
|
129
|
+
if (!subStep.artifact || !subStep['artifact-hash']) continue;
|
|
130
|
+
const fullPath = path.join(projectRoot, '.godpowers', subStep.artifact);
|
|
131
|
+
const currentHash = hashFile(fullPath);
|
|
132
|
+
if (currentHash === null) {
|
|
133
|
+
drift.push({ tierKey, subStepKey, kind: 'missing', recorded: subStep['artifact-hash'] });
|
|
134
|
+
} else if (currentHash !== subStep['artifact-hash']) {
|
|
135
|
+
drift.push({ tierKey, subStepKey, kind: 'modified', recorded: subStep['artifact-hash'], current: currentHash });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return drift;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { read, write, init, updateSubStep, hashFile, detectDrift, statePath };
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story Validator
|
|
3
|
+
*
|
|
4
|
+
* Parses and validates STORY.md files (story-file workflow).
|
|
5
|
+
* Backward-compatible: doesn't replace /god-feature; complements it
|
|
6
|
+
* with finer-grained slices.
|
|
7
|
+
*
|
|
8
|
+
* STORY.md schema:
|
|
9
|
+
* ---
|
|
10
|
+
* id: STORY-{slug}-NNN
|
|
11
|
+
* title: "Short noun phrase"
|
|
12
|
+
* status: pending | in-progress | blocked | done
|
|
13
|
+
* owner: name
|
|
14
|
+
* deps: [STORY-other-001]
|
|
15
|
+
* created: ISO date
|
|
16
|
+
* ---
|
|
17
|
+
*
|
|
18
|
+
* ## User Story
|
|
19
|
+
* As a [persona], I want [capability] so that [outcome].
|
|
20
|
+
*
|
|
21
|
+
* ## Acceptance Criteria
|
|
22
|
+
* - [DECISION] User can do X. Acceptance: clicks Y, sees Z.
|
|
23
|
+
*
|
|
24
|
+
* ## Slice Plan
|
|
25
|
+
* 1. Step 1
|
|
26
|
+
* 2. Step 2
|
|
27
|
+
*
|
|
28
|
+
* ## Notes
|
|
29
|
+
*
|
|
30
|
+
* Public API:
|
|
31
|
+
* parseStory(filePath) -> { id, title, status, owner, deps, body, sections, errors }
|
|
32
|
+
* validateStory(story) -> findings
|
|
33
|
+
* findStoryFiles(projectRoot) -> [paths]
|
|
34
|
+
* listStories(projectRoot) -> [{ id, status, ... }]
|
|
35
|
+
* listByStatus(projectRoot, status) -> [...]
|
|
36
|
+
* isValidId(id) -> bool
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const fs = require('fs');
|
|
40
|
+
const path = require('path');
|
|
41
|
+
|
|
42
|
+
const VALID_STATUSES = ['pending', 'in-progress', 'blocked', 'done'];
|
|
43
|
+
const ID_PATTERN = /^STORY-[\w-]+-\d+$/;
|
|
44
|
+
const USER_STORY_PATTERN = /as\s+a[n]?\s+.+,\s+i\s+want\s+.+\s+so\s+that\s+/i;
|
|
45
|
+
|
|
46
|
+
function isValidId(id) {
|
|
47
|
+
return ID_PATTERN.test(id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function storiesDir(projectRoot) {
|
|
51
|
+
return path.join(projectRoot, '.godpowers', 'stories');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find all STORY-*.md files under .godpowers/stories/.
|
|
56
|
+
*/
|
|
57
|
+
function findStoryFiles(projectRoot) {
|
|
58
|
+
const dir = storiesDir(projectRoot);
|
|
59
|
+
const found = [];
|
|
60
|
+
if (!fs.existsSync(dir)) return found;
|
|
61
|
+
|
|
62
|
+
function walk(d) {
|
|
63
|
+
let entries;
|
|
64
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); }
|
|
65
|
+
catch (e) { return; }
|
|
66
|
+
for (const e of entries) {
|
|
67
|
+
const full = path.join(d, e.name);
|
|
68
|
+
if (e.isDirectory()) walk(full);
|
|
69
|
+
else if (e.isFile() && /^STORY-/.test(e.name) && e.name.endsWith('.md')) {
|
|
70
|
+
found.push(full);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
walk(dir);
|
|
75
|
+
return found;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse a STORY.md. Returns object with frontmatter, parsed sections, errors.
|
|
80
|
+
*/
|
|
81
|
+
function parseStory(filePath) {
|
|
82
|
+
const errors = [];
|
|
83
|
+
if (!fs.existsSync(filePath)) {
|
|
84
|
+
return { errors: ['file-not-found'] };
|
|
85
|
+
}
|
|
86
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
87
|
+
if (!raw.startsWith('---')) {
|
|
88
|
+
return { errors: ['missing-frontmatter'], raw };
|
|
89
|
+
}
|
|
90
|
+
const fmEnd = raw.indexOf('\n---', 3);
|
|
91
|
+
if (fmEnd === -1) {
|
|
92
|
+
return { errors: ['unclosed-frontmatter'], raw };
|
|
93
|
+
}
|
|
94
|
+
const fmText = raw.slice(3, fmEnd).trim();
|
|
95
|
+
const body = raw.slice(fmEnd + 4).trim();
|
|
96
|
+
|
|
97
|
+
const fm = {};
|
|
98
|
+
for (const line of fmText.split('\n')) {
|
|
99
|
+
const m = line.match(/^([\w-]+):\s*(.*)$/);
|
|
100
|
+
if (m) {
|
|
101
|
+
let value = m[2].trim();
|
|
102
|
+
// Parse arrays: [a, b] or ["a", "b"]
|
|
103
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
104
|
+
value = value.slice(1, -1).split(',').map(s => {
|
|
105
|
+
let t = s.trim();
|
|
106
|
+
// Strip surrounding quotes if present
|
|
107
|
+
if ((t.startsWith('"') && t.endsWith('"')) ||
|
|
108
|
+
(t.startsWith("'") && t.endsWith("'"))) {
|
|
109
|
+
t = t.slice(1, -1);
|
|
110
|
+
}
|
|
111
|
+
return t;
|
|
112
|
+
}).filter(Boolean);
|
|
113
|
+
} else if (value.startsWith('"') && value.endsWith('"')) {
|
|
114
|
+
value = value.slice(1, -1);
|
|
115
|
+
}
|
|
116
|
+
fm[m[1]] = value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Parse sections
|
|
121
|
+
const sections = {};
|
|
122
|
+
let currentHeading = null;
|
|
123
|
+
let currentLines = [];
|
|
124
|
+
for (const line of body.split('\n')) {
|
|
125
|
+
const m = line.match(/^##\s+(.+?)\s*$/);
|
|
126
|
+
if (m) {
|
|
127
|
+
if (currentHeading) {
|
|
128
|
+
sections[currentHeading] = currentLines.join('\n').trim();
|
|
129
|
+
}
|
|
130
|
+
currentHeading = m[1].trim();
|
|
131
|
+
currentLines = [];
|
|
132
|
+
} else if (currentHeading) {
|
|
133
|
+
currentLines.push(line);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (currentHeading) {
|
|
137
|
+
sections[currentHeading] = currentLines.join('\n').trim();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
path: filePath,
|
|
142
|
+
id: fm.id,
|
|
143
|
+
title: fm.title,
|
|
144
|
+
status: fm.status,
|
|
145
|
+
owner: fm.owner,
|
|
146
|
+
deps: Array.isArray(fm.deps) ? fm.deps : (fm.deps ? [fm.deps] : []),
|
|
147
|
+
created: fm.created,
|
|
148
|
+
frontmatter: fm,
|
|
149
|
+
body,
|
|
150
|
+
sections,
|
|
151
|
+
errors
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Validate a parsed story. Returns findings.
|
|
157
|
+
*/
|
|
158
|
+
function validateStory(story) {
|
|
159
|
+
const findings = [];
|
|
160
|
+
|
|
161
|
+
if (story.errors && story.errors.length > 0) {
|
|
162
|
+
for (const e of story.errors) {
|
|
163
|
+
findings.push({ severity: 'error', kind: e, message: e });
|
|
164
|
+
}
|
|
165
|
+
return findings;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!story.id) {
|
|
169
|
+
findings.push({ severity: 'error', kind: 'missing-id', message: 'STORY missing `id` in frontmatter' });
|
|
170
|
+
} else if (!isValidId(story.id)) {
|
|
171
|
+
findings.push({
|
|
172
|
+
severity: 'error',
|
|
173
|
+
kind: 'invalid-id-format',
|
|
174
|
+
message: `STORY id "${story.id}" does not match STORY-{slug}-NNN pattern`
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (!story.title) {
|
|
178
|
+
findings.push({ severity: 'error', kind: 'missing-title', message: 'STORY missing `title`' });
|
|
179
|
+
}
|
|
180
|
+
if (!story.status) {
|
|
181
|
+
findings.push({ severity: 'error', kind: 'missing-status', message: 'STORY missing `status`' });
|
|
182
|
+
} else if (!VALID_STATUSES.includes(story.status)) {
|
|
183
|
+
findings.push({
|
|
184
|
+
severity: 'error',
|
|
185
|
+
kind: 'invalid-status',
|
|
186
|
+
message: `Status "${story.status}" not one of ${VALID_STATUSES.join(', ')}`
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (!story.owner) {
|
|
190
|
+
findings.push({ severity: 'warning', kind: 'missing-owner', message: 'STORY missing `owner` field' });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// User Story section format
|
|
194
|
+
if (!story.sections || !story.sections['User Story']) {
|
|
195
|
+
findings.push({
|
|
196
|
+
severity: 'warning',
|
|
197
|
+
kind: 'missing-user-story',
|
|
198
|
+
message: 'STORY missing `## User Story` section'
|
|
199
|
+
});
|
|
200
|
+
} else {
|
|
201
|
+
const us = story.sections['User Story'];
|
|
202
|
+
if (!USER_STORY_PATTERN.test(us)) {
|
|
203
|
+
findings.push({
|
|
204
|
+
severity: 'warning',
|
|
205
|
+
kind: 'user-story-format',
|
|
206
|
+
message: 'User Story does not match "As a X, I want Y so that Z" format'
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Acceptance Criteria
|
|
212
|
+
if (!story.sections || !story.sections['Acceptance Criteria']) {
|
|
213
|
+
findings.push({
|
|
214
|
+
severity: 'warning',
|
|
215
|
+
kind: 'missing-acceptance',
|
|
216
|
+
message: 'STORY missing `## Acceptance Criteria` section'
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return findings;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* List all stories with summary fields.
|
|
225
|
+
*/
|
|
226
|
+
function listStories(projectRoot) {
|
|
227
|
+
const files = findStoryFiles(projectRoot);
|
|
228
|
+
return files.map(f => {
|
|
229
|
+
const story = parseStory(f);
|
|
230
|
+
return {
|
|
231
|
+
id: story.id,
|
|
232
|
+
title: story.title,
|
|
233
|
+
status: story.status,
|
|
234
|
+
owner: story.owner,
|
|
235
|
+
deps: story.deps || [],
|
|
236
|
+
path: f
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function listByStatus(projectRoot, status) {
|
|
242
|
+
return listStories(projectRoot).filter(s => s.status === status);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Detect dep cycles.
|
|
247
|
+
*/
|
|
248
|
+
function detectDepCycles(projectRoot) {
|
|
249
|
+
const stories = listStories(projectRoot);
|
|
250
|
+
const byId = {};
|
|
251
|
+
for (const s of stories) byId[s.id] = s;
|
|
252
|
+
const visited = new Set();
|
|
253
|
+
const stack = new Set();
|
|
254
|
+
const cycles = [];
|
|
255
|
+
function dfs(id, path) {
|
|
256
|
+
if (stack.has(id)) {
|
|
257
|
+
cycles.push([...path, id]);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (visited.has(id)) return;
|
|
261
|
+
visited.add(id);
|
|
262
|
+
stack.add(id);
|
|
263
|
+
const s = byId[id];
|
|
264
|
+
if (s && s.deps) {
|
|
265
|
+
for (const dep of s.deps) {
|
|
266
|
+
if (byId[dep]) dfs(dep, [...path, id]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
stack.delete(id);
|
|
270
|
+
}
|
|
271
|
+
for (const s of stories) dfs(s.id, []);
|
|
272
|
+
return cycles;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Update story status (writes back to file).
|
|
277
|
+
*/
|
|
278
|
+
function setStatus(filePath, newStatus) {
|
|
279
|
+
if (!VALID_STATUSES.includes(newStatus)) {
|
|
280
|
+
throw new Error(`invalid status: ${newStatus}`);
|
|
281
|
+
}
|
|
282
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
283
|
+
const updated = raw.replace(/^status:\s*\w+/m, `status: ${newStatus}`);
|
|
284
|
+
fs.writeFileSync(filePath, updated);
|
|
285
|
+
return { path: filePath, newStatus };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = {
|
|
289
|
+
parseStory,
|
|
290
|
+
validateStory,
|
|
291
|
+
findStoryFiles,
|
|
292
|
+
listStories,
|
|
293
|
+
listByStatus,
|
|
294
|
+
detectDepCycles,
|
|
295
|
+
setStatus,
|
|
296
|
+
isValidId,
|
|
297
|
+
storiesDir,
|
|
298
|
+
VALID_STATUSES,
|
|
299
|
+
ID_PATTERN,
|
|
300
|
+
USER_STORY_PATTERN
|
|
301
|
+
};
|