@tyroneross/build-loop 0.30.3
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/plugins/marketplace.json +10 -0
- package/.claude-plugin/marketplace.json +38 -0
- package/.claude-plugin/plugin.json +30 -0
- package/.codex-plugin/plugin.json +41 -0
- package/.cursor/rules/build-loop-surface.mdc +22 -0
- package/AGENTS.md +498 -0
- package/LICENSE +202 -0
- package/README.md +403 -0
- package/agents/alignment-checker.md +124 -0
- package/agents/api-assessor.md +136 -0
- package/agents/architecture-scout.md +185 -0
- package/agents/assessment-orchestrator.md +163 -0
- package/agents/build-orchestrator.md +198 -0
- package/agents/database-assessor.md +133 -0
- package/agents/design-contract-specialist.md +239 -0
- package/agents/fact-checker.md +69 -0
- package/agents/fix-critique.md +161 -0
- package/agents/frontend-assessor.md +136 -0
- package/agents/implementer.md +185 -0
- package/agents/independent-auditor.md +143 -0
- package/agents/mock-scanner.md +78 -0
- package/agents/optimize-runner.md +112 -0
- package/agents/overfitting-reviewer.md +96 -0
- package/agents/performance-assessor.md +159 -0
- package/agents/plan-critic.md +85 -0
- package/agents/promotion-reviewer.md +121 -0
- package/agents/recurring-pattern-detector.md +248 -0
- package/agents/retrospective-synthesizer.md +109 -0
- package/agents/root-cause-investigator.md +285 -0
- package/agents/scope-auditor.md +121 -0
- package/agents/security-reviewer.md +151 -0
- package/agents/self-improvement-architect.md +125 -0
- package/agents/synthesis-critic.md +99 -0
- package/agents/transcript-pattern-miner.md +165 -0
- package/agents/ui-validator.md +193 -0
- package/codex-skills/build-loop/SKILL.md +20 -0
- package/codex-skills/debug-loop/SKILL.md +18 -0
- package/codex-skills/handoff/SKILL.md +22 -0
- package/codex-skills/knowledge/SKILL.md +21 -0
- package/codex-skills/optimize/SKILL.md +18 -0
- package/codex-skills/research/SKILL.md +18 -0
- package/commands/agent-rally-point.md +301 -0
- package/commands/assess.md +41 -0
- package/commands/debug.md +20 -0
- package/commands/debugger-detail.md +34 -0
- package/commands/debugger-scan.md +15 -0
- package/commands/debugger-status.md +20 -0
- package/commands/debugger.md +32 -0
- package/commands/handoff.md +113 -0
- package/commands/knowledge-review.md +16 -0
- package/commands/optimize.md +31 -0
- package/commands/plan-verify.md +39 -0
- package/commands/promote-experiment.md +129 -0
- package/commands/research.md +22 -0
- package/commands/run.md +12 -0
- package/commands/self-review.md +51 -0
- package/commands/start-prd.md +57 -0
- package/commands/test.md +34 -0
- package/dist/src/assessment-orchestrator.d.ts +78 -0
- package/dist/src/assessment-orchestrator.d.ts.map +1 -0
- package/dist/src/assessment-orchestrator.js +304 -0
- package/dist/src/assessment-orchestrator.js.map +1 -0
- package/dist/src/audit-miner.d.ts +21 -0
- package/dist/src/audit-miner.d.ts.map +1 -0
- package/dist/src/audit-miner.js +421 -0
- package/dist/src/audit-miner.js.map +1 -0
- package/dist/src/batch-operations.d.ts +30 -0
- package/dist/src/batch-operations.d.ts.map +1 -0
- package/dist/src/batch-operations.js +339 -0
- package/dist/src/batch-operations.js.map +1 -0
- package/dist/src/config.d.ts +83 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +176 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context-engine.d.ts +45 -0
- package/dist/src/context-engine.d.ts.map +1 -0
- package/dist/src/context-engine.js +135 -0
- package/dist/src/context-engine.js.map +1 -0
- package/dist/src/debug-wrapper.d.ts +58 -0
- package/dist/src/debug-wrapper.d.ts.map +1 -0
- package/dist/src/debug-wrapper.js +285 -0
- package/dist/src/debug-wrapper.js.map +1 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +149 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interactive-verifier.d.ts +26 -0
- package/dist/src/interactive-verifier.d.ts.map +1 -0
- package/dist/src/interactive-verifier.js +435 -0
- package/dist/src/interactive-verifier.js.map +1 -0
- package/dist/src/lessons.d.ts +59 -0
- package/dist/src/lessons.d.ts.map +1 -0
- package/dist/src/lessons.js +172 -0
- package/dist/src/lessons.js.map +1 -0
- package/dist/src/log-reader.d.ts +39 -0
- package/dist/src/log-reader.d.ts.map +1 -0
- package/dist/src/log-reader.js +291 -0
- package/dist/src/log-reader.js.map +1 -0
- package/dist/src/logger.d.ts +59 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +328 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/parallel-retrieval.d.ts +52 -0
- package/dist/src/parallel-retrieval.d.ts.map +1 -0
- package/dist/src/parallel-retrieval.js +309 -0
- package/dist/src/parallel-retrieval.js.map +1 -0
- package/dist/src/pattern-extractor.d.ts +30 -0
- package/dist/src/pattern-extractor.d.ts.map +1 -0
- package/dist/src/pattern-extractor.js +386 -0
- package/dist/src/pattern-extractor.js.map +1 -0
- package/dist/src/result-aggregator.d.ts +44 -0
- package/dist/src/result-aggregator.d.ts.map +1 -0
- package/dist/src/result-aggregator.js +315 -0
- package/dist/src/result-aggregator.js.map +1 -0
- package/dist/src/retrieval.d.ts +139 -0
- package/dist/src/retrieval.d.ts.map +1 -0
- package/dist/src/retrieval.js +724 -0
- package/dist/src/retrieval.js.map +1 -0
- package/dist/src/setup/auto-setup.d.ts +15 -0
- package/dist/src/setup/auto-setup.d.ts.map +1 -0
- package/dist/src/setup/auto-setup.js +157 -0
- package/dist/src/setup/auto-setup.js.map +1 -0
- package/dist/src/setup/configure-hooks.d.ts +8 -0
- package/dist/src/setup/configure-hooks.d.ts.map +1 -0
- package/dist/src/setup/configure-hooks.js +16 -0
- package/dist/src/setup/configure-hooks.js.map +1 -0
- package/dist/src/setup/create-slash-commands.d.ts +8 -0
- package/dist/src/setup/create-slash-commands.d.ts.map +1 -0
- package/dist/src/setup/create-slash-commands.js +101 -0
- package/dist/src/setup/create-slash-commands.js.map +1 -0
- package/dist/src/setup/index.d.ts +8 -0
- package/dist/src/setup/index.d.ts.map +1 -0
- package/dist/src/setup/index.js +17 -0
- package/dist/src/setup/index.js.map +1 -0
- package/dist/src/setup/inject-claude-md.d.ts +10 -0
- package/dist/src/setup/inject-claude-md.d.ts.map +1 -0
- package/dist/src/setup/inject-claude-md.js +150 -0
- package/dist/src/setup/inject-claude-md.js.map +1 -0
- package/dist/src/setup/uninstall.d.ts +22 -0
- package/dist/src/setup/uninstall.d.ts.map +1 -0
- package/dist/src/setup/uninstall.js +235 -0
- package/dist/src/setup/uninstall.js.map +1 -0
- package/dist/src/storage.d.ts +192 -0
- package/dist/src/storage.d.ts.map +1 -0
- package/dist/src/storage.js +1037 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/string-similarity.d.ts +8 -0
- package/dist/src/string-similarity.d.ts.map +1 -0
- package/dist/src/string-similarity.js +57 -0
- package/dist/src/string-similarity.js.map +1 -0
- package/dist/src/traces/adapters/browser.d.ts +69 -0
- package/dist/src/traces/adapters/browser.d.ts.map +1 -0
- package/dist/src/traces/adapters/browser.js +280 -0
- package/dist/src/traces/adapters/browser.js.map +1 -0
- package/dist/src/traces/adapters/index.d.ts +23 -0
- package/dist/src/traces/adapters/index.d.ts.map +1 -0
- package/dist/src/traces/adapters/index.js +67 -0
- package/dist/src/traces/adapters/index.js.map +1 -0
- package/dist/src/traces/adapters/langchain.d.ts +42 -0
- package/dist/src/traces/adapters/langchain.d.ts.map +1 -0
- package/dist/src/traces/adapters/langchain.js +151 -0
- package/dist/src/traces/adapters/langchain.js.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts +46 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.js +164 -0
- package/dist/src/traces/adapters/opentelemetry.js.map +1 -0
- package/dist/src/traces/adapters/sentry.d.ts +58 -0
- package/dist/src/traces/adapters/sentry.d.ts.map +1 -0
- package/dist/src/traces/adapters/sentry.js +212 -0
- package/dist/src/traces/adapters/sentry.js.map +1 -0
- package/dist/src/traces/index.d.ts +10 -0
- package/dist/src/traces/index.d.ts.map +1 -0
- package/dist/src/traces/index.js +45 -0
- package/dist/src/traces/index.js.map +1 -0
- package/dist/src/traces/storage.d.ts +78 -0
- package/dist/src/traces/storage.d.ts.map +1 -0
- package/dist/src/traces/storage.js +399 -0
- package/dist/src/traces/storage.js.map +1 -0
- package/dist/src/traces/summarizer.d.ts +83 -0
- package/dist/src/traces/summarizer.d.ts.map +1 -0
- package/dist/src/traces/summarizer.js +247 -0
- package/dist/src/traces/summarizer.js.map +1 -0
- package/dist/src/traces/types.d.ts +365 -0
- package/dist/src/traces/types.d.ts.map +1 -0
- package/dist/src/traces/types.js +14 -0
- package/dist/src/traces/types.js.map +1 -0
- package/dist/src/types.d.ts +495 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +27 -0
- package/dist/src/types.js.map +1 -0
- package/docs/agent-surface-policy.md +45 -0
- package/docs/memory-setup.md +226 -0
- package/hooks/_arch_scan_bg.py +88 -0
- package/hooks/_plugin_drift_check_bg.py +85 -0
- package/hooks/_session_start_lib.sh +42 -0
- package/hooks/git/pre-push +141 -0
- package/hooks/hooks.json +126 -0
- package/hooks/post-commit +30 -0
- package/hooks/pre-commit +29 -0
- package/hooks/pre-edit-apppulse.sh +6 -0
- package/hooks/pre-edit-architecture.sh +106 -0
- package/hooks/pre-edit-rally-point.sh +92 -0
- package/hooks/session-start-apppulse.sh +6 -0
- package/hooks/session-start-architecture.sh +10 -0
- package/hooks/session-start-git-hooks.sh +12 -0
- package/hooks/session-start-plugin-drift.sh +27 -0
- package/hooks/session-start-rally-point.sh +44 -0
- package/hooks/session-start-retrieval.sh +24 -0
- package/hooks/session-start-worktree-gc.sh +105 -0
- package/hooks/test_apppulse_hooks.sh +6 -0
- package/hooks/test_rally_point_hooks.sh +388 -0
- package/hooks/test_sec008_hooks.sh +66 -0
- package/package.json +111 -0
- package/scripts/README.md +301 -0
- package/scripts/_db_url.py +61 -0
- package/scripts/_paths.py +445 -0
- package/scripts/blm.py +162 -0
- package/scripts/blm_api.py +263 -0
- package/scripts/check_cache_sync.py +490 -0
- package/scripts/embed_backend.py +475 -0
- package/scripts/install_memory.py +556 -0
- package/scripts/lessons_index/__init__.py +77 -0
- package/scripts/lessons_index/__main__.py +135 -0
- package/scripts/lessons_index/ingest.py +288 -0
- package/scripts/lessons_index/query.py +285 -0
- package/scripts/lessons_index/schema.py +114 -0
- package/scripts/memory_context/__init__.py +610 -0
- package/scripts/memory_graph/__init__.py +393 -0
- package/scripts/project_resolver.py +172 -0
- package/scripts/prune_codex_plugin_cache.py +23 -0
- package/scripts/prune_plugin_cache.py +359 -0
- package/scripts/semantic_index/__init__.py +482 -0
- package/scripts/semantic_index/_bench_hybrid.py +175 -0
- package/scripts/semantic_index/backfill.py +263 -0
- package/scripts/semantic_index/hybrid.py +174 -0
- package/scripts/semantic_index/test_backfill.py +292 -0
- package/scripts/semantic_index/test_hybrid.py +541 -0
- package/scripts/sync_navgator_lessons.py +666 -0
- package/scripts/sync_plugin_cache.py +388 -0
- package/skills/agent-rally-point/SKILL.md +165 -0
- package/skills/agent-rally-watcher/SKILL.md +83 -0
- package/skills/api-registry-bridge/SKILL.md +59 -0
- package/skills/architecture/dead/SKILL.md +56 -0
- package/skills/architecture/impact/SKILL.md +88 -0
- package/skills/architecture/review/SKILL.md +227 -0
- package/skills/architecture/rules/SKILL.md +72 -0
- package/skills/architecture/scan/SKILL.md +79 -0
- package/skills/architecture/trace/SKILL.md +65 -0
- package/skills/attribution-standard/SKILL.md +122 -0
- package/skills/authentication/SKILL.md +81 -0
- package/skills/authentication/references/better-auth-magic-link.md +135 -0
- package/skills/authentication/references/better-auth-setup.md +224 -0
- package/skills/authentication/references/google-calendar-sync.md +278 -0
- package/skills/authentication/references/google-cloud-console.md +159 -0
- package/skills/authentication/references/google-geocoding-directions.md +126 -0
- package/skills/authentication/references/google-lessons-example-web-app.md +177 -0
- package/skills/authentication/references/google-maps.md +162 -0
- package/skills/authentication/references/google-oauth-setup.md +343 -0
- package/skills/authentication/references/google-places.md +173 -0
- package/skills/authentication/references/lessons-example-web-app-better-auth.md +126 -0
- package/skills/authentication/references/resend-email.md +227 -0
- package/skills/authentication/references/resend-otp-magic-link.md +153 -0
- package/skills/authentication/references/supabase-auth.md +172 -0
- package/skills/auto-decision-capture/SKILL.md +419 -0
- package/skills/build-loop/SKILL.md +516 -0
- package/skills/build-loop/detect-plugins.mjs +82 -0
- package/skills/build-loop/eval-guide.md +65 -0
- package/skills/build-loop/fallbacks.md +549 -0
- package/skills/build-loop/phases/fact-check.md +42 -0
- package/skills/build-loop/phases/ui-validation.md +267 -0
- package/skills/build-loop/references/apple-native-planning.md +439 -0
- package/skills/build-loop/references/capability-routing.md +181 -0
- package/skills/build-loop/references/codex-subagents.md +98 -0
- package/skills/build-loop/references/coordination.md +161 -0
- package/skills/build-loop/references/correction-aware-capture.md +177 -0
- package/skills/build-loop/references/independent-auditor.md +72 -0
- package/skills/build-loop/references/intent-capability-pack.md +170 -0
- package/skills/build-loop/references/intent-exploration-prompts.md +96 -0
- package/skills/build-loop/references/leadership.md +72 -0
- package/skills/build-loop/references/memory.md +284 -0
- package/skills/build-loop/references/modular-systems-pack.md +96 -0
- package/skills/build-loop/references/output-style.md +129 -0
- package/skills/build-loop/references/pay-it-forward-arch.md +98 -0
- package/skills/build-loop/references/phase-1-assess.md +218 -0
- package/skills/build-loop/references/phase-2-plan.md +77 -0
- package/skills/build-loop/references/phase-3-execute.md +47 -0
- package/skills/build-loop/references/phase-4-review.md +310 -0
- package/skills/build-loop/references/phase-5-iterate.md +71 -0
- package/skills/build-loop/references/phase-6-learn.md +56 -0
- package/skills/build-loop/references/recent-design-structures.md +274 -0
- package/skills/build-loop/references/refactor-history/ASSESSMENT.md +85 -0
- package/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +149 -0
- package/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +32 -0
- package/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +48 -0
- package/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +60 -0
- package/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +51 -0
- package/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +71 -0
- package/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +52 -0
- package/skills/build-loop/references/refactor-history/trace-comparison.md +202 -0
- package/skills/build-loop/references/self-recursive-dev.md +77 -0
- package/skills/build-loop/references/self-review.md +232 -0
- package/skills/build-loop/references/ui-io-contract.md +116 -0
- package/skills/build-loop/scanners/audit-design-rules.mjs +476 -0
- package/skills/build-loop/scanners/require-visual-evidence.mjs +239 -0
- package/skills/build-loop/templates/backlog-item.md +35 -0
- package/skills/build-loop/templates/codex-worker-prompt.md +89 -0
- package/skills/build-loop/templates/ui-subagent-prompt.md +179 -0
- package/skills/build-loop/templates/ux-fix-plan.md +40 -0
- package/skills/building-with-deepagents/SKILL.md +195 -0
- package/skills/building-with-deepagents/references/anti-patterns.md +141 -0
- package/skills/building-with-deepagents/references/api-quick-reference.md +184 -0
- package/skills/capabilities/SKILL.md +91 -0
- package/skills/debug-loop/SKILL.md +278 -0
- package/skills/debug-loop/references/convergence-rules.md +145 -0
- package/skills/debug-loop/references/swiftui-macos-clunky-ui-debugger.html +823 -0
- package/skills/debugging/assess/SKILL.md +111 -0
- package/skills/debugging/debug-loop/SKILL.md +231 -0
- package/skills/debugging/memory/SKILL.md +147 -0
- package/skills/debugging/store/SKILL.md +153 -0
- package/skills/debugging-memory/SKILL.md +440 -0
- package/skills/debugging-memory/examples/incident-example.json +67 -0
- package/skills/debugging-memory/references/incident-documentation.md +280 -0
- package/skills/debugging-memory/references/ios-notification-alarm-playbook.md +300 -0
- package/skills/debugging-memory/references/pattern-extraction.md +163 -0
- package/skills/debugging-memory/references/subagent-integration.md +312 -0
- package/skills/defenseclaw-bridge/SKILL.md +156 -0
- package/skills/defenseclaw-bridge/references/dc-config-mapping.md +162 -0
- package/skills/defenseclaw-bridge/references/output-format.md +108 -0
- package/skills/handoff/SKILL.md +94 -0
- package/skills/ibr-bridge/SKILL.md +68 -0
- package/skills/knowledge/SKILL.md +127 -0
- package/skills/knowledge/references/recall-integration.md +83 -0
- package/skills/knowledge/templates/madr-minimal.md +56 -0
- package/skills/knowledge-review/SKILL.md +112 -0
- package/skills/logging-tracer/SKILL.md +202 -0
- package/skills/logging-tracer/references/log-analysis.md +128 -0
- package/skills/logging-tracer/references/stack-templates.md +274 -0
- package/skills/logging-tracer-bridge/SKILL.md +70 -0
- package/skills/mcp-builder/SKILL.md +278 -0
- package/skills/mcp-builder/references/mcp-security.md +106 -0
- package/skills/model-tiering/SKILL.md +216 -0
- package/skills/native-ax-driver/SKILL.md +220 -0
- package/skills/native-ax-driver/scripts/layout_fill.py +222 -0
- package/skills/native-ax-driver/scripts/native_driver.py +495 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Package.swift +20 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Sources/main.swift +542 -0
- package/skills/optimize/SKILL.md +202 -0
- package/skills/optimize/profiles.md +61 -0
- package/skills/plan-verify/SKILL.md +115 -0
- package/skills/plan-verify/test-fixtures/example-app-v20-findings.json +27 -0
- package/skills/plan-verify/test-fixtures/example-app-v20.md +42 -0
- package/skills/plan-verify/test-fixtures/example-app-v22-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/example-app-v22.md +1608 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan.md +55 -0
- package/skills/plugin-builder/SKILL.md +329 -0
- package/skills/plugin-builder/references/authoritative-sources.md +104 -0
- package/skills/plugin-builder/references/build-loop-phase-guidance.md +94 -0
- package/skills/plugin-builder/references/components-guide.md +221 -0
- package/skills/plugin-builder/references/distribution.md +317 -0
- package/skills/plugin-builder/references/dual-host-claude-codex.md +220 -0
- package/skills/plugin-builder/references/hooks-reference.md +210 -0
- package/skills/plugin-builder/references/manifest-schema.md +111 -0
- package/skills/plugin-builder/references/plugin-hygiene-lessons.md +307 -0
- package/skills/plugin-tests/SKILL.md +94 -0
- package/skills/prd-bridge/SKILL.md +135 -0
- package/skills/research/SKILL.md +80 -0
- package/skills/security-methodology/SKILL.md +105 -0
- package/skills/security-methodology/references/cross-source-matrix.md +77 -0
- package/skills/security-methodology/references/mitre-atlas-starter.md +83 -0
- package/skills/security-methodology/references/nist-600-1-mapping.md +119 -0
- package/skills/security-methodology/references/owasp-agentic-top-10.md +157 -0
- package/skills/security-methodology/references/owasp-llm-top-10.md +144 -0
- package/skills/security-methodology/references/owasp-web-top-10.md +65 -0
- package/skills/self-improve/SKILL.md +298 -0
- package/skills/spec-writing/SKILL.md +548 -0
- package/skills/spec-writing/scripts/check_checklist.py +478 -0
- package/skills/sync-skills/SKILL.md +132 -0
- package/skills/telemetry/SKILL.md +66 -0
- package/skills/ui-design/SKILL.md +75 -0
- package/skills/ui-design/references/design-patterns-multi.md +165 -0
- package/skills/ui-design/references/design-preferences-from-owned-apps.md +150 -0
- package/skills/ui-design/references/evidence-capture-policy.md +55 -0
- package/skills/ui-design/references/ui-guidance-sources.md +330 -0
- package/skills/ui-design/references/universal-design-principles.alt.md +182 -0
- package/skills/ui-design/references/universal-design-principles.md +419 -0
- package/templates/memory/MEMORY.md.template +55 -0
- package/templates/memory/constitution.md.template +64 -0
- package/templates/memory/manifest.json +47 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com>
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""
|
|
5
|
+
check_checklist.py — deterministic spec-writing checklist verifier.
|
|
6
|
+
|
|
7
|
+
Reads a plan markdown file, locates the <!-- checklist --> HTML comment block,
|
|
8
|
+
and checks whether each required item is answered (not blank, not
|
|
9
|
+
the literal placeholder text, and not omitted entirely).
|
|
10
|
+
|
|
11
|
+
Exit codes:
|
|
12
|
+
0 — all required items answered (Items 15 and 17 are conditional on UI files in scope)
|
|
13
|
+
1 — one or more items missing or unanswered
|
|
14
|
+
2 — verifier error (file not found, parse failure)
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python3 check_checklist.py --plan docs/plans/my-feature.md [--json] [--quiet]
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Constants
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
ITEMS: list[tuple[str, str]] = [
|
|
33
|
+
("item_1_auth_guard", "Item 1 — Auth guard"),
|
|
34
|
+
("item_2_external_apis", "Item 2 — External APIs"),
|
|
35
|
+
("item_3_rate_limit", "Item 3 — Rate-limit criterion"),
|
|
36
|
+
("item_4_discoverability", "Item 4 — Discoverability"),
|
|
37
|
+
("item_5_server_client_boundary", "Item 5 — Server/client boundary"),
|
|
38
|
+
("item_6_concurrency", "Item 6 — Concurrency"),
|
|
39
|
+
("item_7_observability", "Item 7 — Observability"),
|
|
40
|
+
("item_8_input_validation", "Item 8 — Input validation"),
|
|
41
|
+
("item_9_stable_id_traceability", "Item 9 — Stable ID traceability"),
|
|
42
|
+
("item_10_json_spec_object", "Item 10 — JSON spec object"),
|
|
43
|
+
("item_11_blocking_and_novel_question_gate", "Item 11 — Blocking-and-novel question gate"),
|
|
44
|
+
("item_12_low_reversibility_adrs", "Item 12 — Low-reversibility ADRs"),
|
|
45
|
+
("item_13_analytical_lens", "Item 13 — Analytical lens"),
|
|
46
|
+
("item_14_handoff_document", "Item 14 — Handoff document"),
|
|
47
|
+
("item_15_synthesis_dimensions", "Item 15 — Synthesis dimensions"),
|
|
48
|
+
("item_16_risk_reason", "Item 16 — Risk reason"),
|
|
49
|
+
("item_17_ui_io_contract", "Item 17 — UI input/output contract"),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# Values that count as "not answered" — case-insensitive, stripped
|
|
53
|
+
PLACEHOLDER_PATTERNS: list[re.Pattern] = [
|
|
54
|
+
re.compile(r"^<answer>$"),
|
|
55
|
+
re.compile(r"^\.\.\.$"),
|
|
56
|
+
re.compile(r"^$"),
|
|
57
|
+
re.compile(r"^tbd$", re.IGNORECASE),
|
|
58
|
+
re.compile(r"^todo$", re.IGNORECASE),
|
|
59
|
+
re.compile(r"^none$", re.IGNORECASE),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
CHECKLIST_RE = re.compile(
|
|
63
|
+
r"<!--\s*checklist\s*(.*?)\s*-->",
|
|
64
|
+
re.DOTALL | re.IGNORECASE,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
ITEM_LINE_RE = re.compile(
|
|
68
|
+
r"^(Item\s*\d+\s*[—\-–]\s*[^:]+)\s*:\s*(.*)$",
|
|
69
|
+
re.IGNORECASE,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Parsing
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def extract_checklist_block(text: str) -> str | None:
|
|
78
|
+
"""Return the inner text of the first <!-- checklist ... --> block, or None."""
|
|
79
|
+
m = CHECKLIST_RE.search(text)
|
|
80
|
+
return m.group(1).strip() if m else None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parse_checklist_block(block: str) -> dict[str, str]:
|
|
84
|
+
"""
|
|
85
|
+
Parse `Item N — Label: answer` lines from the checklist block.
|
|
86
|
+
Returns a dict mapping the item label prefix (lowercased, spaces→underscores)
|
|
87
|
+
to the answer string.
|
|
88
|
+
"""
|
|
89
|
+
parsed: dict[str, str] = {}
|
|
90
|
+
for line in block.splitlines():
|
|
91
|
+
line = line.strip()
|
|
92
|
+
if not line:
|
|
93
|
+
continue
|
|
94
|
+
m = ITEM_LINE_RE.match(line)
|
|
95
|
+
if m:
|
|
96
|
+
key = _normalize_label(m.group(1))
|
|
97
|
+
parsed[key] = m.group(2).strip()
|
|
98
|
+
return parsed
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _normalize_label(label: str) -> str:
|
|
102
|
+
"""'Item 1 — Auth guard' → 'item_1_auth_guard'"""
|
|
103
|
+
s = label.lower()
|
|
104
|
+
s = re.sub(r"[—\-–]+", " ", s)
|
|
105
|
+
s = re.sub(r"[^a-z0-9\s]", "", s)
|
|
106
|
+
s = re.sub(r"\s+", "_", s.strip())
|
|
107
|
+
return s
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_answered(value: str) -> bool:
|
|
111
|
+
v = value.strip()
|
|
112
|
+
return not any(p.match(v) for p in PLACEHOLDER_PATTERNS)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Structural validators for items 9-14
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
# ID patterns: U-NN, F-NN, D-NN, S-NN, T-NN, A-NN
|
|
120
|
+
_ID_RE = re.compile(r"\b[UFDSTA]-\d+\b")
|
|
121
|
+
_P0_LINE_RE = re.compile(r"\[P0\]", re.IGNORECASE)
|
|
122
|
+
_T_ID_RE = re.compile(r"\bT-\d+\b")
|
|
123
|
+
_A_ID_RE = re.compile(r"\bA-\d+\b")
|
|
124
|
+
_JSON_SPEC_SECTION_RE = re.compile(r"^##\s+Spec Object.*\(JSON\)", re.IGNORECASE | re.MULTILINE)
|
|
125
|
+
_OPEN_Q_SECTION_RE = re.compile(r"^##\s+Open Questions", re.IGNORECASE | re.MULTILINE)
|
|
126
|
+
_BLOCKING_TEST_RE = re.compile(r"blocking-test:\s*T-\d+", re.IGNORECASE)
|
|
127
|
+
_ADR_HEADING_RE = re.compile(r"^##\s+ADR-\d+", re.IGNORECASE | re.MULTILINE)
|
|
128
|
+
_LOW_REV_RE = re.compile(
|
|
129
|
+
r"low-reversib|db choice|auth provider|api contract|public schema",
|
|
130
|
+
re.IGNORECASE,
|
|
131
|
+
)
|
|
132
|
+
_LENS_LINE_RE = re.compile(r"analytical lens\s*:", re.IGNORECASE)
|
|
133
|
+
|
|
134
|
+
# Items 15 and 17 — UI-only checks.
|
|
135
|
+
_UI_FILE_RE = re.compile(
|
|
136
|
+
r"""
|
|
137
|
+
(
|
|
138
|
+
\b[\w/.\-]+\.(?:tsx|jsx|vue|svelte)\b
|
|
139
|
+
|(?:^|[\s`(/])components/[\w/.\-]+
|
|
140
|
+
|(?:^|[\s`(/])(?:app|pages)/(?!(?:api|_app|_document)\b)[\w/.\-]*(?:page|layout)\.(?:ts|tsx|js|jsx)\b
|
|
141
|
+
|(?:^|[\s`(/])Views/[\w/.\-]+\.swift\b
|
|
142
|
+
|\b[\w/.\-]*(?:View|Screen|Page)\.swift\b
|
|
143
|
+
)
|
|
144
|
+
""",
|
|
145
|
+
re.IGNORECASE | re.VERBOSE,
|
|
146
|
+
)
|
|
147
|
+
_SYNTHESIS_BLOCK_RE = re.compile(
|
|
148
|
+
r"synthesis_dimensions\s*:\s*\n((?:[ \t]+[\w_]+\s*:.*\n?)+)", re.IGNORECASE)
|
|
149
|
+
_REQUIRED_SYNTHESIS_KEYS = ("placement", "cta_tier", "copy_tone",
|
|
150
|
+
"visual_weight", "empty_state")
|
|
151
|
+
_UI_IO_SECTION_RE = re.compile(
|
|
152
|
+
r"^##\s+UI\s+Input/Output\s+Contract\b", re.IGNORECASE | re.MULTILINE)
|
|
153
|
+
_UI_IO_REQUIRED_TERMS = (
|
|
154
|
+
"surface", "input", "output", "taxonomy", "operation", "component",
|
|
155
|
+
"state", "modality", "validation", "traceability",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def check_item_15_synthesis_dimensions(plan_text: str) -> tuple[str, str | None]:
|
|
160
|
+
"""OK if no UI in scope; else require synthesis_dimensions block w/ all keys."""
|
|
161
|
+
if not _UI_FILE_RE.search(plan_text):
|
|
162
|
+
return ("OK", None)
|
|
163
|
+
m = _SYNTHESIS_BLOCK_RE.search(plan_text)
|
|
164
|
+
if not m:
|
|
165
|
+
return ("FAIL", "UI files detected but no `synthesis_dimensions:` block. "
|
|
166
|
+
"Required keys: " + ", ".join(_REQUIRED_SYNTHESIS_KEYS) + ".")
|
|
167
|
+
body = m.group(1)
|
|
168
|
+
missing = [k for k in _REQUIRED_SYNTHESIS_KEYS
|
|
169
|
+
if not re.search(rf"^\s+{re.escape(k)}\s*:", body, re.MULTILINE)]
|
|
170
|
+
if missing:
|
|
171
|
+
return ("FAIL", "synthesis_dimensions missing key(s): " + ", ".join(missing))
|
|
172
|
+
return ("OK", None)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def check_item_17_ui_io_contract(plan_text: str) -> tuple[str, str | None]:
|
|
176
|
+
"""OK if no UI in scope; else require a UI Input/Output Contract section."""
|
|
177
|
+
if not _UI_FILE_RE.search(plan_text):
|
|
178
|
+
return ("OK", None)
|
|
179
|
+
|
|
180
|
+
m = _UI_IO_SECTION_RE.search(plan_text)
|
|
181
|
+
if not m:
|
|
182
|
+
return ("FAIL", "UI files detected but no `## UI Input/Output Contract` section found.")
|
|
183
|
+
|
|
184
|
+
section_start = m.end()
|
|
185
|
+
next_heading = re.search(r"^##\s+", plan_text[section_start:], re.MULTILINE)
|
|
186
|
+
section = (
|
|
187
|
+
plan_text[section_start: section_start + next_heading.start()]
|
|
188
|
+
if next_heading else plan_text[section_start:]
|
|
189
|
+
)
|
|
190
|
+
lower = section.lower()
|
|
191
|
+
missing = [term for term in _UI_IO_REQUIRED_TERMS if term not in lower]
|
|
192
|
+
if missing:
|
|
193
|
+
return ("FAIL", "UI Input/Output Contract missing term(s): " + ", ".join(missing))
|
|
194
|
+
return ("OK", None)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _structural_findings(text: str, plan_path: Path) -> list[dict]:
|
|
198
|
+
"""
|
|
199
|
+
Run structural checks for higher-order checklist items against the full plan text.
|
|
200
|
+
Returns a list of finding dicts (same shape as checklist findings).
|
|
201
|
+
Each finding has item_id, label, status ('ok'|'warn'|'fail'), and message.
|
|
202
|
+
These supplement — they do not replace — the checklist block checks.
|
|
203
|
+
"""
|
|
204
|
+
findings = []
|
|
205
|
+
|
|
206
|
+
# Item 9: P0 lines must have at least one T- reference
|
|
207
|
+
p0_lines = [ln for ln in text.splitlines() if _P0_LINE_RE.search(ln)]
|
|
208
|
+
if p0_lines:
|
|
209
|
+
p0_without_test = [ln for ln in p0_lines if not _T_ID_RE.search(ln)]
|
|
210
|
+
if p0_without_test:
|
|
211
|
+
findings.append({
|
|
212
|
+
"item_id": "item_9_stable_id_traceability",
|
|
213
|
+
"label": "Item 9 — Stable ID traceability",
|
|
214
|
+
"status": "warn",
|
|
215
|
+
"message": (
|
|
216
|
+
f"{len(p0_without_test)} [P0] line(s) found without a linked T-NN test ID. "
|
|
217
|
+
"Each P0 must trace to at least one acceptance test."
|
|
218
|
+
),
|
|
219
|
+
})
|
|
220
|
+
else:
|
|
221
|
+
# Also verify at least one full trace chain appears (U-, F-, T-)
|
|
222
|
+
has_chain = (
|
|
223
|
+
bool(_ID_RE.search(text))
|
|
224
|
+
and bool(re.search(r"\bU-\d+\b", text))
|
|
225
|
+
and bool(re.search(r"\bF-\d+\b", text))
|
|
226
|
+
)
|
|
227
|
+
if not has_chain:
|
|
228
|
+
findings.append({
|
|
229
|
+
"item_id": "item_9_stable_id_traceability",
|
|
230
|
+
"label": "Item 9 — Stable ID traceability",
|
|
231
|
+
"status": "warn",
|
|
232
|
+
"message": (
|
|
233
|
+
"P0 lines have T- links but no full U-NN → F-NN → T-NN chain found. "
|
|
234
|
+
"Add stable IDs threading from user need through feature to test."
|
|
235
|
+
),
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
# Item 10: ## Spec Object (JSON) section must exist
|
|
239
|
+
if not _JSON_SPEC_SECTION_RE.search(text):
|
|
240
|
+
findings.append({
|
|
241
|
+
"item_id": "item_10_json_spec_object",
|
|
242
|
+
"label": "Item 10 — JSON spec object",
|
|
243
|
+
"status": "warn",
|
|
244
|
+
"message": (
|
|
245
|
+
"No '## Spec Object (JSON)' section found. "
|
|
246
|
+
"Emit a structured JSON block before rendering markdown."
|
|
247
|
+
),
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
# Item 11: Open Questions entries must carry blocking-test: annotation
|
|
251
|
+
oq_match = _OPEN_Q_SECTION_RE.search(text)
|
|
252
|
+
if oq_match:
|
|
253
|
+
# Extract the Open Questions section (up to the next ## heading)
|
|
254
|
+
oq_start = oq_match.end()
|
|
255
|
+
next_heading = re.search(r"^##\s+", text[oq_start:], re.MULTILINE)
|
|
256
|
+
oq_body = text[oq_start: oq_start + next_heading.start()] if next_heading else text[oq_start:]
|
|
257
|
+
# Count question lines (non-empty, non-heading lines)
|
|
258
|
+
q_lines = [ln.strip() for ln in oq_body.splitlines()
|
|
259
|
+
if ln.strip() and not ln.strip().startswith("#")]
|
|
260
|
+
missing_annotation = [ln for ln in q_lines if not _BLOCKING_TEST_RE.search(ln)]
|
|
261
|
+
if missing_annotation:
|
|
262
|
+
findings.append({
|
|
263
|
+
"item_id": "item_11_blocking_and_novel_question_gate",
|
|
264
|
+
"label": "Item 11 — Blocking-and-novel question gate",
|
|
265
|
+
"status": "warn",
|
|
266
|
+
"message": (
|
|
267
|
+
f"{len(missing_annotation)} Open Question line(s) lack a 'blocking-test: T-NN' "
|
|
268
|
+
"annotation. Convert non-blocking questions to [ASSUMED: ...] entries."
|
|
269
|
+
),
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
# Item 12: Low-reversibility decision mentions must have ADR link
|
|
273
|
+
low_rev_lines = [ln for ln in text.splitlines() if _LOW_REV_RE.search(ln)]
|
|
274
|
+
adr_headings_exist = bool(_ADR_HEADING_RE.search(text))
|
|
275
|
+
if low_rev_lines and not adr_headings_exist:
|
|
276
|
+
findings.append({
|
|
277
|
+
"item_id": "item_12_low_reversibility_adrs",
|
|
278
|
+
"label": "Item 12 — Low-reversibility ADRs",
|
|
279
|
+
"status": "warn",
|
|
280
|
+
"message": (
|
|
281
|
+
f"{len(low_rev_lines)} low-reversibility decision mention(s) found but no "
|
|
282
|
+
"'## ADR-NN:' section detected. Each such decision needs an ADR record."
|
|
283
|
+
),
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
# Item 13: Analytical lens line must appear in the plan body (outside the checklist block)
|
|
287
|
+
body_text = CHECKLIST_RE.sub("", text) # strip the <!-- checklist --> block
|
|
288
|
+
if not _LENS_LINE_RE.search(body_text):
|
|
289
|
+
findings.append({
|
|
290
|
+
"item_id": "item_13_analytical_lens",
|
|
291
|
+
"label": "Item 13 — Analytical lens",
|
|
292
|
+
"status": "warn",
|
|
293
|
+
"message": (
|
|
294
|
+
"No 'Analytical lens:' line found in the plan body. "
|
|
295
|
+
"Name the lens (JTBD, QFD, TRIZ, Pugh/AHP, DSM, etc.) in Locked Decisions."
|
|
296
|
+
),
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
# Item 14: Sibling .handoff.md must exist
|
|
300
|
+
slug = plan_path.stem # e.g. "my-feature" from "my-feature.md"
|
|
301
|
+
handoff_path = plan_path.parent / f"{slug}.handoff.md"
|
|
302
|
+
if not handoff_path.exists():
|
|
303
|
+
findings.append({
|
|
304
|
+
"item_id": "item_14_handoff_document",
|
|
305
|
+
"label": "Item 14 — Handoff document",
|
|
306
|
+
"status": "warn",
|
|
307
|
+
"message": (
|
|
308
|
+
f"Sibling handoff file '{handoff_path.name}' not found. "
|
|
309
|
+
"Generate docs/plans/<slug>.handoff.md alongside the plan."
|
|
310
|
+
),
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
# Item 15: Synthesis dimensions block required when UI is in scope
|
|
314
|
+
status_15, msg_15 = check_item_15_synthesis_dimensions(text)
|
|
315
|
+
if status_15 == "FAIL":
|
|
316
|
+
findings.append({
|
|
317
|
+
"item_id": "item_15_synthesis_dimensions",
|
|
318
|
+
"label": "Item 15 — Synthesis dimensions",
|
|
319
|
+
"status": "fail",
|
|
320
|
+
"message": msg_15,
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
# Item 17: UI Input/Output Contract required when UI is in scope
|
|
324
|
+
status_17, msg_17 = check_item_17_ui_io_contract(text)
|
|
325
|
+
if status_17 == "FAIL":
|
|
326
|
+
findings.append({
|
|
327
|
+
"item_id": "item_17_ui_io_contract",
|
|
328
|
+
"label": "Item 17 — UI input/output contract",
|
|
329
|
+
"status": "fail",
|
|
330
|
+
"message": msg_17,
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
return findings
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
# Main verification
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
def verify(plan_path: Path) -> dict:
|
|
341
|
+
"""
|
|
342
|
+
Returns a result dict:
|
|
343
|
+
{
|
|
344
|
+
"plan": str,
|
|
345
|
+
"checklist_found": bool,
|
|
346
|
+
"findings": [{"item_id": str, "label": str, "answer": str|None, "status": "ok"|"missing"|"unanswered"}],
|
|
347
|
+
"structural_warnings": [{"item_id": str, "label": str, "status": "warn", "message": str}],
|
|
348
|
+
"missing_count": int,
|
|
349
|
+
"structural_warning_count": int,
|
|
350
|
+
"exit_code": int # 0=clean, 1=failures, 2=error
|
|
351
|
+
}
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
text = plan_path.read_text(encoding="utf-8")
|
|
355
|
+
except OSError as exc:
|
|
356
|
+
return {
|
|
357
|
+
"plan": str(plan_path),
|
|
358
|
+
"checklist_found": False,
|
|
359
|
+
"findings": [],
|
|
360
|
+
"structural_warnings": [],
|
|
361
|
+
"missing_count": len(ITEMS),
|
|
362
|
+
"structural_warning_count": 0,
|
|
363
|
+
"exit_code": 2,
|
|
364
|
+
"error": str(exc),
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
block = extract_checklist_block(text)
|
|
368
|
+
if block is None:
|
|
369
|
+
findings = [
|
|
370
|
+
{
|
|
371
|
+
"item_id": item_id,
|
|
372
|
+
"label": label,
|
|
373
|
+
"answer": None,
|
|
374
|
+
"status": "missing",
|
|
375
|
+
"message": "No <!-- checklist --> block found in plan file.",
|
|
376
|
+
}
|
|
377
|
+
for item_id, label in ITEMS
|
|
378
|
+
]
|
|
379
|
+
return {
|
|
380
|
+
"plan": str(plan_path),
|
|
381
|
+
"checklist_found": False,
|
|
382
|
+
"findings": findings,
|
|
383
|
+
"structural_warnings": [],
|
|
384
|
+
"missing_count": len(ITEMS),
|
|
385
|
+
"structural_warning_count": 0,
|
|
386
|
+
"exit_code": 1,
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
parsed = parse_checklist_block(block)
|
|
390
|
+
# Items 15 and 17 are conditional: only required when UI files are in scope.
|
|
391
|
+
ui_in_scope = bool(_UI_FILE_RE.search(text))
|
|
392
|
+
|
|
393
|
+
findings = []
|
|
394
|
+
missing = 0
|
|
395
|
+
for item_id, label in ITEMS:
|
|
396
|
+
normalized = _normalize_label(label)
|
|
397
|
+
# Item 15/17 optional when no UI in scope; Item 16 always optional (only required
|
|
398
|
+
# when author has identified a high-consequence boundary — absent is valid).
|
|
399
|
+
is_optional = (
|
|
400
|
+
(item_id == "item_15_synthesis_dimensions" and not ui_in_scope)
|
|
401
|
+
or item_id == "item_16_risk_reason"
|
|
402
|
+
or (item_id == "item_17_ui_io_contract" and not ui_in_scope)
|
|
403
|
+
)
|
|
404
|
+
if normalized not in parsed:
|
|
405
|
+
if is_optional:
|
|
406
|
+
findings.append({"item_id": item_id, "label": label, "answer": None,
|
|
407
|
+
"status": "ok", "message": "N/A: no UI in scope."})
|
|
408
|
+
continue
|
|
409
|
+
findings.append({"item_id": item_id, "label": label, "answer": None,
|
|
410
|
+
"status": "missing",
|
|
411
|
+
"message": f"Item not found. Expected '{label}:'"})
|
|
412
|
+
missing += 1
|
|
413
|
+
elif not is_answered(parsed[normalized]):
|
|
414
|
+
if is_optional:
|
|
415
|
+
findings.append({"item_id": item_id, "label": label,
|
|
416
|
+
"answer": parsed[normalized], "status": "ok",
|
|
417
|
+
"message": "N/A: no UI in scope."})
|
|
418
|
+
continue
|
|
419
|
+
findings.append({"item_id": item_id, "label": label,
|
|
420
|
+
"answer": parsed[normalized], "status": "unanswered",
|
|
421
|
+
"message": "Placeholder or empty. Provide answer or 'N/A: <reason>'."})
|
|
422
|
+
missing += 1
|
|
423
|
+
else:
|
|
424
|
+
findings.append({"item_id": item_id, "label": label,
|
|
425
|
+
"answer": parsed[normalized], "status": "ok"})
|
|
426
|
+
|
|
427
|
+
structural_warnings = _structural_findings(text, plan_path)
|
|
428
|
+
structural_fail_count = sum(1 for w in structural_warnings if w.get("status") == "fail")
|
|
429
|
+
return {
|
|
430
|
+
"plan": str(plan_path), "checklist_found": True, "findings": findings,
|
|
431
|
+
"structural_warnings": structural_warnings, "missing_count": missing,
|
|
432
|
+
"structural_warning_count": len(structural_warnings),
|
|
433
|
+
"structural_fail_count": structural_fail_count,
|
|
434
|
+
"exit_code": 0 if (missing == 0 and structural_fail_count == 0) else 1,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
# CLI
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
def main() -> int:
|
|
443
|
+
parser = argparse.ArgumentParser(
|
|
444
|
+
description="Verify spec-writing checklist in a plan markdown file."
|
|
445
|
+
)
|
|
446
|
+
parser.add_argument("--plan", required=True, help="Path to the plan markdown file.")
|
|
447
|
+
parser.add_argument("--json", action="store_true", dest="as_json",
|
|
448
|
+
help="Emit JSON output.")
|
|
449
|
+
parser.add_argument("--quiet", action="store_true",
|
|
450
|
+
help="Suppress human-readable summary (use with --json).")
|
|
451
|
+
args = parser.parse_args()
|
|
452
|
+
|
|
453
|
+
plan_path = Path(args.plan)
|
|
454
|
+
result = verify(plan_path)
|
|
455
|
+
|
|
456
|
+
if args.as_json:
|
|
457
|
+
print(json.dumps(result, indent=2))
|
|
458
|
+
|
|
459
|
+
if not args.quiet:
|
|
460
|
+
status = "CLEAN" if result["exit_code"] == 0 else "INCOMPLETE"
|
|
461
|
+
warn_count = result.get("structural_warning_count", 0)
|
|
462
|
+
print(
|
|
463
|
+
f"check_checklist — {status} "
|
|
464
|
+
f"({result['missing_count']} items missing/unanswered, "
|
|
465
|
+
f"{warn_count} structural warnings)"
|
|
466
|
+
)
|
|
467
|
+
for f in result["findings"]:
|
|
468
|
+
if f["status"] != "ok":
|
|
469
|
+
print(f" [{f['status'].upper()}] {f['label']}: {f.get('message', '')}")
|
|
470
|
+
for w in result.get("structural_warnings", []):
|
|
471
|
+
tag = w.get("status", "warn").upper()
|
|
472
|
+
print(f" [{tag}] {w['label']}: {w.get('message', '')}")
|
|
473
|
+
|
|
474
|
+
return result["exit_code"]
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
if __name__ == "__main__":
|
|
478
|
+
sys.exit(main())
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-loop:sync-skills
|
|
3
|
+
description: Use when the user asks to "check skill drift", "sync skills", or "update architecture skills", or when Phase 1 Assess detects stale source_hash values. Walks skills/architecture/ and skills/debugging/, recomputes source_hash, reports drift. Read-only.
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
user-invocable: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
9
|
+
|
|
10
|
+
# Sync-Skills — Drift Detection for Native Copies
|
|
11
|
+
|
|
12
|
+
Build-loop's `skills/architecture/` and `skills/debugging/` skills are copied from canonical upstream repos (NavGator, claude-code-debugger). Each carries a `source:` path and `source_hash:` SHA-256 in its frontmatter. This skill recomputes the hash from the canonical source file and reports any drift.
|
|
13
|
+
|
|
14
|
+
**Read-only.** Never auto-updates a skill — surfaces a list of skills that need refresh and a one-line refresh command.
|
|
15
|
+
|
|
16
|
+
## When to Activate
|
|
17
|
+
|
|
18
|
+
- User asks "check skill drift", "sync skills", "are native skills stale"
|
|
19
|
+
- After updating NavGator or claude-code-debugger locally and you want to know which build-loop skills inherit the change
|
|
20
|
+
- Periodic maintenance (run quarterly, or before any release that touches the architecture/debugging surfaces)
|
|
21
|
+
|
|
22
|
+
## Workflow
|
|
23
|
+
|
|
24
|
+
Run the drift-check script:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/sync_skills.py
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or inline (no script dependency):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python3 - <<'PY'
|
|
34
|
+
import hashlib, os, pathlib, re, sys
|
|
35
|
+
|
|
36
|
+
ROOT = pathlib.Path(os.environ.get("CLAUDE_PLUGIN_ROOT") or pathlib.Path(__file__).resolve().parents[2])
|
|
37
|
+
HOME = pathlib.Path.home()
|
|
38
|
+
|
|
39
|
+
def read_frontmatter(path):
|
|
40
|
+
text = path.read_text()
|
|
41
|
+
m = re.match(r'^---\n(.*?)\n---\n', text, re.DOTALL)
|
|
42
|
+
if not m:
|
|
43
|
+
return {}
|
|
44
|
+
fm = {}
|
|
45
|
+
for line in m.group(1).splitlines():
|
|
46
|
+
if ':' in line:
|
|
47
|
+
k, v = line.split(':', 1)
|
|
48
|
+
fm[k.strip()] = v.strip().strip('"').strip("'")
|
|
49
|
+
return fm
|
|
50
|
+
|
|
51
|
+
def find_canonical(rel_path):
|
|
52
|
+
# Try common parent dirs
|
|
53
|
+
for parent in [HOME / "dev" / "git-folder", ROOT.parent, pathlib.Path.cwd()]:
|
|
54
|
+
candidate = parent / rel_path
|
|
55
|
+
if candidate.exists():
|
|
56
|
+
return candidate
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
drift = []
|
|
60
|
+
checked = 0
|
|
61
|
+
for tree in ["skills/architecture", "skills/debugging"]:
|
|
62
|
+
for skill_md in (ROOT / tree).rglob("SKILL.md"):
|
|
63
|
+
fm = read_frontmatter(skill_md)
|
|
64
|
+
src = fm.get("source")
|
|
65
|
+
expected_hash = fm.get("source_hash")
|
|
66
|
+
if not src or not expected_hash:
|
|
67
|
+
continue
|
|
68
|
+
canonical = find_canonical(src)
|
|
69
|
+
rel = skill_md.relative_to(ROOT)
|
|
70
|
+
checked += 1
|
|
71
|
+
if not canonical:
|
|
72
|
+
drift.append((str(rel), "MISSING", "canonical source not found: " + src))
|
|
73
|
+
continue
|
|
74
|
+
actual = hashlib.sha256(canonical.read_bytes()).hexdigest()
|
|
75
|
+
if actual != expected_hash:
|
|
76
|
+
drift.append((str(rel), "DRIFT", f"expected {expected_hash[:12]} got {actual[:12]}"))
|
|
77
|
+
|
|
78
|
+
print(f"Checked: {checked} skills")
|
|
79
|
+
if not drift:
|
|
80
|
+
print("Status: clean — no drift")
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
print(f"Status: {len(drift)} drift(s)")
|
|
84
|
+
for rel, kind, detail in drift:
|
|
85
|
+
print(f" [{kind}] {rel}")
|
|
86
|
+
print(f" {detail}")
|
|
87
|
+
print()
|
|
88
|
+
print("Refresh: rerun build-loop:sync-skills with --refresh after reviewing the canonical diff,")
|
|
89
|
+
print("or invoke the orchestrator to re-import the affected skill(s).")
|
|
90
|
+
sys.exit(1 if drift else 0)
|
|
91
|
+
PY
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Output
|
|
95
|
+
|
|
96
|
+
- **Clean**: `Checked: 10 skills. Status: clean — no drift.`
|
|
97
|
+
- **Drift detected**: list of `<skill path> | DRIFT | expected <hash-prefix> got <hash-prefix>` plus one-line refresh hint
|
|
98
|
+
- **Missing canonical**: list of `<skill path> | MISSING | canonical source not found: <path>` — means upstream repo moved or was deleted
|
|
99
|
+
|
|
100
|
+
## What This Skill Does NOT Do
|
|
101
|
+
|
|
102
|
+
- Does not auto-update SKILL.md files (refresh is a deliberate, reviewed action)
|
|
103
|
+
- Does not modify the canonical source repos
|
|
104
|
+
- Does not re-import — the user (or build-orchestrator on user request) re-runs the import flow
|
|
105
|
+
|
|
106
|
+
## How to Refresh a Drifted Skill
|
|
107
|
+
|
|
108
|
+
1. Read the canonical source file at the path in `source:`
|
|
109
|
+
2. Diff against the local SKILL.md (skipping the rewritten frontmatter and any build-loop-specific sibling-skill references)
|
|
110
|
+
3. Decide: pull the upstream change, keep local fork, or escalate to user
|
|
111
|
+
4. If pulling: re-author the local SKILL.md preserving the build-loop-specific framing (skill name, sibling references), recompute `source_hash`, update frontmatter
|
|
112
|
+
5. Re-run `build-loop:sync-skills` to confirm drift cleared
|
|
113
|
+
|
|
114
|
+
## Locations Walked
|
|
115
|
+
|
|
116
|
+
- `skills/architecture/scan/SKILL.md`
|
|
117
|
+
- `skills/architecture/impact/SKILL.md`
|
|
118
|
+
- `skills/architecture/trace/SKILL.md`
|
|
119
|
+
- `skills/architecture/rules/SKILL.md`
|
|
120
|
+
- `skills/architecture/dead/SKILL.md`
|
|
121
|
+
- `skills/architecture/review/SKILL.md`
|
|
122
|
+
- `skills/debugging/memory/SKILL.md`
|
|
123
|
+
- `skills/debugging/store/SKILL.md`
|
|
124
|
+
- `skills/debugging/assess/SKILL.md`
|
|
125
|
+
- `skills/debugging/debug-loop/SKILL.md`
|
|
126
|
+
|
|
127
|
+
## Sibling Skills
|
|
128
|
+
|
|
129
|
+
- All skills under `build-loop:architecture-*` — drift-checked by this skill
|
|
130
|
+
- All skills under `build-loop:debugging-*` — drift-checked by this skill
|
|
131
|
+
|
|
132
|
+
*Native to build-loop. Not copied from any upstream — this is the drift-detector itself.*
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: telemetry
|
|
3
|
+
description: Use when adding observability/telemetry to an app, instrumenting traces/metrics/logs, wiring LLM/agent tracing, choosing a monitoring vendor, or auditing an app for missing telemetry. Triggers — "add telemetry", "add observability", "instrument this", "tracing", "OpenTelemetry/OTel", "LLM observability", "LangChain tracing", "Phoenix/Langfuse/LangSmith", "Crashlytics/Sentry/mobile telemetry", "why is this slow in prod", "no visibility into". OpenTelemetry-first, vendor-neutral.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
8
|
+
|
|
9
|
+
# Telemetry — OpenTelemetry-first, vendor-neutral
|
|
10
|
+
|
|
11
|
+
**The rule:** default to **OpenTelemetry (OTel)**. Instrument with OTel SDKs + **GenAI semantic conventions**; export OTLP to whatever backend fits. Never lock telemetry to a single proprietary vendor, and **never roll your own tracing schema** — OTel conventions exist. This codifies the user's already-decided stack (`prefer-opentelemetry`); it is a default, not dogma — document an exception when a closed tool genuinely wins.
|
|
12
|
+
|
|
13
|
+
Canonical working exemplar in-tree: **`sample-observability-app/src/observability.py`** (OTLP/HTTP → Phoenix, env-gated `SAMPLE_OBSERVE=1`, fails silent). Copy this pattern; don't reinvent it.
|
|
14
|
+
|
|
15
|
+
## Decision tree — pick the stack by app type
|
|
16
|
+
|
|
17
|
+
| App type | Instrument with | Dev backend | Prod backend | Error tracking |
|
|
18
|
+
|---|---|---|---|---|
|
|
19
|
+
| **LLM / agent** (LangChain, LangGraph, raw SDK) | `openinference-instrumentation-langchain` **or** `opentelemetry-instrumentation-langchain` (OpenLLMetry) + OTel SDK | **Arize Phoenix** (Apache-2.0, OTLP-native, local eval UI) | **Langfuse** (self-host, OTLP `/api/public/otel`, evals + replay) | Sentry |
|
|
20
|
+
| **Web / server** (Node, Python) | `@opentelemetry/sdk-node` (JS SDK 2.0, stable) / `opentelemetry-sdk` (Python) + auto-instrumentations | OTel Collector → any | Grafana/SigNoz/Honeycomb/Datadog via OTLP | **Sentry** (the record for deployed web apps) |
|
|
21
|
+
| **Mobile / iOS** (SwiftUI) | **Embrace Apple SDK** (Apache-2.0, built on `opentelemetry-swift`; adds crash/ANR/session/UI that bare OTel-Swift lacks) | — | any OTLP backend (Grafana/SigNoz/Honeycomb) | Embrace or Sentry-cocoa (MIT) |
|
|
22
|
+
|
|
23
|
+
**LLM tracing — three paths, in preference order:** (A) **OpenLLMetry** `opentelemetry-instrumentation-langchain` — `LangchainInstrumentor().instrument()`, vendor-neutral OTLP; (B) **OpenInference** `openinference-instrumentation-langchain` — same monkey-patch model, tight Phoenix integration; (C) **LangSmith** — opt-in only (proprietary backend; has OTLP export but canonical storage stays LangSmith). LangChain does **not** emit OTel natively as of 2026, so an instrumentation lib is required. LangSmith stays behind a flag — "no paid posture."
|
|
24
|
+
|
|
25
|
+
**Mobile — the Firebase tradeoff (stated plainly):** Firebase's iOS SDK is Apache-2.0, but **Crashlytics/Performance emit no OTLP** — data is trapped in the Firebase console (or BigQuery export). For OTel portability Firebase Performance is a dead end; Crashlytics is fine for triage only. Prefer Embrace (OTel-compatible) when portability/reliability matter. Note: CocoaPods support for firebase-ios-sdk ends Oct 2026 (SPM only).
|
|
26
|
+
|
|
27
|
+
## Minimal wire-up (Python LLM app)
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
31
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
32
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
33
|
+
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
|
|
34
|
+
|
|
35
|
+
provider = TracerProvider()
|
|
36
|
+
provider.add_span_processor(BatchSpanProcessor(
|
|
37
|
+
OTLPSpanExporter(endpoint="http://otel-collector:4317"))) # swap endpoint = swap backend
|
|
38
|
+
LangchainInstrumentor().instrument(tracer_provider=provider)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use GenAI semconv attributes (`gen_ai.system`, `gen_ai.request.model`, `gen_ai.usage.input_tokens`) so the data is backend-agnostic. Gate observability behind an env flag, default-off on cheap tiers (OTel + batch processor adds ~10–50 ms/call). Probe the backend at startup and **warn without blocking** if it's down (fail-silent, like the sample observability exemplar).
|
|
42
|
+
|
|
43
|
+
## Status (2026, cite before relying — these move)
|
|
44
|
+
|
|
45
|
+
- OTel **GenAI client spans stable** (early 2026); agent/framework spans experimental-but-stable-in-practice. JS SDK 2.0 stable; Python SDK stable.
|
|
46
|
+
- Phoenix, Langfuse, OpenLLMetry, OpenInference all OTLP-compatible and OSS.
|
|
47
|
+
- Embrace Kotlin SDK donated to OTel (CNCF, Mar 2026, KMP — Android-first); Embrace **Apple** SDK stays in Embrace's repo (Apache-2.0) but is built on OTel primitives.
|
|
48
|
+
|
|
49
|
+
## Instrumentation gaps in this user's repos (audit 2026-05-31)
|
|
50
|
+
|
|
51
|
+
Already instrumented (reference these): **sample-observability-app** (OTel+Phoenix+OpenInference, full), **sample-reader/sample-rag** (OTel full + Sentry), **sample-secrets-service** (OTel metrics).
|
|
52
|
+
|
|
53
|
+
**Gaps worth closing first** — LLM/agent apps with zero telemetry:
|
|
54
|
+
1. **sample-research-app** — LangChain + LangGraph, nothing. Drop in the sample observability pattern (~10 lines) for instant span/token visibility.
|
|
55
|
+
2. **sample-strategy-app** — has LangSmith dep but no OTel plumbing; unify with the OTel-first stack.
|
|
56
|
+
3. **sample-agent-app** — LangGraph, no telemetry.
|
|
57
|
+
|
|
58
|
+
Partial (error-only, no traces): sample-product-app, sample-decision-app, sample-prompt-lab (Sentry only). **sample-voice-ios** — no telemetry; candidate for Embrace Apple SDK.
|
|
59
|
+
|
|
60
|
+
## When build-loop fires this skill
|
|
61
|
+
|
|
62
|
+
Phase 1 Assess: if the build touches a server/LLM/mobile app with no telemetry, flag the gap. Phase 2 Plan: if the build adds a new service, LLM call path, or user-facing mobile flow, include an OTel instrumentation step (default-off env flag). Phase 4 Review: a new LLM call path shipped without span coverage is an observability gap to surface, not a blocker.
|
|
63
|
+
|
|
64
|
+
## Sources
|
|
65
|
+
|
|
66
|
+
OTel GenAI semconv: opentelemetry.io/docs/specs/semconv/gen-ai/ · JS SDK 2.0: opentelemetry.io/blog/2025/otel-js-sdk-2-0/ · OpenLLMetry: github.com/traceloop/openllmetry · OpenInference: github.com/Arize-ai/openinference · Phoenix: arize.com/docs/phoenix · Langfuse OTel: langfuse.com/integrations/native/opentelemetry · Embrace Apple: github.com/embrace-io/embrace-apple-sdk · opentelemetry-swift: github.com/open-telemetry/opentelemetry-swift · Sentry OTLP: docs.sentry.io/concepts/otlp/ · Firebase: firebase.google.com/docs/crashlytics, /docs/perf-mon.
|