prizmkit 1.1.99 → 1.1.101
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/bin/create-prizmkit.js +4 -4
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/.env.example +3 -3
- package/bundled/dev-pipeline/README.md +110 -90
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +1 -1
- package/bundled/dev-pipeline/cli.py +27 -0
- package/bundled/dev-pipeline/prizmkit_runtime/__init__.py +13 -0
- package/bundled/dev-pipeline/prizmkit_runtime/__main__.py +9 -0
- package/bundled/dev-pipeline/prizmkit_runtime/cli.py +193 -0
- package/bundled/dev-pipeline/prizmkit_runtime/commands.py +313 -0
- package/bundled/dev-pipeline/prizmkit_runtime/compat.py +32 -0
- package/bundled/dev-pipeline/prizmkit_runtime/config.py +410 -0
- package/bundled/dev-pipeline/prizmkit_runtime/daemon.py +478 -0
- package/bundled/dev-pipeline/prizmkit_runtime/gitops.py +734 -0
- package/bundled/dev-pipeline/prizmkit_runtime/heartbeat.py +256 -0
- package/bundled/dev-pipeline/prizmkit_runtime/interoperability.py +109 -0
- package/bundled/dev-pipeline/prizmkit_runtime/paths.py +110 -0
- package/bundled/dev-pipeline/prizmkit_runtime/processes.py +373 -0
- package/bundled/dev-pipeline/prizmkit_runtime/reset.py +389 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +51 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_classification.py +179 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_models.py +345 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_prompts.py +118 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_recovery.py +187 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runner_status.py +147 -0
- package/bundled/dev-pipeline/prizmkit_runtime/runners.py +422 -0
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +552 -0
- package/bundled/dev-pipeline/prizmkit_runtime/status.py +71 -0
- package/bundled/dev-pipeline/scripts/check-session-status.py +74 -1
- package/bundled/dev-pipeline/scripts/continuation.py +374 -0
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +64 -52
- package/bundled/dev-pipeline/scripts/generate-bugfix-prompt.py +35 -25
- package/bundled/dev-pipeline/scripts/generate-recovery-prompt.py +2 -2
- package/bundled/dev-pipeline/scripts/generate-refactor-prompt.py +15 -20
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +185 -16
- package/bundled/dev-pipeline/scripts/update-bug-status.py +214 -6
- package/bundled/dev-pipeline/scripts/update-feature-status.py +237 -6
- package/bundled/dev-pipeline/scripts/update-refactor-status.py +214 -6
- package/bundled/dev-pipeline/templates/agent-prompts/dev-implement.md +8 -2
- package/bundled/dev-pipeline/templates/bootstrap-prompt.md +4 -0
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +14 -5
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +16 -6
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +16 -6
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +1 -1
- package/bundled/dev-pipeline/templates/refactor-bootstrap-prompt.md +3 -3
- package/bundled/dev-pipeline/templates/sections/context-budget-rules.md +2 -0
- package/bundled/dev-pipeline/templates/sections/log-size-awareness.md +13 -69
- package/bundled/dev-pipeline/templates/sections/phase-context-snapshot-agent-suffix.md +15 -7
- package/bundled/dev-pipeline/templates/sections/phase-implement-agent.md +42 -29
- package/bundled/dev-pipeline/templates/sections/phase-implement-full.md +41 -39
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-agent.md +4 -0
- package/bundled/dev-pipeline/templates/sections/test-failure-recovery-lite.md +4 -0
- package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
- package/bundled/dev-pipeline/tests/conftest.py +1 -0
- package/bundled/dev-pipeline/tests/test_auto_skip.py +932 -0
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +207 -0
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +100 -0
- package/bundled/dev-pipeline/tests/test_generate_refactor_prompt.py +43 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +1138 -0
- package/bundled/dev-pipeline/tests/test_unified_cli.py +1045 -0
- package/bundled/skills/_metadata.json +1 -1
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +29 -34
- package/bundled/skills/bugfix-pipeline-launcher/references/configuration.md +6 -5
- package/bundled/skills/feature-pipeline-launcher/SKILL.md +29 -35
- package/bundled/skills/feature-pipeline-launcher/references/configuration.md +7 -6
- package/bundled/skills/feature-workflow/SKILL.md +3 -6
- package/bundled/skills/recovery-workflow/SKILL.md +23 -32
- package/bundled/skills/refactor-pipeline-launcher/SKILL.md +29 -35
- package/bundled/skills/refactor-pipeline-launcher/references/configuration.md +7 -6
- package/bundled/skills/refactor-workflow/SKILL.md +3 -6
- package/bundled/templates/hooks/commit-intent.json +2 -2
- package/bundled/templates/hooks/diff-prizm-docs.py +226 -0
- package/bundled/templates/hooks/prizm-pre-commit.py +45 -0
- package/bundled/templates/hooks/validate-prizm-docs.py +240 -0
- package/package.json +1 -1
- package/src/config.js +33 -12
- package/src/index.js +3 -6
- package/src/manifest.js +3 -2
- package/src/metadata.js +7 -18
- package/src/prompts.js +0 -12
- package/src/runtimes.js +18 -11
- package/src/scaffold.js +133 -116
- package/src/upgrade.js +1 -1
- package/bundled/dev-pipeline/launch-bugfix-daemon.sh +0 -500
- package/bundled/dev-pipeline/launch-feature-daemon.sh +0 -657
- package/bundled/dev-pipeline/launch-refactor-daemon.sh +0 -502
- package/bundled/dev-pipeline/lib/branch.sh +0 -326
- package/bundled/dev-pipeline/lib/common.sh +0 -1257
- package/bundled/dev-pipeline/lib/heartbeat.sh +0 -469
- package/bundled/dev-pipeline/reset-bug.sh +0 -395
- package/bundled/dev-pipeline/reset-feature.sh +0 -406
- package/bundled/dev-pipeline/reset-refactor.sh +0 -393
- package/bundled/dev-pipeline/run-bugfix.sh +0 -1315
- package/bundled/dev-pipeline/run-feature.sh +0 -1602
- package/bundled/dev-pipeline/run-recovery.sh +0 -648
- package/bundled/dev-pipeline/run-refactor.sh +0 -1348
- package/bundled/dev-pipeline/scripts/monitor-log.sh +0 -104
- package/bundled/dev-pipeline/tests/test-path-resolution.sh +0 -100
- package/bundled/dev-pipeline-windows/.env.example +0 -36
- package/bundled/dev-pipeline-windows/README.md +0 -30
- package/bundled/dev-pipeline-windows/SCHEMA_ANALYSIS.md +0 -533
- package/bundled/dev-pipeline-windows/assets/feature-list-example.json +0 -146
- package/bundled/dev-pipeline-windows/assets/prizm-dev-team-integration.md +0 -137
- package/bundled/dev-pipeline-windows/launch-bugfix-daemon.ps1 +0 -9
- package/bundled/dev-pipeline-windows/launch-feature-daemon.ps1 +0 -9
- package/bundled/dev-pipeline-windows/launch-refactor-daemon.ps1 +0 -9
- package/bundled/dev-pipeline-windows/lib/branch.ps1 +0 -235
- package/bundled/dev-pipeline-windows/lib/common.ps1 +0 -829
- package/bundled/dev-pipeline-windows/lib/daemon.ps1 +0 -140
- package/bundled/dev-pipeline-windows/lib/pipeline.ps1 +0 -967
- package/bundled/dev-pipeline-windows/lib/reset.ps1 +0 -87
- package/bundled/dev-pipeline-windows/reset-bug.ps1 +0 -9
- package/bundled/dev-pipeline-windows/reset-feature.ps1 +0 -9
- package/bundled/dev-pipeline-windows/reset-refactor.ps1 +0 -9
- package/bundled/dev-pipeline-windows/run-bugfix.ps1 +0 -9
- package/bundled/dev-pipeline-windows/run-feature.ps1 +0 -9
- package/bundled/dev-pipeline-windows/run-recovery.ps1 +0 -179
- package/bundled/dev-pipeline-windows/run-refactor.ps1 +0 -9
- package/bundled/dev-pipeline-windows/scripts/check-session-status.py +0 -228
- package/bundled/dev-pipeline-windows/scripts/detect-stuck.py +0 -530
- package/bundled/dev-pipeline-windows/scripts/generate-bootstrap-prompt.py +0 -2282
- package/bundled/dev-pipeline-windows/scripts/generate-bugfix-prompt.py +0 -767
- package/bundled/dev-pipeline-windows/scripts/generate-recovery-prompt.py +0 -805
- package/bundled/dev-pipeline-windows/scripts/generate-refactor-prompt.py +0 -845
- package/bundled/dev-pipeline-windows/scripts/init-bugfix-pipeline.py +0 -316
- package/bundled/dev-pipeline-windows/scripts/init-dev-team.py +0 -134
- package/bundled/dev-pipeline-windows/scripts/init-pipeline.py +0 -382
- package/bundled/dev-pipeline-windows/scripts/init-refactor-pipeline.py +0 -399
- package/bundled/dev-pipeline-windows/scripts/monitor-log.ps1 +0 -102
- package/bundled/dev-pipeline-windows/scripts/parse-stream-progress.py +0 -1232
- package/bundled/dev-pipeline-windows/scripts/patch-completion-notes.py +0 -191
- package/bundled/dev-pipeline-windows/scripts/prizmkit-test-gate.py +0 -446
- package/bundled/dev-pipeline-windows/scripts/update-bug-status.py +0 -913
- package/bundled/dev-pipeline-windows/scripts/update-checkpoint.py +0 -173
- package/bundled/dev-pipeline-windows/scripts/update-feature-status.py +0 -1558
- package/bundled/dev-pipeline-windows/scripts/update-refactor-status.py +0 -1119
- package/bundled/dev-pipeline-windows/scripts/utils.py +0 -542
- package/bundled/dev-pipeline-windows/templates/agent-prompts/critic-plan-challenge.md +0 -7
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-fix.md +0 -7
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-implement.md +0 -65
- package/bundled/dev-pipeline-windows/templates/agent-prompts/dev-resume.md +0 -5
- package/bundled/dev-pipeline-windows/templates/agent-prompts/reviewer-review.md +0 -7
- package/bundled/dev-pipeline-windows/templates/bootstrap-prompt.md +0 -73
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier1.md +0 -589
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier2.md +0 -710
- package/bundled/dev-pipeline-windows/templates/bootstrap-tier3.md +0 -791
- package/bundled/dev-pipeline-windows/templates/bug-fix-list-schema.json +0 -263
- package/bundled/dev-pipeline-windows/templates/bugfix-bootstrap-prompt.md +0 -326
- package/bundled/dev-pipeline-windows/templates/feature-list-schema.json +0 -237
- package/bundled/dev-pipeline-windows/templates/refactor-bootstrap-prompt.md +0 -340
- package/bundled/dev-pipeline-windows/templates/refactor-list-schema.json +0 -270
- package/bundled/dev-pipeline-windows/templates/sections/ac-verification-checklist.md +0 -7
- package/bundled/dev-pipeline-windows/templates/sections/checkpoint-system.md +0 -91
- package/bundled/dev-pipeline-windows/templates/sections/context-budget-rules.md +0 -34
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-agent.md +0 -10
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-full.md +0 -12
- package/bundled/dev-pipeline-windows/templates/sections/critical-paths-lite.md +0 -7
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-agent.md +0 -8
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-full.md +0 -9
- package/bundled/dev-pipeline-windows/templates/sections/directory-convention-lite.md +0 -6
- package/bundled/dev-pipeline-windows/templates/sections/failure-capture.md +0 -21
- package/bundled/dev-pipeline-windows/templates/sections/feature-context.md +0 -21
- package/bundled/dev-pipeline-windows/templates/sections/log-size-awareness.md +0 -78
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-auto.md +0 -283
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification-opencli.md +0 -112
- package/bundled/dev-pipeline-windows/templates/sections/phase-browser-verification.md +0 -168
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit-full.md +0 -82
- package/bundled/dev-pipeline-windows/templates/sections/phase-commit.md +0 -75
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-agent-suffix.md +0 -23
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-base.md +0 -14
- package/bundled/dev-pipeline-windows/templates/sections/phase-context-snapshot-lite-suffix.md +0 -19
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan-full.md +0 -63
- package/bundled/dev-pipeline-windows/templates/sections/phase-critic-plan.md +0 -42
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-agent.md +0 -44
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-full.md +0 -59
- package/bundled/dev-pipeline-windows/templates/sections/phase-implement-lite.md +0 -39
- package/bundled/dev-pipeline-windows/templates/sections/phase-plan-agent.md +0 -27
- package/bundled/dev-pipeline-windows/templates/sections/phase-plan-lite.md +0 -27
- package/bundled/dev-pipeline-windows/templates/sections/phase-prizmkit-test.md +0 -39
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-agent.md +0 -35
- package/bundled/dev-pipeline-windows/templates/sections/phase-review-full.md +0 -35
- package/bundled/dev-pipeline-windows/templates/sections/phase-specify-plan-full.md +0 -73
- package/bundled/dev-pipeline-windows/templates/sections/phase0-init.md +0 -13
- package/bundled/dev-pipeline-windows/templates/sections/phase0-test-baseline.md +0 -21
- package/bundled/dev-pipeline-windows/templates/sections/session-context.md +0 -5
- package/bundled/dev-pipeline-windows/templates/sections/subagent-timeout-recovery.md +0 -6
- package/bundled/dev-pipeline-windows/templates/sections/task-contract.md +0 -34
- package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-agent.md +0 -48
- package/bundled/dev-pipeline-windows/templates/sections/test-failure-recovery-lite.md +0 -48
- package/bundled/dev-pipeline-windows/templates/session-status-schema.json +0 -83
- package/bundled/skills-windows/app-planner/SKILL.md +0 -296
- package/bundled/skills-windows/app-planner/assets/app-design-guide.md +0 -101
- package/bundled/skills-windows/app-planner/references/architecture-decisions.md +0 -52
- package/bundled/skills-windows/app-planner/references/brainstorm-guide.md +0 -101
- package/bundled/skills-windows/app-planner/references/frontend-design-guide.md +0 -71
- package/bundled/skills-windows/app-planner/references/infrastructure-convention-discovery.md +0 -108
- package/bundled/skills-windows/app-planner/references/project-brief-guide.md +0 -82
- package/bundled/skills-windows/app-planner/references/project-conventions-discovery.md +0 -59
- package/bundled/skills-windows/app-planner/references/project-state-detection.md +0 -90
- package/bundled/skills-windows/app-planner/references/red-team-checklist.md +0 -40
- package/bundled/skills-windows/app-planner/references/rules/backend/derivation-rules.md +0 -619
- package/bundled/skills-windows/app-planner/references/rules/backend/fixed-rules.md +0 -285
- package/bundled/skills-windows/app-planner/references/rules/backend/question-bank.md +0 -249
- package/bundled/skills-windows/app-planner/references/rules/backend/question-manifest.json +0 -46
- package/bundled/skills-windows/app-planner/references/rules/backend/template.md +0 -173
- package/bundled/skills-windows/app-planner/references/rules/database/derivation-rules.md +0 -382
- package/bundled/skills-windows/app-planner/references/rules/database/fixed-rules.md +0 -211
- package/bundled/skills-windows/app-planner/references/rules/database/question-bank.md +0 -184
- package/bundled/skills-windows/app-planner/references/rules/database/question-manifest.json +0 -39
- package/bundled/skills-windows/app-planner/references/rules/database/template.md +0 -158
- package/bundled/skills-windows/app-planner/references/rules/frontend/derivation-rules.md +0 -820
- package/bundled/skills-windows/app-planner/references/rules/frontend/fixed-rules.md +0 -188
- package/bundled/skills-windows/app-planner/references/rules/frontend/question-bank.md +0 -319
- package/bundled/skills-windows/app-planner/references/rules/frontend/question-manifest.json +0 -51
- package/bundled/skills-windows/app-planner/references/rules/frontend/template.md +0 -339
- package/bundled/skills-windows/app-planner/references/rules/mobile/derivation-rules.md +0 -649
- package/bundled/skills-windows/app-planner/references/rules/mobile/fixed-rules.md +0 -290
- package/bundled/skills-windows/app-planner/references/rules/mobile/question-bank.md +0 -232
- package/bundled/skills-windows/app-planner/references/rules/mobile/question-manifest.json +0 -47
- package/bundled/skills-windows/app-planner/references/rules/mobile/template.md +0 -175
- package/bundled/skills-windows/app-planner/references/rules-configuration.md +0 -46
- package/bundled/skills-windows/bug-fix-workflow/SKILL.md +0 -379
- package/bundled/skills-windows/bug-fix-workflow/references/bug-diagnosis.md +0 -41
- package/bundled/skills-windows/bug-planner/SKILL.md +0 -402
- package/bundled/skills-windows/bug-planner/assets/bug-confirmation-template.md +0 -43
- package/bundled/skills-windows/bug-planner/references/critic-and-verification.md +0 -44
- package/bundled/skills-windows/bug-planner/references/error-recovery.md +0 -73
- package/bundled/skills-windows/bug-planner/references/input-formats.md +0 -53
- package/bundled/skills-windows/bug-planner/references/schema-validation.md +0 -25
- package/bundled/skills-windows/bug-planner/references/severity-rules.md +0 -16
- package/bundled/skills-windows/bug-planner/scripts/validate-bug-list.py +0 -322
- package/bundled/skills-windows/bugfix-pipeline-launcher/SKILL.md +0 -304
- package/bundled/skills-windows/bugfix-pipeline-launcher/references/configuration.md +0 -84
- package/bundled/skills-windows/feature-pipeline-launcher/SKILL.md +0 -381
- package/bundled/skills-windows/feature-pipeline-launcher/references/configuration.md +0 -67
- package/bundled/skills-windows/feature-pipeline-launcher/scripts/preflight-check.py +0 -462
- package/bundled/skills-windows/feature-planner/SKILL.md +0 -397
- package/bundled/skills-windows/feature-planner/assets/evaluation-guide.md +0 -64
- package/bundled/skills-windows/feature-planner/assets/planning-guide.md +0 -214
- package/bundled/skills-windows/feature-planner/references/browser-interaction.md +0 -59
- package/bundled/skills-windows/feature-planner/references/completeness-review.md +0 -57
- package/bundled/skills-windows/feature-planner/references/decomposition-patterns.md +0 -75
- package/bundled/skills-windows/feature-planner/references/error-recovery.md +0 -90
- package/bundled/skills-windows/feature-planner/references/incremental-feature-planning.md +0 -112
- package/bundled/skills-windows/feature-planner/references/new-project-planning.md +0 -85
- package/bundled/skills-windows/feature-planner/scripts/validate-and-generate.py +0 -1020
- package/bundled/skills-windows/feature-workflow/SKILL.md +0 -374
- package/bundled/skills-windows/feature-workflow/references/brainstorm-guide.md +0 -137
- package/bundled/skills-windows/prizm-kit/SKILL.md +0 -93
- package/bundled/skills-windows/prizmkit-code-review/SKILL.md +0 -161
- package/bundled/skills-windows/prizmkit-code-review/references/dev-agent-prompt.md +0 -30
- package/bundled/skills-windows/prizmkit-code-review/references/review-report-template.md +0 -31
- package/bundled/skills-windows/prizmkit-code-review/references/reviewer-agent-prompt.md +0 -66
- package/bundled/skills-windows/prizmkit-code-review/scripts/check_loop.py +0 -186
- package/bundled/skills-windows/prizmkit-committer/SKILL.md +0 -89
- package/bundled/skills-windows/prizmkit-deploy/SKILL.md +0 -444
- package/bundled/skills-windows/prizmkit-deploy/references/ci-cd-workflows.md +0 -115
- package/bundled/skills-windows/prizmkit-deploy/references/cloud-platform-deploy.md +0 -93
- package/bundled/skills-windows/prizmkit-deploy/references/data-safety-examples.md +0 -120
- package/bundled/skills-windows/prizmkit-deploy/references/database-setup.md +0 -46
- package/bundled/skills-windows/prizmkit-deploy/references/deploy-config-schema.md +0 -148
- package/bundled/skills-windows/prizmkit-deploy/references/deploy-history-schema.md +0 -62
- package/bundled/skills-windows/prizmkit-deploy/references/deployment-modes.md +0 -50
- package/bundled/skills-windows/prizmkit-deploy/references/direct-upload.md +0 -26
- package/bundled/skills-windows/prizmkit-deploy/references/dns-setup.md +0 -42
- package/bundled/skills-windows/prizmkit-deploy/references/docker-deploy.md +0 -31
- package/bundled/skills-windows/prizmkit-deploy/references/firewall-setup.md +0 -37
- package/bundled/skills-windows/prizmkit-deploy/references/live-validation-notes.md +0 -21
- package/bundled/skills-windows/prizmkit-deploy/references/nginx-blue-green.md +0 -59
- package/bundled/skills-windows/prizmkit-deploy/references/ssh-bootstrap-flow.md +0 -49
- package/bundled/skills-windows/prizmkit-deploy/references/ssh-execution-flow.md +0 -41
- package/bundled/skills-windows/prizmkit-deploy/references/ssh-takeover.md +0 -20
- package/bundled/skills-windows/prizmkit-deploy/references/ssl-setup.md +0 -56
- package/bundled/skills-windows/prizmkit-implement/SKILL.md +0 -71
- package/bundled/skills-windows/prizmkit-init/SKILL.md +0 -356
- package/bundled/skills-windows/prizmkit-init/assets/project-brief-template.md +0 -82
- package/bundled/skills-windows/prizmkit-init/references/config-schema.md +0 -70
- package/bundled/skills-windows/prizmkit-init/references/rules/layer-detection.md +0 -41
- package/bundled/skills-windows/prizmkit-init/references/tech-stack-catalog.md +0 -13
- package/bundled/skills-windows/prizmkit-init/references/update-supplement.md +0 -9
- package/bundled/skills-windows/prizmkit-plan/SKILL.md +0 -102
- package/bundled/skills-windows/prizmkit-plan/assets/plan-template.md +0 -115
- package/bundled/skills-windows/prizmkit-plan/assets/spec-template.md +0 -73
- package/bundled/skills-windows/prizmkit-plan/references/clarify-guide.md +0 -67
- package/bundled/skills-windows/prizmkit-plan/references/examples.md +0 -85
- package/bundled/skills-windows/prizmkit-plan/references/verification-checklist.md +0 -60
- package/bundled/skills-windows/prizmkit-prizm-docs/SKILL.md +0 -129
- package/bundled/skills-windows/prizmkit-prizm-docs/assets/prizm-docs-format.md +0 -613
- package/bundled/skills-windows/prizmkit-prizm-docs/references/op-init.md +0 -45
- package/bundled/skills-windows/prizmkit-prizm-docs/references/op-rebuild.md +0 -15
- package/bundled/skills-windows/prizmkit-prizm-docs/references/op-status.md +0 -14
- package/bundled/skills-windows/prizmkit-prizm-docs/references/op-update.md +0 -19
- package/bundled/skills-windows/prizmkit-prizm-docs/references/op-validate.md +0 -17
- package/bundled/skills-windows/prizmkit-retrospective/SKILL.md +0 -87
- package/bundled/skills-windows/prizmkit-retrospective/references/knowledge-injection-steps.md +0 -50
- package/bundled/skills-windows/prizmkit-retrospective/references/structural-sync-steps.md +0 -43
- package/bundled/skills-windows/prizmkit-test/SKILL.md +0 -227
- package/bundled/skills-windows/prizmkit-test/references/boundary-coverage-protocol.md +0 -220
- package/bundled/skills-windows/prizmkit-test/references/examples.md +0 -143
- package/bundled/skills-windows/prizmkit-test/references/service-boundary-test-catalog.md +0 -225
- package/bundled/skills-windows/prizmkit-test/references/test-generation-steps.md +0 -109
- package/bundled/skills-windows/prizmkit-test/references/test-report-template.md +0 -111
- package/bundled/skills-windows/prizmkit-test/scripts/validate_boundary_report.py +0 -347
- package/bundled/skills-windows/recovery-workflow/SKILL.md +0 -355
- package/bundled/skills-windows/recovery-workflow/evals/evals.json +0 -46
- package/bundled/skills-windows/recovery-workflow/references/detection.md +0 -58
- package/bundled/skills-windows/recovery-workflow/scripts/detect-recovery-state.py +0 -544
- package/bundled/skills-windows/refactor-pipeline-launcher/SKILL.md +0 -346
- package/bundled/skills-windows/refactor-pipeline-launcher/references/configuration.md +0 -72
- package/bundled/skills-windows/refactor-planner/SKILL.md +0 -398
- package/bundled/skills-windows/refactor-planner/assets/planning-guide.md +0 -292
- package/bundled/skills-windows/refactor-planner/references/behavior-preservation.md +0 -301
- package/bundled/skills-windows/refactor-planner/references/fast-path.md +0 -59
- package/bundled/skills-windows/refactor-planner/references/planning-phases.md +0 -135
- package/bundled/skills-windows/refactor-planner/references/refactor-scoping-guide.md +0 -221
- package/bundled/skills-windows/refactor-planner/scripts/validate-and-generate-refactor.py +0 -858
- package/bundled/skills-windows/refactor-workflow/SKILL.md +0 -343
- package/bundled/skills-windows/refactor-workflow/references/brainstorm-guide.md +0 -116
- package/bundled/templates/hooks/diff-prizm-docs.sh +0 -158
- package/bundled/templates/hooks/prizm-pre-commit.sh +0 -108
- package/bundled/templates/hooks/validate-prizm-docs.sh +0 -139
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
"""Tests for the unified Python CLI architecture skeleton."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import FrozenInstanceError
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
15
|
+
PIPELINE_ROOT = REPO_ROOT / "dev-pipeline"
|
|
16
|
+
CLI_PATH = PIPELINE_ROOT / "cli.py"
|
|
17
|
+
def _python_310_plus() -> str:
|
|
18
|
+
if sys.version_info >= (3, 10):
|
|
19
|
+
return sys.executable
|
|
20
|
+
for executable in ("python3.12", "python3.11", "python3.10"):
|
|
21
|
+
resolved = shutil.which(executable)
|
|
22
|
+
if resolved:
|
|
23
|
+
return resolved
|
|
24
|
+
return sys.executable
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
PYTHON_310_PLUS = _python_310_plus()
|
|
28
|
+
|
|
29
|
+
if str(PIPELINE_ROOT) not in sys.path:
|
|
30
|
+
sys.path.insert(0, str(PIPELINE_ROOT))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run_cli(*args):
|
|
34
|
+
return subprocess.run(
|
|
35
|
+
[PYTHON_310_PLUS, str(CLI_PATH), *args],
|
|
36
|
+
cwd=REPO_ROOT,
|
|
37
|
+
text=True,
|
|
38
|
+
stdout=subprocess.PIPE,
|
|
39
|
+
stderr=subprocess.PIPE,
|
|
40
|
+
check=False,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_cli_help_exposes_python_runtime_command_groups():
|
|
45
|
+
result = run_cli("--help")
|
|
46
|
+
|
|
47
|
+
assert result.returncode == 0
|
|
48
|
+
assert "canonical Python runtime CLI" in result.stdout
|
|
49
|
+
assert "without shell wrappers" in result.stdout
|
|
50
|
+
for group in [
|
|
51
|
+
"feature",
|
|
52
|
+
"bugfix",
|
|
53
|
+
"refactor",
|
|
54
|
+
"recovery",
|
|
55
|
+
"reset",
|
|
56
|
+
"daemon",
|
|
57
|
+
"status",
|
|
58
|
+
"diagnostics",
|
|
59
|
+
]:
|
|
60
|
+
assert group in result.stdout
|
|
61
|
+
assert "Traceback" not in result.stderr
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_module_entrypoint_help_exposes_same_cli():
|
|
65
|
+
env = {**os.environ, "PYTHONPATH": str(PIPELINE_ROOT)}
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
[PYTHON_310_PLUS, "-m", "prizmkit_runtime", "--help"],
|
|
68
|
+
cwd=REPO_ROOT,
|
|
69
|
+
env=env,
|
|
70
|
+
text=True,
|
|
71
|
+
stdout=subprocess.PIPE,
|
|
72
|
+
stderr=subprocess.PIPE,
|
|
73
|
+
check=False,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert result.returncode == 0
|
|
77
|
+
assert "canonical Python runtime CLI" in result.stdout
|
|
78
|
+
assert "diagnostics" in result.stdout
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_invalid_arguments_report_usage_without_traceback():
|
|
82
|
+
result = run_cli("unknown-command")
|
|
83
|
+
|
|
84
|
+
assert result.returncode == 2
|
|
85
|
+
assert "usage:" in result.stderr
|
|
86
|
+
assert "invalid choice" in result.stderr
|
|
87
|
+
assert "Traceback" not in result.stderr
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_python_version_guard_mentions_required_version():
|
|
91
|
+
from prizmkit_runtime.compat import UnsupportedPythonVersion, ensure_supported_python
|
|
92
|
+
|
|
93
|
+
ensure_supported_python((3, 10, 0))
|
|
94
|
+
ensure_supported_python((3, 11, 1))
|
|
95
|
+
|
|
96
|
+
with pytest.raises(UnsupportedPythonVersion) as exc_info:
|
|
97
|
+
ensure_supported_python((3, 9, 18))
|
|
98
|
+
|
|
99
|
+
assert "Python 3.10+" in str(exc_info.value)
|
|
100
|
+
assert "detected Python 3.9" in str(exc_info.value)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_runtime_paths_support_framework_source_layout():
|
|
104
|
+
from prizmkit_runtime.paths import RuntimePaths, resolve_runtime_paths
|
|
105
|
+
|
|
106
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
|
|
107
|
+
|
|
108
|
+
assert isinstance(paths, RuntimePaths)
|
|
109
|
+
assert paths.layout == "framework-source"
|
|
110
|
+
assert paths.pipeline_root == PIPELINE_ROOT.resolve()
|
|
111
|
+
assert paths.project_root == REPO_ROOT.resolve()
|
|
112
|
+
assert paths.prizmkit_dir == REPO_ROOT / ".prizmkit"
|
|
113
|
+
assert paths.feature_plan == paths.plans_dir / "feature-list.json"
|
|
114
|
+
assert paths.bugfix_plan == paths.plans_dir / "bug-fix-list.json"
|
|
115
|
+
assert paths.refactor_plan == paths.plans_dir / "refactor-list.json"
|
|
116
|
+
assert paths.feature_state_dir == paths.state_dir / "features"
|
|
117
|
+
assert paths.specs_dir == paths.prizmkit_dir / "specs"
|
|
118
|
+
assert all(isinstance(value, Path) for value in paths.path_values())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_runtime_paths_support_installed_layout(tmp_path):
|
|
122
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
123
|
+
|
|
124
|
+
installed_pipeline = tmp_path / "project" / ".prizmkit" / "dev-pipeline"
|
|
125
|
+
installed_pipeline.mkdir(parents=True)
|
|
126
|
+
|
|
127
|
+
paths = resolve_runtime_paths(pipeline_root=installed_pipeline)
|
|
128
|
+
|
|
129
|
+
assert paths.layout == "installed"
|
|
130
|
+
assert paths.project_root == tmp_path / "project"
|
|
131
|
+
assert paths.prizmkit_dir == tmp_path / "project" / ".prizmkit"
|
|
132
|
+
assert paths.pipeline_root == installed_pipeline.resolve()
|
|
133
|
+
assert paths.logs_dir == paths.state_dir / "logs"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_runtime_config_models_environment_and_ai_resolution():
|
|
137
|
+
from prizmkit_runtime.config import AIClientResolution, RuntimeEnvironment, load_runtime_config
|
|
138
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
139
|
+
|
|
140
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
|
|
141
|
+
config = load_runtime_config(
|
|
142
|
+
paths,
|
|
143
|
+
env={
|
|
144
|
+
"AI_CLI": "claude",
|
|
145
|
+
"PRIZMKIT_PLATFORM": "codex",
|
|
146
|
+
"MODEL": "claude-sonnet-4.6",
|
|
147
|
+
"PRIZMKIT_EFFORT": "full",
|
|
148
|
+
"MAX_RETRIES": "5",
|
|
149
|
+
"PATH": "/usr/bin:/bin",
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
assert config.max_retries == 5
|
|
154
|
+
assert config.model == "claude-sonnet-4.6"
|
|
155
|
+
assert config.effort == "full"
|
|
156
|
+
assert isinstance(config.environment, RuntimeEnvironment)
|
|
157
|
+
assert isinstance(config.ai_client, AIClientResolution)
|
|
158
|
+
assert config.ai_client.command == "claude"
|
|
159
|
+
assert config.ai_client.platform == "claude"
|
|
160
|
+
assert config.ai_client.source == "environment:AI_CLI"
|
|
161
|
+
assert "AI_CLI" in config.ai_client.precedence
|
|
162
|
+
assert ".prizmkit/config.json:ai_cli" in config.ai_client.precedence
|
|
163
|
+
assert config.runtime_mode == "python-runtime-canonical"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_runtime_config_matches_canonical_ai_cli_precedence(tmp_path):
|
|
167
|
+
from prizmkit_runtime.config import load_runtime_config
|
|
168
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
169
|
+
|
|
170
|
+
project = tmp_path / "project"
|
|
171
|
+
pipeline = project / ".prizmkit" / "dev-pipeline"
|
|
172
|
+
pipeline.mkdir(parents=True)
|
|
173
|
+
prizmkit_dir = project / ".prizmkit"
|
|
174
|
+
(prizmkit_dir / "config.json").write_text(json.dumps({"ai_cli": "config-cbc", "platform": "claude"}), encoding="utf-8")
|
|
175
|
+
(prizmkit_dir / "manifest.json").write_text(json.dumps({"platform": "codex"}), encoding="utf-8")
|
|
176
|
+
paths = resolve_runtime_paths(pipeline_root=pipeline)
|
|
177
|
+
|
|
178
|
+
config_cli = load_runtime_config(paths, env={"PATH": ""}).ai_client
|
|
179
|
+
assert config_cli.command == "config-cbc"
|
|
180
|
+
assert config_cli.platform == "codebuddy"
|
|
181
|
+
assert config_cli.source == "prizmkit-config:ai_cli"
|
|
182
|
+
|
|
183
|
+
(prizmkit_dir / "config.json").write_text(json.dumps({"platform": "claude"}), encoding="utf-8")
|
|
184
|
+
manifest_platform = load_runtime_config(paths, env={"PATH": ""}).ai_client
|
|
185
|
+
assert manifest_platform.command is None
|
|
186
|
+
assert manifest_platform.platform == "codex"
|
|
187
|
+
assert manifest_platform.source == "unresolved:prizmkit-manifest:platform"
|
|
188
|
+
assert "codex" in manifest_platform.notes[0]
|
|
189
|
+
|
|
190
|
+
(prizmkit_dir / "manifest.json").unlink()
|
|
191
|
+
(prizmkit_dir / "config.json").write_text("{}", encoding="utf-8")
|
|
192
|
+
legacy_alias = load_runtime_config(paths, env={"CODEBUDDY_CLI": "custom-cbc", "PATH": ""}).ai_client
|
|
193
|
+
assert legacy_alias.command == "custom-cbc"
|
|
194
|
+
assert legacy_alias.platform == "codebuddy"
|
|
195
|
+
assert legacy_alias.source == "environment:CODEBUDDY_CLI"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_runtime_config_supplied_env_does_not_leak_ambient_path(tmp_path):
|
|
199
|
+
from prizmkit_runtime.config import load_runtime_config
|
|
200
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
201
|
+
|
|
202
|
+
isolated_pipeline = tmp_path / "project" / ".prizmkit" / "dev-pipeline"
|
|
203
|
+
isolated_pipeline.mkdir(parents=True)
|
|
204
|
+
paths = resolve_runtime_paths(pipeline_root=isolated_pipeline)
|
|
205
|
+
config = load_runtime_config(paths, env={})
|
|
206
|
+
|
|
207
|
+
assert config.environment.path_entries == ()
|
|
208
|
+
assert config.ai_client.command is None
|
|
209
|
+
assert config.ai_client.source == "unresolved"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
from prizmkit_runtime.commands import list_runtime_commands
|
|
214
|
+
|
|
215
|
+
command_keys = {command.key for command in list_runtime_commands()}
|
|
216
|
+
|
|
217
|
+
expected = {
|
|
218
|
+
("feature", None, "run"),
|
|
219
|
+
("feature", None, "status"),
|
|
220
|
+
("feature", None, "unskip"),
|
|
221
|
+
("feature", None, "test-cli"),
|
|
222
|
+
("feature", None, "describe"),
|
|
223
|
+
("bugfix", None, "run"),
|
|
224
|
+
("bugfix", None, "status"),
|
|
225
|
+
("bugfix", None, "unskip"),
|
|
226
|
+
("bugfix", None, "describe"),
|
|
227
|
+
("refactor", None, "run"),
|
|
228
|
+
("refactor", None, "status"),
|
|
229
|
+
("refactor", None, "unskip"),
|
|
230
|
+
("refactor", None, "describe"),
|
|
231
|
+
("recovery", None, "run"),
|
|
232
|
+
("recovery", None, "detect"),
|
|
233
|
+
("recovery", None, "describe"),
|
|
234
|
+
("reset", "feature", "reset"),
|
|
235
|
+
("reset", "bugfix", "reset"),
|
|
236
|
+
("reset", "refactor", "reset"),
|
|
237
|
+
("reset", None, "describe"),
|
|
238
|
+
("status", "feature", "status"),
|
|
239
|
+
("status", "bugfix", "status"),
|
|
240
|
+
("status", "refactor", "status"),
|
|
241
|
+
("status", "all", "status"),
|
|
242
|
+
("diagnostics", None, "paths"),
|
|
243
|
+
("diagnostics", None, "entrypoints"),
|
|
244
|
+
("diagnostics", None, "version"),
|
|
245
|
+
}
|
|
246
|
+
for target in ["feature", "bugfix", "refactor"]:
|
|
247
|
+
for action in ["start", "stop", "status", "logs", "restart", "describe"]:
|
|
248
|
+
expected.add(("daemon", target, action))
|
|
249
|
+
|
|
250
|
+
assert expected.issubset(command_keys)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_python_runtime_command_output_describes_canonical_boundary():
|
|
254
|
+
result = run_cli("feature", "describe")
|
|
255
|
+
|
|
256
|
+
assert result.returncode == 0
|
|
257
|
+
assert "Python runtime command contract" in result.stdout
|
|
258
|
+
assert "canonical PrizmKit Python runtime" in result.stdout
|
|
259
|
+
assert "current entrypoint" in result.stdout
|
|
260
|
+
assert "python-runtime-canonical" in result.stdout
|
|
261
|
+
assert "fully replaces" not in result.stdout.lower()
|
|
262
|
+
assert "shell runtime remains canonical" not in result.stdout
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_option_first_runtime_arguments_are_forwarded(monkeypatch):
|
|
266
|
+
from prizmkit_runtime import cli as runtime_cli
|
|
267
|
+
|
|
268
|
+
captured = []
|
|
269
|
+
|
|
270
|
+
def fake_handle(group, action, *, target=None, paths=None, legacy_args=()):
|
|
271
|
+
captured.append((group, action, target, legacy_args))
|
|
272
|
+
return runtime_cli.CommandResult(0, "ok", "ok")
|
|
273
|
+
|
|
274
|
+
monkeypatch.setattr(runtime_cli, "ensure_supported_python", lambda: None)
|
|
275
|
+
monkeypatch.setattr(runtime_cli, "handle_runtime_command", fake_handle)
|
|
276
|
+
|
|
277
|
+
assert runtime_cli.main(["recovery", "run", "--dry-run"]) == 0
|
|
278
|
+
assert captured[-1] == ("recovery", "run", None, ("--dry-run",))
|
|
279
|
+
|
|
280
|
+
assert runtime_cli.main(["daemon", "feature", "start", "--mode", "standard"]) == 0
|
|
281
|
+
assert captured[-1] == ("daemon", "start", "feature", ("--mode", "standard"))
|
|
282
|
+
|
|
283
|
+
assert runtime_cli.main(["daemon", "feature", "logs", "--follow"]) == 0
|
|
284
|
+
assert captured[-1] == ("daemon", "logs", "feature", ("--follow",))
|
|
285
|
+
|
|
286
|
+
assert runtime_cli.main(["reset", "feature", "--failed"]) == 0
|
|
287
|
+
assert captured[-1] == ("reset", "reset", "feature", ("--failed",))
|
|
288
|
+
|
|
289
|
+
assert runtime_cli.main(["--project-root", "feature", "feature", "describe"]) == 0
|
|
290
|
+
assert captured[-1] == ("feature", "describe", None, ())
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_unskip_dispatches_status_updater_not_contract(monkeypatch):
|
|
294
|
+
from prizmkit_runtime import runners
|
|
295
|
+
from prizmkit_runtime.commands import handle_runtime_command
|
|
296
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
297
|
+
|
|
298
|
+
class FakeStatus:
|
|
299
|
+
return_code = 0
|
|
300
|
+
stdout = "unskipped\n"
|
|
301
|
+
stderr = ""
|
|
302
|
+
|
|
303
|
+
captured = []
|
|
304
|
+
|
|
305
|
+
def fake_run_status_action(family, action, invocation, *, item_id="", project_root=None, **kwargs):
|
|
306
|
+
captured.append((family.kind, action, invocation.item_id, item_id, project_root))
|
|
307
|
+
return FakeStatus()
|
|
308
|
+
|
|
309
|
+
monkeypatch.setattr(runners, "run_status_action", fake_run_status_action)
|
|
310
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
|
|
311
|
+
|
|
312
|
+
result = handle_runtime_command("feature", "unskip", paths=paths, legacy_args=("F-005",))
|
|
313
|
+
|
|
314
|
+
assert result.exit_code == 0
|
|
315
|
+
assert result.title == "Python feature unskip"
|
|
316
|
+
assert "unskipped" in result.render()
|
|
317
|
+
assert "Python runtime command contract" not in result.render()
|
|
318
|
+
assert captured == [("feature", "unskip", "F-005", "F-005", REPO_ROOT.resolve())]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def test_test_cli_reports_ai_resolution_not_contract(monkeypatch):
|
|
322
|
+
from prizmkit_runtime import runners
|
|
323
|
+
from prizmkit_runtime.commands import handle_runtime_command
|
|
324
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
325
|
+
|
|
326
|
+
class FakeAIClient:
|
|
327
|
+
command = "claude"
|
|
328
|
+
platform = "claude"
|
|
329
|
+
source = "test"
|
|
330
|
+
notes = ()
|
|
331
|
+
|
|
332
|
+
class FakeConfig:
|
|
333
|
+
ai_client = FakeAIClient()
|
|
334
|
+
runtime_mode = "python-runtime-canonical"
|
|
335
|
+
model = "claude-sonnet-4.6"
|
|
336
|
+
|
|
337
|
+
monkeypatch.setattr(runners, "load_runtime_config", lambda paths: FakeConfig())
|
|
338
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
|
|
339
|
+
|
|
340
|
+
result = handle_runtime_command("feature", "test-cli", paths=paths)
|
|
341
|
+
rendered = result.render()
|
|
342
|
+
|
|
343
|
+
assert result.exit_code == 0
|
|
344
|
+
assert result.title == "Python feature AI CLI test"
|
|
345
|
+
assert "detected_cli: claude" in rendered
|
|
346
|
+
assert "Python runtime command contract" not in rendered
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_diagnostics_paths_and_entrypoints_are_standard_library_cli_actions():
|
|
350
|
+
paths_result = run_cli("diagnostics", "paths")
|
|
351
|
+
entrypoints_result = run_cli("diagnostics", "entrypoints")
|
|
352
|
+
|
|
353
|
+
assert paths_result.returncode == 0
|
|
354
|
+
assert "Runtime path diagnostics" in paths_result.stdout
|
|
355
|
+
assert "pipeline_root:" in paths_result.stdout
|
|
356
|
+
assert entrypoints_result.returncode == 0
|
|
357
|
+
assert "Python runtime entrypoint inventory" in entrypoints_result.stdout
|
|
358
|
+
assert "runtime:cli:" in entrypoints_result.stdout
|
|
359
|
+
assert "run-feature.sh" not in entrypoints_result.stdout
|
|
360
|
+
assert "generate-bootstrap-prompt.py" in entrypoints_result.stdout
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_interoperability_preserves_existing_utility_mapping():
|
|
364
|
+
from prizmkit_runtime.interoperability import (
|
|
365
|
+
UTILITY_ENTRYPOINTS,
|
|
366
|
+
UTILITY_LIBRARY_ONLY,
|
|
367
|
+
describe_existing_entrypoint,
|
|
368
|
+
)
|
|
369
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
370
|
+
|
|
371
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
|
|
372
|
+
invocation = describe_existing_entrypoint("utility", "generate-bootstrap-prompt", paths=paths)
|
|
373
|
+
|
|
374
|
+
assert invocation.script_path == PIPELINE_ROOT / "scripts" / "generate-bootstrap-prompt.py"
|
|
375
|
+
assert invocation.arguments == ()
|
|
376
|
+
assert invocation.cwd == paths.project_root
|
|
377
|
+
|
|
378
|
+
help_result = subprocess.run(
|
|
379
|
+
[sys.executable, str(invocation.script_path), "--help"],
|
|
380
|
+
cwd=REPO_ROOT,
|
|
381
|
+
text=True,
|
|
382
|
+
stdout=subprocess.PIPE,
|
|
383
|
+
stderr=subprocess.PIPE,
|
|
384
|
+
check=False,
|
|
385
|
+
)
|
|
386
|
+
assert help_result.returncode == 0
|
|
387
|
+
assert "--feature-list" in help_result.stdout
|
|
388
|
+
|
|
389
|
+
actual_scripts = {path.name for path in (PIPELINE_ROOT / "scripts").glob("*.py")}
|
|
390
|
+
mapped_scripts = {Path(relative_path).name for relative_path in UTILITY_ENTRYPOINTS.values()}
|
|
391
|
+
explicitly_library_only = set(UTILITY_LIBRARY_ONLY)
|
|
392
|
+
|
|
393
|
+
assert actual_scripts == mapped_scripts | explicitly_library_only
|
|
394
|
+
assert explicitly_library_only == {"continuation.py", "utils.py"}
|
|
395
|
+
assert "parse-stream-progress.py" in mapped_scripts
|
|
396
|
+
assert "prizmkit-test-gate.py" in mapped_scripts
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def test_boundary_dataclasses_are_cohesive_and_frozen():
|
|
400
|
+
from prizmkit_runtime.gitops import BranchContext, WorktreePolicy
|
|
401
|
+
from prizmkit_runtime.heartbeat import HeartbeatConfig, HeartbeatState
|
|
402
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
403
|
+
from prizmkit_runtime.processes import ProcessHandle, ProcessSpec
|
|
404
|
+
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
405
|
+
from prizmkit_runtime.status import StatusTarget, StatusUpdateRequest, default_status_targets
|
|
406
|
+
|
|
407
|
+
paths = resolve_runtime_paths(pipeline_root=PIPELINE_ROOT)
|
|
408
|
+
session = AISessionConfig(
|
|
409
|
+
cli_command="claude",
|
|
410
|
+
platform="claude",
|
|
411
|
+
model="claude-sonnet-4.6",
|
|
412
|
+
prompt_path=paths.state_dir / "prompt.md",
|
|
413
|
+
cwd=paths.project_root,
|
|
414
|
+
)
|
|
415
|
+
process = ProcessSpec(command=("python3", "-m", "prizmkit_runtime"), cwd=paths.project_root)
|
|
416
|
+
handle = ProcessHandle(pid=1234, spec=process, status="described")
|
|
417
|
+
heartbeat = HeartbeatConfig(heartbeat_file=paths.state_dir / ".heartbeat")
|
|
418
|
+
heartbeat_state = HeartbeatState(last_seen_epoch=1.0, stale=False, details="fresh")
|
|
419
|
+
branch = BranchContext(base_branch="main", working_branch="dev/example", project_root=paths.project_root)
|
|
420
|
+
policy = WorktreePolicy(use_worktree=True, cleanup_on_success=False, preserve_on_failure=True)
|
|
421
|
+
targets = default_status_targets(paths)
|
|
422
|
+
status_target = StatusTarget(kind="feature", plan_path=paths.feature_plan, state_dir=paths.feature_state_dir)
|
|
423
|
+
status_request = StatusUpdateRequest(target=status_target, action="status")
|
|
424
|
+
|
|
425
|
+
assert AISessionLauncher(session).describe().startswith("AI session launch boundary")
|
|
426
|
+
assert handle.spec == process
|
|
427
|
+
assert heartbeat_state.details == "fresh"
|
|
428
|
+
assert branch.project_root == paths.project_root
|
|
429
|
+
assert policy.preserve_on_failure is True
|
|
430
|
+
assert targets["feature"].plan_path == paths.feature_plan
|
|
431
|
+
assert status_request.target == status_target
|
|
432
|
+
|
|
433
|
+
with pytest.raises(FrozenInstanceError):
|
|
434
|
+
session.model = "other"
|
|
435
|
+
with pytest.raises(FrozenInstanceError):
|
|
436
|
+
heartbeat.interval_seconds = 10
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_runtime_config_loads_prizmkit_env_and_validates_effort(tmp_path):
|
|
440
|
+
from prizmkit_runtime.config import load_runtime_config, validate_effort
|
|
441
|
+
from prizmkit_runtime.paths import resolve_runtime_paths
|
|
442
|
+
|
|
443
|
+
project = tmp_path / "project"
|
|
444
|
+
pipeline = project / ".prizmkit" / "dev-pipeline"
|
|
445
|
+
pipeline.mkdir(parents=True)
|
|
446
|
+
prizmkit_dir = project / ".prizmkit"
|
|
447
|
+
(prizmkit_dir / ".env").write_text(
|
|
448
|
+
"AI_CLI=env-claude\nPRIZMKIT_EFFORT=max\nMODEL=env-model\n",
|
|
449
|
+
encoding="utf-8",
|
|
450
|
+
)
|
|
451
|
+
paths = resolve_runtime_paths(pipeline_root=pipeline)
|
|
452
|
+
|
|
453
|
+
config = load_runtime_config(paths, env={"PATH": ""})
|
|
454
|
+
|
|
455
|
+
assert config.ai_client.command == "env-claude"
|
|
456
|
+
assert config.ai_client.platform == "claude"
|
|
457
|
+
assert config.ai_client.source == "environment:AI_CLI"
|
|
458
|
+
assert config.model == "env-model"
|
|
459
|
+
assert config.effort_validation.valid is True
|
|
460
|
+
assert validate_effort("max", "codex").valid is False
|
|
461
|
+
assert "Codex" in validate_effort("max", "codex").message
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def test_stream_json_support_detection_matches_provider_rules():
|
|
465
|
+
from prizmkit_runtime.sessions import detect_stream_json_support
|
|
466
|
+
|
|
467
|
+
def help_runner(command):
|
|
468
|
+
if tuple(command) == ("codex", "exec", "--help"):
|
|
469
|
+
return "Usage: codex exec --json"
|
|
470
|
+
if tuple(command) == ("claude", "--help"):
|
|
471
|
+
return "--output-format stream-json"
|
|
472
|
+
return ""
|
|
473
|
+
|
|
474
|
+
codebuddy = detect_stream_json_support("cbc", "codebuddy", help_runner)
|
|
475
|
+
codex = detect_stream_json_support("codex", "codex", help_runner)
|
|
476
|
+
claude = detect_stream_json_support("claude", "claude", help_runner)
|
|
477
|
+
unsupported = detect_stream_json_support("other", "claude", lambda _cmd: "plain help")
|
|
478
|
+
|
|
479
|
+
assert codebuddy.enabled and codebuddy.format == "stream-json"
|
|
480
|
+
assert codex.enabled and codex.format == "codex-json"
|
|
481
|
+
assert claude.enabled and claude.format == "stream-json"
|
|
482
|
+
assert unsupported.enabled is False
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def test_ai_session_command_builders_match_unix_runtime_snapshot(tmp_path):
|
|
486
|
+
from prizmkit_runtime.sessions import AISessionConfig, build_ai_session_command
|
|
487
|
+
|
|
488
|
+
snapshot = json.loads((REPO_ROOT / "tests" / "fixtures" / "dev-pipeline-behavior-snapshots" / "unix-runtime.json").read_text(encoding="utf-8"))
|
|
489
|
+
prompt = tmp_path / "prompt.md"
|
|
490
|
+
prompt.write_text("hello", encoding="utf-8")
|
|
491
|
+
project = tmp_path / "project"
|
|
492
|
+
project.mkdir()
|
|
493
|
+
|
|
494
|
+
claude = build_ai_session_command(
|
|
495
|
+
AISessionConfig(
|
|
496
|
+
cli_command="claude",
|
|
497
|
+
platform="claude",
|
|
498
|
+
model="sonnet",
|
|
499
|
+
prompt_path=prompt,
|
|
500
|
+
cwd=project,
|
|
501
|
+
effort="high",
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
expected_claude = tuple(snapshot["aiCliCommands"]["claudeCommand"]["argv"])
|
|
505
|
+
assert claude.argv[1:] == expected_claude
|
|
506
|
+
assert claude.stdin_text is None
|
|
507
|
+
|
|
508
|
+
codex = build_ai_session_command(
|
|
509
|
+
AISessionConfig(
|
|
510
|
+
cli_command="codex",
|
|
511
|
+
platform="codex",
|
|
512
|
+
model="codex-model",
|
|
513
|
+
prompt_path=prompt,
|
|
514
|
+
cwd=project,
|
|
515
|
+
effort="xhigh",
|
|
516
|
+
use_stream_json=True,
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
expected_prefix = tuple(snapshot["aiCliCommands"]["codexCommand"]["argvPrefix"])
|
|
520
|
+
assert codex.argv[1 : 1 + len(expected_prefix)] == expected_prefix
|
|
521
|
+
assert "--cd" in codex.argv
|
|
522
|
+
assert str(project) in codex.argv
|
|
523
|
+
assert "--json" in codex.argv
|
|
524
|
+
effort_index = codex.argv.index("model_reasoning_effort=xhigh")
|
|
525
|
+
assert codex.argv[effort_index - 1] == "--config"
|
|
526
|
+
assert codex.argv[-1] == "-"
|
|
527
|
+
assert codex.stdin_text == "hello"
|
|
528
|
+
|
|
529
|
+
codebuddy = build_ai_session_command(
|
|
530
|
+
AISessionConfig(
|
|
531
|
+
cli_command="custom-cbc",
|
|
532
|
+
platform="codebuddy",
|
|
533
|
+
model="codebuddy-model",
|
|
534
|
+
prompt_path=prompt,
|
|
535
|
+
cwd=project,
|
|
536
|
+
effort="medium",
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
expected_codebuddy = tuple(snapshot["aiCliCommands"]["customWrapperCommand"]["argv"])
|
|
540
|
+
assert codebuddy.argv[1:] == expected_codebuddy
|
|
541
|
+
assert codebuddy.stdin_text == "hello"
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def test_progress_parser_adds_required_metadata(tmp_path):
|
|
545
|
+
import importlib.util
|
|
546
|
+
|
|
547
|
+
parser_path = PIPELINE_ROOT / "scripts" / "parse-stream-progress.py"
|
|
548
|
+
spec = importlib.util.spec_from_file_location("parse_stream_progress", parser_path)
|
|
549
|
+
module = importlib.util.module_from_spec(spec)
|
|
550
|
+
assert spec.loader is not None
|
|
551
|
+
spec.loader.exec_module(module)
|
|
552
|
+
|
|
553
|
+
tracker = module.ProgressTracker()
|
|
554
|
+
tracker.process_event({"type": "system", "subtype": "init", "session_id": "claude-session", "cwd": str(tmp_path)})
|
|
555
|
+
tracker.process_event({"type": "assistant", "message": {"content": [{"type": "text", "text": "working"}]}})
|
|
556
|
+
tracker.process_event({"type": "result", "subtype": "success", "stop_reason": "end_turn", "usage": {"input_tokens": 1}, "result": "done"})
|
|
557
|
+
tracker.process_event({"type": "error", "error": {"message": "permission denied by user"}})
|
|
558
|
+
data = tracker.to_dict()
|
|
559
|
+
|
|
560
|
+
assert data["event_type_counts"]["system"] == 1
|
|
561
|
+
assert data["event_type_counts"]["assistant"] == 1
|
|
562
|
+
assert data["claude_session_id"] == "claude-session"
|
|
563
|
+
assert data["result_subtype"] == "success"
|
|
564
|
+
assert data["stop_reason"] == "end_turn"
|
|
565
|
+
assert data["has_usage"] is True
|
|
566
|
+
assert data["permission_denial_count"] == 1
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def test_session_dual_writes_progress_and_recovers_backup(tmp_path):
|
|
570
|
+
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher, recover_session_log
|
|
571
|
+
|
|
572
|
+
fake_cli = tmp_path / "fake_cli.py"
|
|
573
|
+
fake_cli.write_text(
|
|
574
|
+
"#!/usr/bin/env python3\n"
|
|
575
|
+
"import json, sys\n"
|
|
576
|
+
"print(json.dumps({'type':'system','subtype':'init','session_id':'s1'}), flush=True)\n"
|
|
577
|
+
"print(json.dumps({'type':'result','subtype':'success','stop_reason':'end_turn','usage':{'tokens':1},'result':'ok'}), flush=True)\n",
|
|
578
|
+
encoding="utf-8",
|
|
579
|
+
)
|
|
580
|
+
fake_cli.chmod(0o755)
|
|
581
|
+
prompt = tmp_path / "prompt.md"
|
|
582
|
+
prompt.write_text("hello", encoding="utf-8")
|
|
583
|
+
session_log = tmp_path / "state" / "sessions" / "sess-1" / "logs" / "session.log"
|
|
584
|
+
backup_log = tmp_path / "home" / ".prizmkit" / "session-backups" / "sess-1.log"
|
|
585
|
+
progress = session_log.with_name("progress.json")
|
|
586
|
+
|
|
587
|
+
result = AISessionLauncher(
|
|
588
|
+
AISessionConfig(
|
|
589
|
+
cli_command=str(fake_cli),
|
|
590
|
+
platform="codebuddy",
|
|
591
|
+
model=None,
|
|
592
|
+
prompt_path=prompt,
|
|
593
|
+
cwd=tmp_path,
|
|
594
|
+
log_path=session_log,
|
|
595
|
+
backup_log_path=backup_log,
|
|
596
|
+
progress_path=progress,
|
|
597
|
+
)
|
|
598
|
+
).run()
|
|
599
|
+
|
|
600
|
+
assert result.exit_code == 0
|
|
601
|
+
assert session_log.exists()
|
|
602
|
+
assert progress.exists()
|
|
603
|
+
progress_data = json.loads(progress.read_text(encoding="utf-8"))
|
|
604
|
+
assert progress_data["event_type_counts"]["result"] == 1
|
|
605
|
+
assert progress_data["claude_session_id"] == "s1"
|
|
606
|
+
assert progress_data["has_usage"] is True
|
|
607
|
+
assert result.log_recovery is not None
|
|
608
|
+
|
|
609
|
+
second_backup = tmp_path / "backup.log"
|
|
610
|
+
second_log = tmp_path / "main.log"
|
|
611
|
+
second_log.write_text("tiny", encoding="utf-8")
|
|
612
|
+
second_backup.write_text("larger recovered log", encoding="utf-8")
|
|
613
|
+
recovery = recover_session_log(second_log, second_backup)
|
|
614
|
+
assert recovery.recovered is True
|
|
615
|
+
assert recovery.cleaned_backup is True
|
|
616
|
+
assert not second_backup.exists()
|
|
617
|
+
assert second_log.read_text(encoding="utf-8") == "larger recovered log"
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def test_session_final_progress_merge_preserves_tracker_fatal_error(tmp_path):
|
|
621
|
+
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
622
|
+
|
|
623
|
+
fake_cli = tmp_path / "context_cli.py"
|
|
624
|
+
fake_cli.write_text(
|
|
625
|
+
"#!/usr/bin/env python3\n"
|
|
626
|
+
"import json\n"
|
|
627
|
+
"print(json.dumps({'type':'result','is_error':True,'result':'API Error: Your input exceeds the context window'}), flush=True)\n",
|
|
628
|
+
encoding="utf-8",
|
|
629
|
+
)
|
|
630
|
+
fake_cli.chmod(0o755)
|
|
631
|
+
prompt = tmp_path / "prompt.md"
|
|
632
|
+
prompt.write_text("hello", encoding="utf-8")
|
|
633
|
+
session_log = tmp_path / "state" / "sessions" / "sess-context" / "logs" / "session.log"
|
|
634
|
+
progress = session_log.with_name("progress.json")
|
|
635
|
+
|
|
636
|
+
result = AISessionLauncher(
|
|
637
|
+
AISessionConfig(
|
|
638
|
+
cli_command=str(fake_cli),
|
|
639
|
+
platform="codebuddy",
|
|
640
|
+
model=None,
|
|
641
|
+
prompt_path=prompt,
|
|
642
|
+
cwd=tmp_path,
|
|
643
|
+
log_path=session_log,
|
|
644
|
+
progress_path=progress,
|
|
645
|
+
)
|
|
646
|
+
).run()
|
|
647
|
+
|
|
648
|
+
progress_data = json.loads(progress.read_text(encoding="utf-8"))
|
|
649
|
+
assert result.progress_summary.fatal_error_code == "context_overflow"
|
|
650
|
+
assert result.fatal_error_code == "context_overflow"
|
|
651
|
+
assert progress_data["fatal_error_code"] == "context_overflow"
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def test_session_unsets_claudecode_and_merges_stderr_order(tmp_path, monkeypatch):
|
|
655
|
+
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|
|
656
|
+
|
|
657
|
+
fake_cli = tmp_path / "env_cli.py"
|
|
658
|
+
fake_cli.write_text(
|
|
659
|
+
"#!/usr/bin/env python3\n"
|
|
660
|
+
"import os, sys\n"
|
|
661
|
+
"print('stdout-before', flush=True)\n"
|
|
662
|
+
"print('claudecode=' + os.environ.get('CLAUDECODE', '<unset>'), flush=True)\n"
|
|
663
|
+
"print('stderr-after', file=sys.stderr, flush=True)\n",
|
|
664
|
+
encoding="utf-8",
|
|
665
|
+
)
|
|
666
|
+
fake_cli.chmod(0o755)
|
|
667
|
+
monkeypatch.setenv("CLAUDECODE", "1")
|
|
668
|
+
prompt = tmp_path / "prompt.md"
|
|
669
|
+
prompt.write_text("hello", encoding="utf-8")
|
|
670
|
+
session_log = tmp_path / "state" / "sessions" / "sess-env" / "logs" / "session.log"
|
|
671
|
+
|
|
672
|
+
result = AISessionLauncher(
|
|
673
|
+
AISessionConfig(
|
|
674
|
+
cli_command=str(fake_cli),
|
|
675
|
+
platform="codebuddy",
|
|
676
|
+
model=None,
|
|
677
|
+
prompt_path=prompt,
|
|
678
|
+
cwd=tmp_path,
|
|
679
|
+
log_path=session_log,
|
|
680
|
+
)
|
|
681
|
+
).run()
|
|
682
|
+
|
|
683
|
+
text = session_log.read_text(encoding="utf-8")
|
|
684
|
+
assert result.exit_code == 0
|
|
685
|
+
assert "claudecode=<unset>" in text
|
|
686
|
+
assert text.index("stdout-before") < text.index("stderr-after")
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def test_heartbeat_fatal_error_writes_shell_compatible_markers(tmp_path):
|
|
690
|
+
from prizmkit_runtime.heartbeat import HeartbeatConfig, HeartbeatMonitor
|
|
691
|
+
from prizmkit_runtime.processes import ProcessSpec, launch_process
|
|
692
|
+
|
|
693
|
+
sleeper = tmp_path / "fatal_sleep.py"
|
|
694
|
+
sleeper.write_text("import time\ntime.sleep(5)\n", encoding="utf-8")
|
|
695
|
+
progress = tmp_path / "progress.json"
|
|
696
|
+
progress.write_text('{"fatal_error_code":"context_overflow"}\n', encoding="utf-8")
|
|
697
|
+
handle = launch_process(ProcessSpec(command=(sys.executable, str(sleeper)), cwd=tmp_path))
|
|
698
|
+
monitor = HeartbeatMonitor(
|
|
699
|
+
handle,
|
|
700
|
+
HeartbeatConfig(
|
|
701
|
+
heartbeat_file=tmp_path / "heartbeat.json",
|
|
702
|
+
session_log=tmp_path / "session.log",
|
|
703
|
+
progress_file=progress,
|
|
704
|
+
marker_dir=tmp_path,
|
|
705
|
+
interval_seconds=0.05,
|
|
706
|
+
stale_after_seconds=10,
|
|
707
|
+
kill_after_seconds=10,
|
|
708
|
+
grace_seconds=0.1,
|
|
709
|
+
),
|
|
710
|
+
)
|
|
711
|
+
monitor.start()
|
|
712
|
+
deadline = time.time() + 3
|
|
713
|
+
while time.time() < deadline and not (tmp_path / "fatal-error.json").exists():
|
|
714
|
+
time.sleep(0.05)
|
|
715
|
+
result = monitor.stop()
|
|
716
|
+
|
|
717
|
+
fatal = json.loads((tmp_path / "fatal-error.json").read_text(encoding="utf-8"))
|
|
718
|
+
stale = json.loads((tmp_path / "stale-kill.json").read_text(encoding="utf-8"))
|
|
719
|
+
assert result.killed is True
|
|
720
|
+
assert result.reason == "context_overflow"
|
|
721
|
+
assert fatal["killed_at"].endswith("Z")
|
|
722
|
+
assert fatal["fatal_error_code"] == "context_overflow"
|
|
723
|
+
assert stale["reason"] == "context_overflow"
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
from prizmkit_runtime.heartbeat import HeartbeatConfig, write_stale_marker
|
|
728
|
+
from prizmkit_runtime.processes import ProcessSpec, platform_termination_strategy, run_process_with_capture
|
|
729
|
+
|
|
730
|
+
sleeper = tmp_path / "sleep.py"
|
|
731
|
+
sleeper.write_text("import time\ntime.sleep(5)\n", encoding="utf-8")
|
|
732
|
+
result = run_process_with_capture(
|
|
733
|
+
ProcessSpec(command=(sys.executable, str(sleeper)), cwd=tmp_path),
|
|
734
|
+
timeout_seconds=0.2,
|
|
735
|
+
grace_seconds=0.1,
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
assert result.timed_out is True
|
|
739
|
+
assert result.termination is not None
|
|
740
|
+
assert result.termination.reason == "timeout"
|
|
741
|
+
assert platform_termination_strategy()
|
|
742
|
+
|
|
743
|
+
marker = write_stale_marker(tmp_path, reason="stale_session", stale_seconds=3, threshold=2)
|
|
744
|
+
marker_data = json.loads(marker.read_text(encoding="utf-8"))
|
|
745
|
+
assert marker_data["reason"] == "stale_session"
|
|
746
|
+
assert marker_data["threshold"] == 2
|
|
747
|
+
assert HeartbeatConfig(heartbeat_file=tmp_path / "heartbeat").interval_seconds == 30
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def test_git_branch_worktree_plans_preserve_shell_invariants(tmp_path):
|
|
751
|
+
from prizmkit_runtime.gitops import (
|
|
752
|
+
BranchContext,
|
|
753
|
+
GitCommandResult,
|
|
754
|
+
GitPlanResult,
|
|
755
|
+
WorktreePolicy,
|
|
756
|
+
branch_create_plan,
|
|
757
|
+
branch_ensure_return_plan,
|
|
758
|
+
branch_merge_plan,
|
|
759
|
+
branch_save_wip,
|
|
760
|
+
branch_save_wip_plan,
|
|
761
|
+
worktree_add_plan,
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
context = BranchContext(base_branch="main", working_branch="dev/example", project_root=tmp_path)
|
|
765
|
+
create = branch_create_plan(context)
|
|
766
|
+
merge = branch_merge_plan(context, auto_push=True)
|
|
767
|
+
save_wip = branch_save_wip_plan(context)
|
|
768
|
+
ensure = branch_ensure_return_plan(context)
|
|
769
|
+
worktree = worktree_add_plan(context, WorktreePolicy(True, cleanup_on_success=False, preserve_on_failure=True))
|
|
770
|
+
|
|
771
|
+
assert create.name == "branch_create"
|
|
772
|
+
assert any(command.args[:2] == ("checkout", "-b") for command in create.commands)
|
|
773
|
+
assert any(command.args[:2] == ("stash", "push") for command in merge.commands)
|
|
774
|
+
assert any(command.args[:2] == ("merge", "--ff-only") for command in merge.commands)
|
|
775
|
+
assert any(command.args == ("push",) for command in merge.commands)
|
|
776
|
+
assert save_wip.never_fails is True
|
|
777
|
+
assert ensure.never_fails is True
|
|
778
|
+
assert any(command.args[:2] == ("rebase", "--abort") for command in ensure.commands)
|
|
779
|
+
assert any(command.args[:2] == ("add", "-A") for command in ensure.commands)
|
|
780
|
+
assert any(".*/worktrees/**" in " ".join(command.args) for command in save_wip.commands)
|
|
781
|
+
assert worktree.commands[0].args[:2] == ("worktree", "add")
|
|
782
|
+
|
|
783
|
+
allowed_failure_result = GitPlanResult(
|
|
784
|
+
plan=create,
|
|
785
|
+
results=(
|
|
786
|
+
GitCommandResult(create.commands[0].args, 1, "", "missing"),
|
|
787
|
+
GitCommandResult(create.commands[1].args, 0, "", ""),
|
|
788
|
+
),
|
|
789
|
+
)
|
|
790
|
+
assert allowed_failure_result.ok is True
|
|
791
|
+
|
|
792
|
+
failed_branch_change = GitPlanResult(
|
|
793
|
+
plan=create,
|
|
794
|
+
results=(
|
|
795
|
+
GitCommandResult(create.commands[0].args, 1, "", "missing"),
|
|
796
|
+
GitCommandResult(create.commands[1].args, 1, "", "bad base"),
|
|
797
|
+
GitCommandResult(create.commands[2].args, 1, "", "missing branch"),
|
|
798
|
+
),
|
|
799
|
+
)
|
|
800
|
+
assert failed_branch_change.ok is False
|
|
801
|
+
|
|
802
|
+
skipped_wip = branch_save_wip(tmp_path, "dev/example")
|
|
803
|
+
assert skipped_wip.ok is True
|
|
804
|
+
assert skipped_wip.results == ()
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def test_worktree_runtime_context_uses_safe_deterministic_path(tmp_path):
|
|
808
|
+
from prizmkit_runtime.gitops import BranchContext, WorktreePolicy, worktree_runtime_context
|
|
809
|
+
|
|
810
|
+
context = BranchContext(base_branch="main", working_branch="dev/F-001-example", project_root=tmp_path / "repo")
|
|
811
|
+
policy = WorktreePolicy(
|
|
812
|
+
use_worktree=True,
|
|
813
|
+
cleanup_on_success=True,
|
|
814
|
+
preserve_on_failure=False,
|
|
815
|
+
worktree_root=tmp_path / "state" / "worktrees",
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
runtime = worktree_runtime_context(context, policy)
|
|
819
|
+
|
|
820
|
+
assert runtime.worktree_root == (tmp_path / "state" / "worktrees").resolve()
|
|
821
|
+
assert runtime.worktree_path == runtime.worktree_root / "dev-F-001-example"
|
|
822
|
+
assert runtime.sentinel_path == runtime.worktree_path / ".prizmkit-worktree"
|
|
823
|
+
assert runtime.is_cleanup_path_safe() is True
|
|
824
|
+
|
|
825
|
+
unsafe = runtime.__class__(
|
|
826
|
+
context=context,
|
|
827
|
+
policy=policy,
|
|
828
|
+
worktree_root=runtime.worktree_root,
|
|
829
|
+
worktree_path=tmp_path / "other" / "dev-F-001-example",
|
|
830
|
+
sentinel_path=tmp_path / "other" / "dev-F-001-example" / ".prizmkit-worktree",
|
|
831
|
+
)
|
|
832
|
+
assert unsafe.is_cleanup_path_safe() is False
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def test_linked_worktree_plans_create_branch_without_checkout_and_cleanup_after_merge(tmp_path):
|
|
836
|
+
from prizmkit_runtime.gitops import (
|
|
837
|
+
BranchContext,
|
|
838
|
+
WorktreePolicy,
|
|
839
|
+
linked_worktree_setup_plan,
|
|
840
|
+
worktree_merge_plan,
|
|
841
|
+
worktree_remove_plan,
|
|
842
|
+
worktree_runtime_context,
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
context = BranchContext(base_branch="main", working_branch="dev/F-001-example", project_root=tmp_path / "repo")
|
|
846
|
+
policy = WorktreePolicy(True, cleanup_on_success=True, preserve_on_failure=False, worktree_root=tmp_path / "worktrees")
|
|
847
|
+
runtime = worktree_runtime_context(context, policy)
|
|
848
|
+
|
|
849
|
+
setup = linked_worktree_setup_plan(runtime)
|
|
850
|
+
merge = worktree_merge_plan(runtime, auto_push=True)
|
|
851
|
+
remove = worktree_remove_plan(runtime, delete_branch=True)
|
|
852
|
+
|
|
853
|
+
assert setup.name == "linked_worktree_setup"
|
|
854
|
+
assert setup.commands[0].args[:2] == ("rev-parse", "--verify")
|
|
855
|
+
assert setup.commands[1].args[:4] == ("branch", context.working_branch, context.base_branch,)
|
|
856
|
+
assert setup.commands[2].args[:2] == ("worktree", "list")
|
|
857
|
+
assert setup.commands[3].args[:3] == ("worktree", "add", "--checkout")
|
|
858
|
+
assert all(command.args[0] != "checkout" for command in setup.commands)
|
|
859
|
+
assert setup.require_one_success == ((0, 1),)
|
|
860
|
+
|
|
861
|
+
assert merge.commands[0].args[:2] == ("rebase", context.base_branch)
|
|
862
|
+
assert merge.commands[1].args == ("merge", "--ff-only", context.working_branch)
|
|
863
|
+
assert merge.commands[2].args == ("push",)
|
|
864
|
+
assert remove.commands[0].args[:2] == ("worktree", "remove")
|
|
865
|
+
assert remove.commands[1].args == ("branch", "-d", context.working_branch)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def test_guarded_worktree_removal_refuses_unregistered_or_unsafe_paths(monkeypatch, tmp_path):
|
|
869
|
+
from prizmkit_runtime.gitops import (
|
|
870
|
+
BranchContext,
|
|
871
|
+
GitCommandResult,
|
|
872
|
+
WorktreePolicy,
|
|
873
|
+
guarded_worktree_remove,
|
|
874
|
+
worktree_runtime_context,
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
context = BranchContext(base_branch="main", working_branch="dev/F-001-example", project_root=tmp_path / "repo")
|
|
878
|
+
runtime = worktree_runtime_context(
|
|
879
|
+
context,
|
|
880
|
+
WorktreePolicy(True, cleanup_on_success=True, preserve_on_failure=False, worktree_root=tmp_path / "worktrees"),
|
|
881
|
+
)
|
|
882
|
+
runtime.worktree_path.mkdir(parents=True)
|
|
883
|
+
|
|
884
|
+
calls = []
|
|
885
|
+
|
|
886
|
+
def fake_run(project_root, args):
|
|
887
|
+
calls.append(tuple(args))
|
|
888
|
+
if tuple(args)[:2] == ("worktree", "list"):
|
|
889
|
+
return GitCommandResult(tuple(args), 0, "", "")
|
|
890
|
+
return GitCommandResult(tuple(args), 0, "", "")
|
|
891
|
+
|
|
892
|
+
monkeypatch.setattr("prizmkit_runtime.gitops.run_git_command", fake_run)
|
|
893
|
+
result = guarded_worktree_remove(runtime, delete_branch=True)
|
|
894
|
+
|
|
895
|
+
assert result.ok is False
|
|
896
|
+
assert result.reason == "missing_worktree_sentinel"
|
|
897
|
+
assert all(call[:2] != ("worktree", "remove") for call in calls)
|
|
898
|
+
|
|
899
|
+
(runtime.sentinel_path).write_text(context.working_branch, encoding="utf-8")
|
|
900
|
+
calls.clear()
|
|
901
|
+
result = guarded_worktree_remove(runtime, delete_branch=True)
|
|
902
|
+
|
|
903
|
+
assert result.ok is False
|
|
904
|
+
assert result.reason == "worktree_not_registered"
|
|
905
|
+
assert all(call[:2] != ("worktree", "remove") for call in calls)
|
|
906
|
+
calls.clear()
|
|
907
|
+
|
|
908
|
+
def registered_run(project_root, args):
|
|
909
|
+
calls.append(tuple(args))
|
|
910
|
+
if tuple(args)[:2] == ("worktree", "list"):
|
|
911
|
+
return GitCommandResult(tuple(args), 0, str(runtime.worktree_path) + " abc [dev/F-001-example]\n", "")
|
|
912
|
+
return GitCommandResult(tuple(args), 0, "", "")
|
|
913
|
+
|
|
914
|
+
monkeypatch.setattr("prizmkit_runtime.gitops.run_git_command", registered_run)
|
|
915
|
+
result = guarded_worktree_remove(runtime, delete_branch=True)
|
|
916
|
+
|
|
917
|
+
assert result.ok is True
|
|
918
|
+
assert ("worktree", "remove", "--force", str(runtime.worktree_path)) in calls
|
|
919
|
+
assert ("branch", "-d", context.working_branch) in calls
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def test_git_status_safe_ignores_worktree_support_assets(monkeypatch, tmp_path):
|
|
924
|
+
from prizmkit_runtime.gitops import HIDDEN_TOOL_WORKTREE_EXCLUDES, git_status_safe
|
|
925
|
+
|
|
926
|
+
captured = []
|
|
927
|
+
|
|
928
|
+
def fake_run(project_root, args):
|
|
929
|
+
captured.append(tuple(args))
|
|
930
|
+
return type("Result", (), {"return_code": 0, "stdout": ""})()
|
|
931
|
+
|
|
932
|
+
monkeypatch.setattr("prizmkit_runtime.gitops.run_git_command", fake_run)
|
|
933
|
+
|
|
934
|
+
assert git_status_safe(tmp_path) == ""
|
|
935
|
+
status_args = captured[0]
|
|
936
|
+
joined = " ".join(status_args)
|
|
937
|
+
assert ":(top,exclude).prizmkit-worktree" in status_args
|
|
938
|
+
assert ":(top,exclude).claude" in status_args
|
|
939
|
+
assert ":(top,exclude).prizmkit/dev-pipeline/scripts" in status_args
|
|
940
|
+
assert all(exclude in status_args for exclude in HIDDEN_TOOL_WORKTREE_EXCLUDES)
|
|
941
|
+
assert ".prizmkit-worktree" in joined
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _init_temp_repo(repo):
|
|
945
|
+
repo.mkdir(parents=True, exist_ok=True)
|
|
946
|
+
subprocess.run(["git", "init", "-b", "main"], cwd=repo, check=True, stdout=subprocess.DEVNULL)
|
|
947
|
+
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=repo, check=True)
|
|
948
|
+
subprocess.run(["git", "config", "user.name", "Test"], cwd=repo, check=True)
|
|
949
|
+
(repo / "base.txt").write_text("base\n", encoding="utf-8")
|
|
950
|
+
subprocess.run(["git", "add", "base.txt"], cwd=repo, check=True)
|
|
951
|
+
subprocess.run(["git", "commit", "-m", "base"], cwd=repo, check=True, stdout=subprocess.DEVNULL)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def test_materialize_worktree_support_assets_copies_ignored_assets_without_retired_shell_sources(tmp_path):
|
|
955
|
+
from prizmkit_runtime.gitops import (
|
|
956
|
+
BranchContext,
|
|
957
|
+
WorktreePolicy,
|
|
958
|
+
ensure_linked_worktree,
|
|
959
|
+
guarded_worktree_remove,
|
|
960
|
+
materialize_worktree_support_assets,
|
|
961
|
+
worktree_runtime_context,
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
repo = tmp_path / "repo"
|
|
965
|
+
_init_temp_repo(repo)
|
|
966
|
+
(repo / ".gitignore").write_text(".claude/\n.prizmkit/\n", encoding="utf-8")
|
|
967
|
+
subprocess.run(["git", "add", ".gitignore"], cwd=repo, check=True)
|
|
968
|
+
subprocess.run(["git", "commit", "-m", "ignore support"], cwd=repo, check=True, stdout=subprocess.DEVNULL)
|
|
969
|
+
|
|
970
|
+
(repo / ".claude" / "agents").mkdir(parents=True)
|
|
971
|
+
(repo / ".claude" / "agents" / "prizm-dev-team-dev.md").write_text("dev", encoding="utf-8")
|
|
972
|
+
(repo / ".prizmkit" / "prizm-docs").mkdir(parents=True)
|
|
973
|
+
(repo / ".prizmkit" / "prizm-docs" / "root.prizm").write_text("PRIZM_VERSION: 1\n", encoding="utf-8")
|
|
974
|
+
(repo / ".prizmkit" / "config.json").write_text("{}", encoding="utf-8")
|
|
975
|
+
(repo / ".prizmkit" / "manifest.json").write_text("{}", encoding="utf-8")
|
|
976
|
+
(repo / ".prizmkit" / "dev-pipeline" / "scripts").mkdir(parents=True)
|
|
977
|
+
(repo / ".prizmkit" / "dev-pipeline" / "scripts" / "update-checkpoint.py").write_text("# update", encoding="utf-8")
|
|
978
|
+
(repo / ".prizmkit" / "dev-pipeline" / "run-feature.sh").write_text("retired", encoding="utf-8")
|
|
979
|
+
|
|
980
|
+
runtime = worktree_runtime_context(
|
|
981
|
+
BranchContext("main", "dev/support-assets", repo),
|
|
982
|
+
WorktreePolicy(True, cleanup_on_success=True, preserve_on_failure=True, worktree_root=tmp_path / "worktrees"),
|
|
983
|
+
)
|
|
984
|
+
setup = ensure_linked_worktree(runtime)
|
|
985
|
+
assert setup.ok
|
|
986
|
+
assert not (runtime.worktree_path / ".claude" / "agents" / "prizm-dev-team-dev.md").exists()
|
|
987
|
+
|
|
988
|
+
result = materialize_worktree_support_assets(runtime)
|
|
989
|
+
|
|
990
|
+
assert result.ok
|
|
991
|
+
assert (runtime.worktree_path / ".claude" / "agents" / "prizm-dev-team-dev.md").is_file()
|
|
992
|
+
assert (runtime.worktree_path / ".prizmkit" / "prizm-docs" / "root.prizm").is_file()
|
|
993
|
+
assert (runtime.worktree_path / ".prizmkit" / "config.json").is_file()
|
|
994
|
+
assert (runtime.worktree_path / ".prizmkit" / "manifest.json").is_file()
|
|
995
|
+
assert (runtime.worktree_path / ".prizmkit" / "dev-pipeline" / "scripts" / "update-checkpoint.py").is_file()
|
|
996
|
+
assert not (runtime.worktree_path / ".prizmkit" / "dev-pipeline" / "run-feature.sh").exists()
|
|
997
|
+
|
|
998
|
+
assert guarded_worktree_remove(runtime, delete_branch=False).ok
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def test_worktree_terminal_failure_wip_commit_preserves_dirty_changes_before_cleanup(tmp_path):
|
|
1002
|
+
from prizmkit_runtime.gitops import (
|
|
1003
|
+
BranchContext,
|
|
1004
|
+
WorktreePolicy,
|
|
1005
|
+
ensure_linked_worktree,
|
|
1006
|
+
guarded_worktree_remove,
|
|
1007
|
+
worktree_runtime_context,
|
|
1008
|
+
worktree_save_wip,
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
repo = tmp_path / "repo"
|
|
1012
|
+
_init_temp_repo(repo)
|
|
1013
|
+
runtime = worktree_runtime_context(
|
|
1014
|
+
BranchContext("main", "dev/crashed-work", repo),
|
|
1015
|
+
WorktreePolicy(True, cleanup_on_success=True, preserve_on_failure=True, worktree_root=tmp_path / "worktrees"),
|
|
1016
|
+
)
|
|
1017
|
+
setup = ensure_linked_worktree(runtime)
|
|
1018
|
+
assert setup.ok
|
|
1019
|
+
|
|
1020
|
+
(runtime.worktree_path / "dirty.txt").write_text("dirty work\n", encoding="utf-8")
|
|
1021
|
+
save = worktree_save_wip(runtime)
|
|
1022
|
+
assert save.ok
|
|
1023
|
+
assert any(result.args and result.args[0] == "commit" and result.return_code == 0 for result in save.results)
|
|
1024
|
+
|
|
1025
|
+
removal = guarded_worktree_remove(runtime, delete_branch=False)
|
|
1026
|
+
assert removal.ok
|
|
1027
|
+
assert not runtime.worktree_path.exists()
|
|
1028
|
+
|
|
1029
|
+
branches = subprocess.run(["git", "branch", "--list", "dev/crashed-work"], cwd=repo, check=True, stdout=subprocess.PIPE, text=True).stdout
|
|
1030
|
+
assert "dev/crashed-work" in branches
|
|
1031
|
+
show = subprocess.run(["git", "show", "dev/crashed-work:dirty.txt"], cwd=repo, check=True, stdout=subprocess.PIPE, text=True).stdout
|
|
1032
|
+
assert show == "dirty work\n"
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def test_daemon_status_cli_preserves_pure_json_stdout(tmp_path):
|
|
1036
|
+
project = tmp_path / "project"
|
|
1037
|
+
(project / ".prizmkit" / "state" / "features").mkdir(parents=True)
|
|
1038
|
+
result = run_cli("--project-root", str(project), "daemon", "feature", "status")
|
|
1039
|
+
|
|
1040
|
+
assert result.returncode == 1
|
|
1041
|
+
assert json.loads(result.stdout) == {"running": False}
|
|
1042
|
+
assert "Pipeline is not running" in result.stderr
|
|
1043
|
+
assert "Python feature daemon" not in result.stdout
|
|
1044
|
+
assert "Transition command contract" not in result.stdout
|
|
1045
|
+
|