gsd-pi 2.10.12 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundled-extension-paths.d.ts +2 -0
- package/dist/bundled-extension-paths.js +10 -0
- package/dist/loader.js +45 -28
- package/dist/onboarding.js +100 -0
- package/dist/resource-loader.js +12 -3
- package/dist/resources/GSD-WORKFLOW.md +661 -0
- package/dist/resources/agents/researcher.md +29 -0
- package/dist/resources/agents/scout.md +56 -0
- package/dist/resources/agents/worker.md +31 -0
- package/dist/resources/extensions/ask-user-questions.ts +289 -0
- package/dist/resources/extensions/async-jobs/async-bash-tool.ts +211 -0
- package/dist/resources/extensions/async-jobs/await-tool.ts +101 -0
- package/dist/resources/extensions/async-jobs/cancel-job-tool.ts +34 -0
- package/dist/resources/extensions/async-jobs/index.ts +133 -0
- package/dist/resources/extensions/async-jobs/job-manager.ts +211 -0
- package/dist/resources/extensions/bg-shell/index.ts +3179 -0
- package/dist/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/dist/resources/extensions/browser-tools/capture.ts +165 -0
- package/dist/resources/extensions/browser-tools/core.d.ts +205 -0
- package/dist/resources/extensions/browser-tools/core.js +1057 -0
- package/dist/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
- package/dist/resources/extensions/browser-tools/index.ts +51 -0
- package/dist/resources/extensions/browser-tools/lifecycle.ts +270 -0
- package/dist/resources/extensions/browser-tools/package.json +24 -0
- package/dist/resources/extensions/browser-tools/refs.ts +264 -0
- package/dist/resources/extensions/browser-tools/settle.ts +197 -0
- package/dist/resources/extensions/browser-tools/state.ts +408 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
- package/dist/resources/extensions/browser-tools/tools/assertions.ts +342 -0
- package/dist/resources/extensions/browser-tools/tools/forms.ts +801 -0
- package/dist/resources/extensions/browser-tools/tools/inspection.ts +492 -0
- package/dist/resources/extensions/browser-tools/tools/intent.ts +614 -0
- package/dist/resources/extensions/browser-tools/tools/interaction.ts +865 -0
- package/dist/resources/extensions/browser-tools/tools/navigation.ts +232 -0
- package/dist/resources/extensions/browser-tools/tools/pages.ts +303 -0
- package/dist/resources/extensions/browser-tools/tools/refs.ts +541 -0
- package/dist/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
- package/dist/resources/extensions/browser-tools/tools/session.ts +400 -0
- package/dist/resources/extensions/browser-tools/tools/wait.ts +247 -0
- package/dist/resources/extensions/browser-tools/utils.ts +660 -0
- package/dist/resources/extensions/context7/index.ts +428 -0
- package/dist/resources/extensions/context7/package.json +11 -0
- package/dist/resources/extensions/get-secrets-from-user.ts +625 -0
- package/dist/resources/extensions/google-search/index.ts +336 -0
- package/dist/resources/extensions/google-search/package.json +9 -0
- package/dist/resources/extensions/gsd/activity-log.ts +116 -0
- package/dist/resources/extensions/gsd/auto.ts +3732 -0
- package/dist/resources/extensions/gsd/commands.ts +544 -0
- package/dist/resources/extensions/gsd/crash-recovery.ts +105 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +564 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +78 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +278 -0
- package/dist/resources/extensions/gsd/doctor.ts +766 -0
- package/dist/resources/extensions/gsd/exit-command.ts +18 -0
- package/dist/resources/extensions/gsd/files.ts +857 -0
- package/dist/resources/extensions/gsd/git-service.ts +904 -0
- package/dist/resources/extensions/gsd/gitignore.ts +200 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +1054 -0
- package/dist/resources/extensions/gsd/index.ts +735 -0
- package/dist/resources/extensions/gsd/metrics.ts +374 -0
- package/dist/resources/extensions/gsd/migrate/command.ts +218 -0
- package/dist/resources/extensions/gsd/migrate/index.ts +42 -0
- package/dist/resources/extensions/gsd/migrate/parser.ts +323 -0
- package/dist/resources/extensions/gsd/migrate/parsers.ts +624 -0
- package/dist/resources/extensions/gsd/migrate/preview.ts +48 -0
- package/dist/resources/extensions/gsd/migrate/transformer.ts +346 -0
- package/dist/resources/extensions/gsd/migrate/types.ts +370 -0
- package/dist/resources/extensions/gsd/migrate/validator.ts +55 -0
- package/dist/resources/extensions/gsd/migrate/writer.ts +539 -0
- package/dist/resources/extensions/gsd/native-git-bridge.ts +181 -0
- package/dist/resources/extensions/gsd/native-parser-bridge.ts +135 -0
- package/dist/resources/extensions/gsd/observability-validator.ts +408 -0
- package/dist/resources/extensions/gsd/package.json +11 -0
- package/dist/resources/extensions/gsd/paths.ts +339 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/dist/resources/extensions/gsd/preferences.ts +988 -0
- package/dist/resources/extensions/gsd/prompt-loader.ts +92 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +27 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +237 -0
- package/dist/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +63 -0
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +61 -0
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +3 -0
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +29 -0
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -0
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +13 -0
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +78 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +55 -0
- package/dist/resources/extensions/gsd/prompts/queue.md +100 -0
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +39 -0
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +37 -0
- package/dist/resources/extensions/gsd/prompts/research-slice.md +28 -0
- package/dist/resources/extensions/gsd/prompts/review-migration.md +66 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +109 -0
- package/dist/resources/extensions/gsd/prompts/system.md +190 -0
- package/dist/resources/extensions/gsd/prompts/worktree-merge.md +123 -0
- package/dist/resources/extensions/gsd/roadmap-slices.ts +50 -0
- package/dist/resources/extensions/gsd/session-forensics.ts +500 -0
- package/dist/resources/extensions/gsd/skill-discovery.ts +137 -0
- package/dist/resources/extensions/gsd/state.ts +569 -0
- package/dist/resources/extensions/gsd/templates/context.md +76 -0
- package/dist/resources/extensions/gsd/templates/decisions.md +8 -0
- package/dist/resources/extensions/gsd/templates/milestone-summary.md +73 -0
- package/dist/resources/extensions/gsd/templates/plan.md +131 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +23 -0
- package/dist/resources/extensions/gsd/templates/project.md +31 -0
- package/dist/resources/extensions/gsd/templates/reassessment.md +28 -0
- package/dist/resources/extensions/gsd/templates/requirements.md +81 -0
- package/dist/resources/extensions/gsd/templates/research.md +46 -0
- package/dist/resources/extensions/gsd/templates/roadmap.md +118 -0
- package/dist/resources/extensions/gsd/templates/secrets-manifest.md +22 -0
- package/dist/resources/extensions/gsd/templates/slice-context.md +58 -0
- package/dist/resources/extensions/gsd/templates/slice-summary.md +99 -0
- package/dist/resources/extensions/gsd/templates/state.md +19 -0
- package/dist/resources/extensions/gsd/templates/task-plan.md +52 -0
- package/dist/resources/extensions/gsd/templates/task-summary.md +57 -0
- package/dist/resources/extensions/gsd/templates/uat.md +54 -0
- package/dist/resources/extensions/gsd/tests/activity-log-prune.test.ts +297 -0
- package/dist/resources/extensions/gsd/tests/activity-log-save.test.ts +127 -0
- package/dist/resources/extensions/gsd/tests/auto-draft-pause.test.ts +109 -0
- package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +45 -0
- package/dist/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +196 -0
- package/dist/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
- package/dist/resources/extensions/gsd/tests/collect-from-manifest.test.ts +469 -0
- package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +200 -0
- package/dist/resources/extensions/gsd/tests/cost-projection.test.ts +134 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +312 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +299 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +660 -0
- package/dist/resources/extensions/gsd/tests/discuss-prompt.test.ts +27 -0
- package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +91 -0
- package/dist/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +170 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +594 -0
- package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +169 -0
- package/dist/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +1788 -0
- package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +577 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +580 -0
- package/dist/resources/extensions/gsd/tests/manifest-status.test.ts +283 -0
- package/dist/resources/extensions/gsd/tests/metrics-io.test.ts +176 -0
- package/dist/resources/extensions/gsd/tests/metrics.test.ts +188 -0
- package/dist/resources/extensions/gsd/tests/migrate-command.test.ts +369 -0
- package/dist/resources/extensions/gsd/tests/migrate-parser.test.ts +757 -0
- package/dist/resources/extensions/gsd/tests/migrate-transformer.test.ts +635 -0
- package/dist/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +414 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +297 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer.test.ts +398 -0
- package/dist/resources/extensions/gsd/tests/must-have-parser.test.ts +291 -0
- package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +54 -0
- package/dist/resources/extensions/gsd/tests/next-milestone-id.test.ts +66 -0
- package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +353 -0
- package/dist/resources/extensions/gsd/tests/parsers.test.ts +1664 -0
- package/dist/resources/extensions/gsd/tests/plan-milestone.test.ts +133 -0
- package/dist/resources/extensions/gsd/tests/plan-quality-validator.test.ts +363 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/dist/resources/extensions/gsd/tests/queue-draft-detection.test.ts +126 -0
- package/dist/resources/extensions/gsd/tests/reassess-prompt.test.ts +142 -0
- package/dist/resources/extensions/gsd/tests/regex-hardening.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
- package/dist/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +493 -0
- package/dist/resources/extensions/gsd/tests/requirements.test.ts +106 -0
- package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +35 -0
- package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
- package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +41 -0
- package/dist/resources/extensions/gsd/tests/run-uat.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/secure-env-collect.test.ts +185 -0
- package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +123 -0
- package/dist/resources/extensions/gsd/tests/test-helpers.ts +61 -0
- package/dist/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +219 -0
- package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +234 -0
- package/dist/resources/extensions/gsd/tests/workspace-index.test.ts +75 -0
- package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +234 -0
- package/dist/resources/extensions/gsd/tests/worktree-manager.test.ts +141 -0
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +597 -0
- package/dist/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
- package/dist/resources/extensions/gsd/types.ts +296 -0
- package/dist/resources/extensions/gsd/unit-runtime.ts +184 -0
- package/dist/resources/extensions/gsd/workspace-index.ts +189 -0
- package/dist/resources/extensions/gsd/worktree-command.ts +845 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +459 -0
- package/dist/resources/extensions/gsd/{worktree.js → worktree.ts} +92 -61
- package/dist/resources/extensions/mac-tools/index.ts +852 -0
- package/dist/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/dist/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/dist/resources/extensions/mcporter/index.ts +429 -0
- package/dist/resources/extensions/remote-questions/config.ts +81 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +128 -0
- package/dist/resources/extensions/remote-questions/format.ts +163 -0
- package/dist/resources/extensions/remote-questions/manager.ts +192 -0
- package/dist/resources/extensions/remote-questions/remote-command.ts +361 -0
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +92 -0
- package/dist/resources/extensions/remote-questions/status.ts +31 -0
- package/dist/resources/extensions/remote-questions/store.ts +77 -0
- package/dist/resources/extensions/remote-questions/types.ts +75 -0
- package/dist/resources/extensions/search-the-web/cache.ts +78 -0
- package/dist/resources/extensions/search-the-web/command-search-provider.ts +99 -0
- package/dist/resources/extensions/search-the-web/format.ts +258 -0
- package/dist/resources/extensions/search-the-web/http.ts +238 -0
- package/dist/resources/extensions/search-the-web/index.ts +65 -0
- package/dist/resources/extensions/search-the-web/native-search.ts +165 -0
- package/dist/resources/extensions/search-the-web/provider.ts +136 -0
- package/dist/resources/extensions/search-the-web/tavily.ts +116 -0
- package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +581 -0
- package/dist/resources/extensions/search-the-web/tool-llm-context.ts +620 -0
- package/dist/resources/extensions/search-the-web/tool-search.ts +635 -0
- package/dist/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/dist/resources/extensions/shared/bundled-extension-paths.ts +11 -0
- package/dist/resources/extensions/shared/confirm-ui.ts +126 -0
- package/dist/resources/extensions/shared/interview-ui.ts +613 -0
- package/dist/resources/extensions/shared/next-action-ui.ts +197 -0
- package/dist/resources/extensions/shared/progress-widget.ts +282 -0
- package/dist/resources/extensions/shared/terminal.ts +23 -0
- package/dist/resources/extensions/shared/thinking-widget.ts +107 -0
- package/dist/resources/extensions/shared/ui.ts +400 -0
- package/dist/resources/extensions/shared/wizard-ui.ts +551 -0
- package/dist/resources/extensions/slash-commands/audit.ts +88 -0
- package/dist/resources/extensions/slash-commands/clear.ts +10 -0
- package/dist/resources/extensions/slash-commands/create-extension.ts +297 -0
- package/dist/resources/extensions/slash-commands/create-slash-command.ts +234 -0
- package/dist/resources/extensions/slash-commands/index.ts +12 -0
- package/dist/resources/extensions/subagent/agents.ts +126 -0
- package/dist/resources/extensions/subagent/index.ts +1072 -0
- package/dist/resources/extensions/subagent/isolation.ts +498 -0
- package/dist/resources/extensions/ttsr/index.ts +163 -0
- package/dist/resources/extensions/ttsr/rule-loader.ts +121 -0
- package/dist/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
- package/dist/resources/extensions/ttsr/ttsr-manager.ts +430 -0
- package/dist/resources/extensions/universal-config/discovery.ts +78 -0
- package/dist/resources/extensions/universal-config/format.ts +160 -0
- package/dist/resources/extensions/universal-config/index.ts +118 -0
- package/dist/resources/extensions/universal-config/package.json +11 -0
- package/dist/resources/extensions/universal-config/scanners.ts +579 -0
- package/dist/resources/extensions/universal-config/tests/discovery.test.ts +111 -0
- package/dist/resources/extensions/universal-config/tests/format.test.ts +111 -0
- package/dist/resources/extensions/universal-config/tests/scanners.test.ts +438 -0
- package/dist/resources/extensions/universal-config/tools.ts +60 -0
- package/dist/resources/extensions/universal-config/types.ts +116 -0
- package/dist/resources/extensions/voice/index.ts +271 -0
- package/dist/resources/extensions/voice/speech-recognizer.py +504 -0
- package/dist/resources/extensions/voice/speech-recognizer.swift +154 -0
- package/dist/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/dist/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/dist/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/dist/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/dist/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/dist/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/dist/resources/skills/frontend-design/SKILL.md +45 -0
- package/dist/resources/skills/github-workflows/SKILL.md +92 -0
- package/dist/resources/skills/github-workflows/references/gh/SKILL.md +255 -0
- package/dist/resources/skills/github-workflows/references/gh/references/issue-stories.md +204 -0
- package/dist/resources/skills/github-workflows/references/gh/references/labels.md +170 -0
- package/dist/resources/skills/github-workflows/references/gh/references/milestones.md +158 -0
- package/dist/resources/skills/github-workflows/references/gh/references/projects-v2.md +177 -0
- package/dist/resources/skills/github-workflows/references/gh/scripts/experiment_cleanup.py +191 -0
- package/dist/resources/skills/github-workflows/references/gh/scripts/github_project_setup.py +799 -0
- package/dist/resources/skills/github-workflows/references/gh/tests/__init__.py +0 -0
- package/dist/resources/skills/github-workflows/references/gh/tests/test_github_project_setup.py +608 -0
- package/dist/resources/skills/swiftui/SKILL.md +208 -0
- package/dist/resources/skills/swiftui/references/animations.md +921 -0
- package/dist/resources/skills/swiftui/references/architecture.md +1561 -0
- package/dist/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/dist/resources/skills/swiftui/references/navigation.md +1492 -0
- package/dist/resources/skills/swiftui/references/networking-async.md +214 -0
- package/dist/resources/skills/swiftui/references/performance.md +1706 -0
- package/dist/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/dist/resources/skills/swiftui/references/state-management.md +1443 -0
- package/dist/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/dist/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/dist/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/dist/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/dist/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/dist/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/dist/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/dist/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/dist/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/wizard.js +1 -0
- package/package.json +8 -6
- package/packages/native/dist/fd/index.d.ts +1 -0
- package/packages/native/dist/fd/index.js +1 -0
- package/packages/native/dist/grep/index.d.ts +2 -1
- package/packages/native/dist/grep/index.js +1 -0
- package/packages/native/src/__tests__/fd.test.mjs +35 -0
- package/packages/native/src/__tests__/grep.test.mjs +26 -9
- package/packages/native/src/fd/index.ts +1 -0
- package/packages/native/src/grep/index.ts +3 -2
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +169 -55
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +13 -1
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +16 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +91 -1
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +273 -63
- package/packages/pi-agent-core/src/agent.ts +24 -0
- package/packages/pi-agent-core/src/types.ts +98 -0
- package/packages/pi-ai/dist/env-api-keys.js +2 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +314 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +236 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.js +6 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +2 -0
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +1 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/env-api-keys.ts +2 -0
- package/packages/pi-ai/src/models.generated.ts +236 -0
- package/packages/pi-ai/src/providers/google-gemini-cli.ts +8 -0
- package/packages/pi-ai/src/providers/openai-completions.ts +3 -0
- package/packages/pi-ai/src/types.ts +2 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +1 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +137 -22
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +43 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/fallback-resolver.d.ts +51 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.js +119 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.test.js +183 -0
- package/packages/pi-coding-agent/dist/core/fallback-resolver.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/index.js +1 -0
- package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts +12 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +37 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/session-manager.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.test.js +61 -0
- package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +47 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/find.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/find.js +1 -0
- package/packages/pi-coding-agent/dist/core/tools/find.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +6 -15
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts +18 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +46 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +25 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +1 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +167 -22
- package/packages/pi-coding-agent/src/core/auth-storage.ts +45 -0
- package/packages/pi-coding-agent/src/core/fallback-resolver.test.ts +229 -0
- package/packages/pi-coding-agent/src/core/fallback-resolver.ts +165 -0
- package/packages/pi-coding-agent/src/core/index.ts +1 -0
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/packages/pi-coding-agent/src/core/session-manager.test.ts +65 -0
- package/packages/pi-coding-agent/src/core/session-manager.ts +51 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +66 -0
- package/packages/pi-coding-agent/src/core/tools/find.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +6 -16
- package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +49 -4
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +28 -2
- package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
- package/packages/pi-tui/dist/autocomplete.js +3 -4
- package/packages/pi-tui/dist/autocomplete.js.map +1 -1
- package/packages/pi-tui/dist/components/editor.d.ts +7 -0
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +57 -50
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/autocomplete.ts +3 -5
- package/packages/pi-tui/src/components/editor.ts +70 -49
- package/pkg/dist/core/export-html/ansi-to-html.d.ts +22 -0
- package/pkg/dist/core/export-html/ansi-to-html.d.ts.map +1 -0
- package/pkg/dist/core/export-html/ansi-to-html.js +249 -0
- package/pkg/dist/core/export-html/ansi-to-html.js.map +1 -0
- package/pkg/dist/core/export-html/index.d.ts +37 -0
- package/pkg/dist/core/export-html/index.d.ts.map +1 -0
- package/pkg/dist/core/export-html/index.js +223 -0
- package/pkg/dist/core/export-html/index.js.map +1 -0
- package/pkg/dist/core/export-html/template.css +971 -0
- package/pkg/dist/core/export-html/template.html +54 -0
- package/pkg/dist/core/export-html/template.js +1583 -0
- package/pkg/dist/core/export-html/tool-renderer.d.ts +38 -0
- package/pkg/dist/core/export-html/tool-renderer.d.ts.map +1 -0
- package/pkg/dist/core/export-html/tool-renderer.js +70 -0
- package/pkg/dist/core/export-html/tool-renderer.js.map +1 -0
- package/pkg/dist/core/export-html/vendor/highlight.min.js +1213 -0
- package/pkg/dist/core/export-html/vendor/marked.min.js +6 -0
- package/scripts/postinstall.js +30 -17
- package/src/resources/extensions/bg-shell/index.ts +51 -7
- package/src/resources/extensions/gsd/activity-log.ts +61 -14
- package/src/resources/extensions/gsd/auto.ts +288 -19
- package/src/resources/extensions/gsd/commands.ts +9 -3
- package/src/resources/extensions/gsd/dashboard-overlay.ts +55 -12
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
- package/src/resources/extensions/gsd/doctor.ts +60 -3
- package/src/resources/extensions/gsd/files.ts +45 -1
- package/src/resources/extensions/gsd/git-service.ts +46 -41
- package/src/resources/extensions/gsd/guided-flow.ts +81 -9
- package/src/resources/extensions/gsd/index.ts +62 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +181 -0
- package/src/resources/extensions/gsd/paths.ts +35 -4
- package/src/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/src/resources/extensions/gsd/preferences.ts +232 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -3
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +10 -8
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/queue.md +3 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/state.ts +130 -18
- package/src/resources/extensions/gsd/templates/context.md +1 -1
- package/src/resources/extensions/gsd/templates/state.md +3 -3
- package/src/resources/extensions/gsd/tests/activity-log-save.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/doctor.test.ts +115 -1
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +6 -2
- package/src/resources/extensions/gsd/tests/git-service.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +8 -8
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/orphaned-branch.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree.test.ts +19 -19
- package/src/resources/extensions/gsd/types.ts +109 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +72 -13
- package/src/resources/extensions/gsd/worktree.ts +5 -4
- package/src/resources/extensions/search-the-web/command-search-provider.ts +8 -4
- package/src/resources/extensions/search-the-web/native-search.ts +32 -23
- package/src/resources/extensions/search-the-web/provider.ts +19 -2
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
- package/src/resources/extensions/search-the-web/tool-search.ts +62 -3
- package/src/resources/extensions/shared/bundled-extension-paths.ts +11 -0
- package/src/resources/extensions/subagent/index.ts +2 -1
- package/src/resources/skills/github-workflows/SKILL.md +5 -0
- package/dist/resources/extensions/gsd/git-service.d.ts +0 -225
- package/dist/resources/extensions/gsd/git-service.js +0 -756
- package/dist/resources/extensions/gsd/preferences.d.ts +0 -122
- package/dist/resources/extensions/gsd/preferences.js +0 -616
- package/dist/resources/extensions/gsd/worktree.d.ts +0 -106
- package/dist/resources/extensions/remote-questions/config.d.ts +0 -14
- package/dist/resources/extensions/remote-questions/config.js +0 -68
- package/dist/resources/extensions/remote-questions/discord-adapter.d.ts +0 -17
- package/dist/resources/extensions/remote-questions/discord-adapter.js +0 -118
- package/dist/resources/extensions/remote-questions/format.d.ts +0 -38
- package/dist/resources/extensions/remote-questions/format.js +0 -119
- package/dist/resources/extensions/remote-questions/manager.d.ts +0 -23
- package/dist/resources/extensions/remote-questions/manager.js +0 -161
- package/dist/resources/extensions/remote-questions/remote-command.d.ts +0 -6
- package/dist/resources/extensions/remote-questions/remote-command.js +0 -358
- package/dist/resources/extensions/remote-questions/slack-adapter.d.ts +0 -15
- package/dist/resources/extensions/remote-questions/slack-adapter.js +0 -82
- package/dist/resources/extensions/remote-questions/status.d.ts +0 -9
- package/dist/resources/extensions/remote-questions/status.js +0 -25
- package/dist/resources/extensions/remote-questions/store.d.ts +0 -11
- package/dist/resources/extensions/remote-questions/store.js +0 -66
- package/dist/resources/extensions/remote-questions/types.d.ts +0 -68
- package/dist/resources/extensions/remote-questions/types.js +0 -4
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Git Service
|
|
3
|
+
*
|
|
4
|
+
* Core git operations for GSD: types, constants, and pure helpers.
|
|
5
|
+
* Higher-level operations (commit, staging, branching) build on these.
|
|
6
|
+
*
|
|
7
|
+
* This module centralizes the GitPreferences interface, runtime exclusion
|
|
8
|
+
* paths, commit type inference, and the runGit shell helper.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join, sep } from "node:path";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
detectWorktreeName,
|
|
17
|
+
getSliceBranchName,
|
|
18
|
+
SLICE_BRANCH_RE,
|
|
19
|
+
} from "./worktree.js";
|
|
20
|
+
import {
|
|
21
|
+
nativeGetCurrentBranch,
|
|
22
|
+
nativeDetectMainBranch,
|
|
23
|
+
nativeBranchExists,
|
|
24
|
+
nativeHasMergeConflicts,
|
|
25
|
+
nativeHasChanges,
|
|
26
|
+
nativeCommitCountBetween,
|
|
27
|
+
} from "./native-git-bridge.js";
|
|
28
|
+
|
|
29
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export interface GitPreferences {
|
|
32
|
+
auto_push?: boolean;
|
|
33
|
+
push_branches?: boolean;
|
|
34
|
+
remote?: string;
|
|
35
|
+
snapshots?: boolean;
|
|
36
|
+
pre_merge_check?: boolean | string;
|
|
37
|
+
commit_type?: string;
|
|
38
|
+
main_branch?: string;
|
|
39
|
+
merge_strategy?: "squash" | "merge";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
|
|
43
|
+
|
|
44
|
+
export interface CommitOptions {
|
|
45
|
+
message: string;
|
|
46
|
+
allowEmpty?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MergeSliceResult {
|
|
50
|
+
branch: string;
|
|
51
|
+
mergedCommitMessage: string;
|
|
52
|
+
deletedBranch: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Thrown when a slice merge hits code conflicts in non-.gsd files.
|
|
57
|
+
* The working tree is left in a conflicted state (no reset) so the
|
|
58
|
+
* caller can dispatch a fix-merge session to resolve it.
|
|
59
|
+
*/
|
|
60
|
+
export class MergeConflictError extends Error {
|
|
61
|
+
readonly conflictedFiles: string[];
|
|
62
|
+
readonly strategy: "squash" | "merge";
|
|
63
|
+
readonly branch: string;
|
|
64
|
+
readonly mainBranch: string;
|
|
65
|
+
|
|
66
|
+
constructor(
|
|
67
|
+
conflictedFiles: string[],
|
|
68
|
+
strategy: "squash" | "merge",
|
|
69
|
+
branch: string,
|
|
70
|
+
mainBranch: string,
|
|
71
|
+
) {
|
|
72
|
+
super(
|
|
73
|
+
`${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" ` +
|
|
74
|
+
`failed with conflicts in ${conflictedFiles.length} non-.gsd file(s): ${conflictedFiles.join(", ")}`,
|
|
75
|
+
);
|
|
76
|
+
this.name = "MergeConflictError";
|
|
77
|
+
this.conflictedFiles = conflictedFiles;
|
|
78
|
+
this.strategy = strategy;
|
|
79
|
+
this.branch = branch;
|
|
80
|
+
this.mainBranch = mainBranch;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PreMergeCheckResult {
|
|
85
|
+
passed: boolean;
|
|
86
|
+
skipped?: boolean;
|
|
87
|
+
command?: string;
|
|
88
|
+
error?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* GSD runtime paths that should be excluded from smart staging.
|
|
95
|
+
* These are transient/generated artifacts that should never be committed.
|
|
96
|
+
* Matches the union of SKIP_PATHS + SKIP_EXACT in worktree-manager.ts
|
|
97
|
+
* and the first 7 entries in gitignore.ts BASELINE_PATTERNS.
|
|
98
|
+
*/
|
|
99
|
+
export const RUNTIME_EXCLUSION_PATHS: readonly string[] = [
|
|
100
|
+
".gsd/activity/",
|
|
101
|
+
".gsd/runtime/",
|
|
102
|
+
".gsd/worktrees/",
|
|
103
|
+
".gsd/auto.lock",
|
|
104
|
+
".gsd/metrics.json",
|
|
105
|
+
".gsd/completed-units.json",
|
|
106
|
+
".gsd/STATE.md",
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* GSD planning artifact paths that must be force-added even when .gsd/
|
|
111
|
+
* is in .gitignore. These are durable planning files that the agent writes
|
|
112
|
+
* and that must survive squash-merges to main.
|
|
113
|
+
*
|
|
114
|
+
* `git add --force` is a no-op when the path doesn't exist or has no
|
|
115
|
+
* changes, so this list is safe to apply unconditionally.
|
|
116
|
+
*/
|
|
117
|
+
const GSD_DURABLE_PATHS: readonly string[] = [
|
|
118
|
+
".gsd/milestones/",
|
|
119
|
+
".gsd/DECISIONS.md",
|
|
120
|
+
".gsd/QUEUE.md",
|
|
121
|
+
".gsd/PROJECT.md",
|
|
122
|
+
".gsd/REQUIREMENTS.md",
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
// ─── Integration Branch Metadata ───────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Path to the milestone metadata file that stores the integration branch.
|
|
129
|
+
* Format: .gsd/milestones/<MID>/<MID>-META.json
|
|
130
|
+
*/
|
|
131
|
+
function milestoneMetaPath(basePath: string, milestoneId: string): string {
|
|
132
|
+
return join(basePath, ".gsd", "milestones", milestoneId, `${milestoneId}-META.json`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Read the integration branch recorded for a milestone.
|
|
137
|
+
* Returns null if no metadata file exists or the branch isn't set.
|
|
138
|
+
*/
|
|
139
|
+
export function readIntegrationBranch(basePath: string, milestoneId: string): string | null {
|
|
140
|
+
try {
|
|
141
|
+
const metaFile = milestoneMetaPath(basePath, milestoneId);
|
|
142
|
+
if (!existsSync(metaFile)) return null;
|
|
143
|
+
const data = JSON.parse(readFileSync(metaFile, "utf-8"));
|
|
144
|
+
const branch = data?.integrationBranch;
|
|
145
|
+
if (typeof branch === "string" && branch.trim() !== "" && VALID_BRANCH_NAME.test(branch)) {
|
|
146
|
+
return branch;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Persist the integration branch for a milestone.
|
|
156
|
+
*
|
|
157
|
+
* Called when auto-mode starts on a milestone. Records the branch the user
|
|
158
|
+
* was on at that point, so that slice branches merge back to it instead of
|
|
159
|
+
* the repo's default branch. Idempotent when the branch matches; updates
|
|
160
|
+
* the record when the user starts from a different branch.
|
|
161
|
+
*
|
|
162
|
+
* The file is committed immediately so it survives branch switches — the
|
|
163
|
+
* pre-switch auto-commit excludes `.gsd/` to avoid merge conflicts, and
|
|
164
|
+
* uncommitted `.gsd/` files are discarded during checkout.
|
|
165
|
+
*/
|
|
166
|
+
export function writeIntegrationBranch(basePath: string, milestoneId: string, branch: string): void {
|
|
167
|
+
// Don't record slice branches as the integration target
|
|
168
|
+
if (SLICE_BRANCH_RE.test(branch)) return;
|
|
169
|
+
// Validate
|
|
170
|
+
if (!VALID_BRANCH_NAME.test(branch)) return;
|
|
171
|
+
// Skip if already recorded with the same branch (idempotent across restarts).
|
|
172
|
+
// If recorded with a different branch, update it — the user started auto-mode
|
|
173
|
+
// from a new branch and expects slices to merge back there (#300).
|
|
174
|
+
const existingBranch = readIntegrationBranch(basePath, milestoneId);
|
|
175
|
+
if (existingBranch === branch) return;
|
|
176
|
+
|
|
177
|
+
const metaFile = milestoneMetaPath(basePath, milestoneId);
|
|
178
|
+
mkdirSync(join(basePath, ".gsd", "milestones", milestoneId), { recursive: true });
|
|
179
|
+
|
|
180
|
+
// Merge with existing metadata if present
|
|
181
|
+
let existing: Record<string, unknown> = {};
|
|
182
|
+
try {
|
|
183
|
+
if (existsSync(metaFile)) {
|
|
184
|
+
existing = JSON.parse(readFileSync(metaFile, "utf-8"));
|
|
185
|
+
}
|
|
186
|
+
} catch { /* corrupt file — overwrite */ }
|
|
187
|
+
|
|
188
|
+
existing.integrationBranch = branch;
|
|
189
|
+
writeFileSync(metaFile, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
190
|
+
|
|
191
|
+
// Commit immediately — .gsd/ files are discarded during branch switches
|
|
192
|
+
// (ensureSliceBranch excludes .gsd/ from pre-switch auto-commit and runs
|
|
193
|
+
// git checkout -- .gsd/ to prevent checkout conflicts). Without this
|
|
194
|
+
// commit, the metadata would be lost on the first branch switch.
|
|
195
|
+
try {
|
|
196
|
+
runGit(basePath, ["add", "--force", metaFile]);
|
|
197
|
+
runGit(basePath, ["commit", "--no-verify", "-F", "-"], {
|
|
198
|
+
input: `chore(${milestoneId}): record integration branch`,
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
// Non-fatal — file is on disk even if commit fails (e.g. nothing to commit
|
|
202
|
+
// because the file was already tracked with identical content)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── Git Helper ────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/** Env overlay that suppresses interactive git credential prompts and git-svn noise. */
|
|
209
|
+
const GIT_NO_PROMPT_ENV = {
|
|
210
|
+
...process.env,
|
|
211
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
212
|
+
GIT_ASKPASS: "",
|
|
213
|
+
GIT_SVN_ID: "",
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Strip git-svn noise from error messages.
|
|
218
|
+
* Some systems (notably Arch Linux) have a buggy git-svn Perl module that
|
|
219
|
+
* emits warnings on every git invocation, confusing users. See #404.
|
|
220
|
+
*/
|
|
221
|
+
function filterGitSvnNoise(message: string): string {
|
|
222
|
+
return message
|
|
223
|
+
.replace(/Duplicate specification "[^"]*" for option "[^"]*"\n?/g, "")
|
|
224
|
+
.replace(/Unable to determine upstream SVN information from .*\n?/g, "")
|
|
225
|
+
.replace(/Perhaps the repository is empty\. at .*git-svn.*\n?/g, "")
|
|
226
|
+
.trim();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Run a git command in the given directory.
|
|
231
|
+
* Returns trimmed stdout. Throws on non-zero exit unless allowFailure is set.
|
|
232
|
+
* When `input` is provided, it is piped to stdin.
|
|
233
|
+
*/
|
|
234
|
+
export function runGit(basePath: string, args: string[], options: { allowFailure?: boolean; input?: string } = {}): string {
|
|
235
|
+
try {
|
|
236
|
+
return execSync(`git ${args.join(" ")}`, {
|
|
237
|
+
cwd: basePath,
|
|
238
|
+
stdio: [options.input != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
239
|
+
encoding: "utf-8",
|
|
240
|
+
env: GIT_NO_PROMPT_ENV,
|
|
241
|
+
...(options.input != null ? { input: options.input } : {}),
|
|
242
|
+
}).trim();
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (options.allowFailure) return "";
|
|
245
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
246
|
+
throw new Error(`git ${args.join(" ")} failed in ${basePath}: ${filterGitSvnNoise(message)}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ─── Commit Type Inference ─────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Keyword-to-commit-type mapping. Order matters — first match wins.
|
|
254
|
+
* Each entry: [keywords[], commitType]
|
|
255
|
+
*/
|
|
256
|
+
const COMMIT_TYPE_RULES: [string[], string][] = [
|
|
257
|
+
[["fix", "bug", "patch", "hotfix"], "fix"],
|
|
258
|
+
[["refactor", "restructure", "reorganize"], "refactor"],
|
|
259
|
+
[["doc", "docs", "documentation"], "docs"],
|
|
260
|
+
[["test", "tests", "testing"], "test"],
|
|
261
|
+
[["chore", "cleanup", "clean up", "archive", "remove", "delete"], "chore"],
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Infer a conventional commit type from a slice title.
|
|
266
|
+
* Uses case-insensitive word-boundary matching against known keywords.
|
|
267
|
+
* Returns "feat" when no keywords match.
|
|
268
|
+
*/
|
|
269
|
+
// ─── GitServiceImpl ────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
export class GitServiceImpl {
|
|
272
|
+
readonly basePath: string;
|
|
273
|
+
readonly prefs: GitPreferences;
|
|
274
|
+
|
|
275
|
+
/** Active milestone ID — used to resolve the integration branch. */
|
|
276
|
+
private _milestoneId: string | null = null;
|
|
277
|
+
|
|
278
|
+
constructor(basePath: string, prefs: GitPreferences = {}) {
|
|
279
|
+
this.basePath = basePath;
|
|
280
|
+
this.prefs = prefs;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Set the active milestone ID for integration branch resolution.
|
|
285
|
+
* When set, getMainBranch() will check the milestone's metadata file
|
|
286
|
+
* for a recorded integration branch before falling back to repo defaults.
|
|
287
|
+
*/
|
|
288
|
+
setMilestoneId(milestoneId: string | null): void {
|
|
289
|
+
this._milestoneId = milestoneId;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Convenience wrapper: run git in this repo's basePath. */
|
|
293
|
+
private git(args: string[], options: { allowFailure?: boolean; input?: string } = {}): string {
|
|
294
|
+
return runGit(this.basePath, args, options);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Smart staging: `git add -A` excluding GSD runtime paths via pathspec.
|
|
299
|
+
* Falls back to plain `git add -A` if the exclusion pathspec fails.
|
|
300
|
+
* @param extraExclusions Additional pathspec exclusions beyond RUNTIME_EXCLUSION_PATHS.
|
|
301
|
+
*/
|
|
302
|
+
private smartStage(extraExclusions: readonly string[] = []): void {
|
|
303
|
+
const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
|
|
304
|
+
|
|
305
|
+
// One-time cleanup: if runtime files are already tracked in the index
|
|
306
|
+
// (from older versions where the fallback bug staged them), untrack them
|
|
307
|
+
// in a dedicated commit. This must happen as a separate commit because
|
|
308
|
+
// the git reset HEAD step below would otherwise undo the rm --cached.
|
|
309
|
+
if (!this._runtimeFilesCleanedUp) {
|
|
310
|
+
let cleaned = false;
|
|
311
|
+
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
|
|
312
|
+
const result = this.git(["rm", "--cached", "-r", "--ignore-unmatch", exclusion], { allowFailure: true });
|
|
313
|
+
if (result && result.includes("rm '")) cleaned = true;
|
|
314
|
+
}
|
|
315
|
+
if (cleaned) {
|
|
316
|
+
this.git(["commit", "--no-verify", "-F", "-"], { input: "chore: untrack .gsd/ runtime files from git index" });
|
|
317
|
+
}
|
|
318
|
+
this._runtimeFilesCleanedUp = true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Stage everything, then unstage excluded paths.
|
|
322
|
+
//
|
|
323
|
+
// Previous approach used pathspec excludes (:(exclude)...) with git add -A,
|
|
324
|
+
// but that fails when .gsd/ is in .gitignore — git exits non-zero before
|
|
325
|
+
// evaluating the excludes. The catch fallback ran plain `git add -A`,
|
|
326
|
+
// staging all tracked runtime files unconditionally and defeating the
|
|
327
|
+
// exclusion list entirely.
|
|
328
|
+
//
|
|
329
|
+
// git reset HEAD silently succeeds when the path isn't staged, so no
|
|
330
|
+
// error handling is needed per-path.
|
|
331
|
+
this.git(["add", "-A"]);
|
|
332
|
+
|
|
333
|
+
// Force-add GSD planning artifacts that live under .gsd/ but may be
|
|
334
|
+
// blocked by a .gsd/ gitignore pattern. `git add -A` respects .gitignore,
|
|
335
|
+
// so new files (CONTEXT.md, SUMMARY.md, PLAN.md, etc.) in gitignored
|
|
336
|
+
// directories are silently skipped. Without this force-add, planning
|
|
337
|
+
// artifacts are never committed — they exist on disk but not in git.
|
|
338
|
+
// Squash-merges then delete them on main because they appear as "removed
|
|
339
|
+
// relative to main" during the merge.
|
|
340
|
+
//
|
|
341
|
+
// Only force-add durable planning paths — runtime paths are excluded
|
|
342
|
+
// by the reset step below.
|
|
343
|
+
for (const durablePath of GSD_DURABLE_PATHS) {
|
|
344
|
+
this.git(["add", "--force", "--", durablePath], { allowFailure: true });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const exclusion of allExclusions) {
|
|
348
|
+
this.git(["reset", "HEAD", "--", exclusion], { allowFailure: true });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Tracks whether runtime file cleanup has run this session. */
|
|
353
|
+
private _runtimeFilesCleanedUp = false;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Stage files (smart staging) and commit.
|
|
357
|
+
* Returns the commit message string on success, or null if nothing to commit.
|
|
358
|
+
* Uses `git commit -F -` with stdin pipe for safe multi-line message handling.
|
|
359
|
+
*/
|
|
360
|
+
commit(opts: CommitOptions): string | null {
|
|
361
|
+
this.smartStage();
|
|
362
|
+
|
|
363
|
+
// Check if anything was actually staged
|
|
364
|
+
const staged = this.git(["diff", "--cached", "--stat"], { allowFailure: true });
|
|
365
|
+
if (!staged && !opts.allowEmpty) return null;
|
|
366
|
+
|
|
367
|
+
this.git(
|
|
368
|
+
["commit", "--no-verify", "-F", "-", ...(opts.allowEmpty ? ["--allow-empty"] : [])],
|
|
369
|
+
{ input: opts.message },
|
|
370
|
+
);
|
|
371
|
+
return opts.message;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Auto-commit dirty working tree with a conventional chore message.
|
|
376
|
+
* Returns the commit message on success, or null if nothing to commit.
|
|
377
|
+
* @param extraExclusions Additional paths to exclude from staging (e.g. [".gsd/"] for pre-switch commits).
|
|
378
|
+
*/
|
|
379
|
+
autoCommit(unitType: string, unitId: string, extraExclusions: readonly string[] = []): string | null {
|
|
380
|
+
// Quick check: is there anything dirty at all?
|
|
381
|
+
// Native path uses libgit2 (single syscall), fallback spawns git.
|
|
382
|
+
if (!nativeHasChanges(this.basePath)) return null;
|
|
383
|
+
|
|
384
|
+
this.smartStage(extraExclusions);
|
|
385
|
+
|
|
386
|
+
// After smart staging, check if anything was actually staged
|
|
387
|
+
// (all changes might have been runtime files that got excluded)
|
|
388
|
+
const staged = this.git(["diff", "--cached", "--stat"], { allowFailure: true });
|
|
389
|
+
if (!staged) return null;
|
|
390
|
+
|
|
391
|
+
const message = `chore(${unitId}): auto-commit after ${unitType}`;
|
|
392
|
+
this.git(["commit", "--no-verify", "-F", "-"], { input: message });
|
|
393
|
+
return message;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ─── Branch Queries ────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get the integration branch for this repo — the branch that slice
|
|
400
|
+
* branches are created from and merged back into.
|
|
401
|
+
*
|
|
402
|
+
* This is often `main` or `master`, but not necessarily. When a user
|
|
403
|
+
* starts GSD on a feature branch like `f-123-new-thing`, that branch
|
|
404
|
+
* is recorded as the integration target, and all slice branches merge
|
|
405
|
+
* back into it — not the repo's default branch. The name "main branch"
|
|
406
|
+
* in variable names is historical; think of it as "integration branch".
|
|
407
|
+
*
|
|
408
|
+
* Resolution order:
|
|
409
|
+
* 1. Explicit `main_branch` preference (user override, highest priority)
|
|
410
|
+
* 2. Milestone integration branch from metadata file (recorded at milestone start)
|
|
411
|
+
* 3. Worktree base branch (worktree/<name>)
|
|
412
|
+
* 4. origin/HEAD symbolic-ref → main/master fallback → current branch
|
|
413
|
+
*/
|
|
414
|
+
getMainBranch(): string {
|
|
415
|
+
// Explicit preference takes priority (double-check validity as defense-in-depth)
|
|
416
|
+
if (this.prefs.main_branch && VALID_BRANCH_NAME.test(this.prefs.main_branch)) {
|
|
417
|
+
return this.prefs.main_branch;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Check milestone integration branch — recorded when auto-mode starts
|
|
421
|
+
if (this._milestoneId) {
|
|
422
|
+
const integrationBranch = readIntegrationBranch(this.basePath, this._milestoneId);
|
|
423
|
+
if (integrationBranch) {
|
|
424
|
+
// Verify the branch still exists locally (could have been deleted)
|
|
425
|
+
if (nativeBranchExists(this.basePath, integrationBranch)) return integrationBranch;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const wtName = detectWorktreeName(this.basePath);
|
|
430
|
+
if (wtName) {
|
|
431
|
+
const wtBranch = `worktree/${wtName}`;
|
|
432
|
+
if (nativeBranchExists(this.basePath, wtBranch)) return wtBranch;
|
|
433
|
+
return nativeGetCurrentBranch(this.basePath);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Repo-level default detection: origin/HEAD → main → master → current branch.
|
|
437
|
+
// Native path uses libgit2 (single call), fallback spawns multiple git processes.
|
|
438
|
+
return nativeDetectMainBranch(this.basePath);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Get the current branch name. Native libgit2 when available, execSync fallback. */
|
|
442
|
+
getCurrentBranch(): string {
|
|
443
|
+
return nativeGetCurrentBranch(this.basePath);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** True if currently on a GSD slice branch. */
|
|
447
|
+
isOnSliceBranch(): boolean {
|
|
448
|
+
const current = this.getCurrentBranch();
|
|
449
|
+
return SLICE_BRANCH_RE.test(current);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Returns the slice branch name if on one, null otherwise. */
|
|
453
|
+
getActiveSliceBranch(): string | null {
|
|
454
|
+
try {
|
|
455
|
+
const current = this.getCurrentBranch();
|
|
456
|
+
return SLICE_BRANCH_RE.test(current) ? current : null;
|
|
457
|
+
} catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ─── Branch Lifecycle ──────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Check if a local branch exists. Native libgit2 when available, execSync fallback.
|
|
466
|
+
*/
|
|
467
|
+
private branchExists(branch: string): boolean {
|
|
468
|
+
return nativeBranchExists(this.basePath, branch);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Ensure the slice branch exists and is checked out.
|
|
473
|
+
*
|
|
474
|
+
* Creates the branch from the current working branch if it's not a slice
|
|
475
|
+
* branch (preserves planning artifacts). Falls back to the integration
|
|
476
|
+
* branch when on another slice branch (avoids chaining slice branches).
|
|
477
|
+
*
|
|
478
|
+
* Auto-commits dirty state via smart staging before checkout so runtime
|
|
479
|
+
* files are never accidentally committed during branch switches.
|
|
480
|
+
*
|
|
481
|
+
* Returns true if the branch was newly created.
|
|
482
|
+
*/
|
|
483
|
+
ensureSliceBranch(milestoneId: string, sliceId: string): boolean {
|
|
484
|
+
const wtName = detectWorktreeName(this.basePath);
|
|
485
|
+
const branch = getSliceBranchName(milestoneId, sliceId, wtName);
|
|
486
|
+
const current = this.getCurrentBranch();
|
|
487
|
+
|
|
488
|
+
if (current === branch) return false;
|
|
489
|
+
|
|
490
|
+
let created = false;
|
|
491
|
+
|
|
492
|
+
if (!this.branchExists(branch)) {
|
|
493
|
+
// Fetch from remote before creating a new branch (best-effort).
|
|
494
|
+
const remotes = this.git(["remote"], { allowFailure: true });
|
|
495
|
+
if (remotes) {
|
|
496
|
+
const remote = this.prefs.remote ?? "origin";
|
|
497
|
+
const fetchResult = this.git(["fetch", "--prune", remote], { allowFailure: true });
|
|
498
|
+
if (fetchResult === "" && remotes.split("\n").includes(remote)) {
|
|
499
|
+
// Check if local is behind upstream (informational only)
|
|
500
|
+
const behind = this.git(
|
|
501
|
+
["rev-list", "--count", "HEAD..@{upstream}"],
|
|
502
|
+
{ allowFailure: true },
|
|
503
|
+
);
|
|
504
|
+
if (behind && parseInt(behind, 10) > 0) {
|
|
505
|
+
console.error(`GitService: local branch is ${behind} commit(s) behind upstream`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Branch from current when it's a normal working branch (not a slice).
|
|
511
|
+
// If already on a slice branch, fall back to the integration branch to avoid chaining.
|
|
512
|
+
const mainBranch = this.getMainBranch();
|
|
513
|
+
const base = SLICE_BRANCH_RE.test(current) ? mainBranch : current;
|
|
514
|
+
this.git(["branch", branch, base]);
|
|
515
|
+
created = true;
|
|
516
|
+
} else {
|
|
517
|
+
// Branch exists — check it's not checked out in another worktree
|
|
518
|
+
const worktreeList = this.git(["worktree", "list", "--porcelain"]);
|
|
519
|
+
if (worktreeList.includes(`branch refs/heads/${branch}`)) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Branch "${branch}" is already in use by another worktree. ` +
|
|
522
|
+
`Remove that worktree first, or switch it to a different branch.`,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Auto-commit dirty state via smart staging before checkout.
|
|
528
|
+
// Exclude .gsd/ to prevent merge conflicts when both branches modify planning artifacts.
|
|
529
|
+
this.autoCommit("pre-switch", current, [".gsd/"]);
|
|
530
|
+
|
|
531
|
+
// Discard uncommitted .gsd/ changes so checkout doesn't fail.
|
|
532
|
+
// Two-step approach handles both tracked and untracked runtime files:
|
|
533
|
+
// 1. `checkout --` reverts tracked .gsd/ files to their HEAD versions.
|
|
534
|
+
// 2. `clean -fdx` removes untracked runtime files that the target branch has
|
|
535
|
+
// tracked — e.g., when a prior cleanup commit removed STATE.md from the
|
|
536
|
+
// current branch's HEAD but the target branch still has it committed.
|
|
537
|
+
this.git(["checkout", "--", ".gsd/"], { allowFailure: true });
|
|
538
|
+
this.discardUntrackedRuntimeFiles();
|
|
539
|
+
|
|
540
|
+
this.git(["checkout", branch]);
|
|
541
|
+
return created;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Switch to the integration branch, auto-committing dirty state via smart staging first.
|
|
546
|
+
*/
|
|
547
|
+
switchToMain(): void {
|
|
548
|
+
const mainBranch = this.getMainBranch();
|
|
549
|
+
const current = this.getCurrentBranch();
|
|
550
|
+
if (current === mainBranch) return;
|
|
551
|
+
|
|
552
|
+
// Exclude .gsd/ to prevent merge conflicts when both branches modify planning artifacts.
|
|
553
|
+
this.autoCommit("pre-switch", current, [".gsd/"]);
|
|
554
|
+
|
|
555
|
+
// Discard uncommitted .gsd/ changes so checkout doesn't fail.
|
|
556
|
+
// Two-step approach handles both tracked and untracked runtime files.
|
|
557
|
+
this.git(["checkout", "--", ".gsd/"], { allowFailure: true });
|
|
558
|
+
this.discardUntrackedRuntimeFiles();
|
|
559
|
+
|
|
560
|
+
this.git(["checkout", mainBranch]);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Remove untracked runtime files from the working tree.
|
|
565
|
+
*
|
|
566
|
+
* Complements `git checkout -- .gsd/` (which only handles tracked files).
|
|
567
|
+
* Runtime files can end up untracked after a cleanup commit removes them
|
|
568
|
+
* from the current branch's HEAD — but the target branch may still have
|
|
569
|
+
* them committed. Without this step, `git checkout` fails with:
|
|
570
|
+
* "The following untracked working tree files would be overwritten by checkout"
|
|
571
|
+
*
|
|
572
|
+
* `git clean -fdx` is safe here because:
|
|
573
|
+
* - Only removes *untracked* files (tracked files are untouched)
|
|
574
|
+
* - Targets only the specific runtime paths listed in RUNTIME_EXCLUSION_PATHS
|
|
575
|
+
* - These files are always regenerated by GSD on the next run
|
|
576
|
+
*/
|
|
577
|
+
private discardUntrackedRuntimeFiles(): void {
|
|
578
|
+
this.git(["clean", "-fdx", "--", ...RUNTIME_EXCLUSION_PATHS], { allowFailure: true });
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ─── S05 Features ─────────────────────────────────────────────────────
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Create a snapshot ref for the given label (typically a slice branch name).
|
|
585
|
+
* Gated on prefs.snapshots === true. Ref path: refs/gsd/snapshots/<label>/<timestamp>
|
|
586
|
+
* The ref points at HEAD, capturing the current commit before destructive operations.
|
|
587
|
+
*/
|
|
588
|
+
createSnapshot(label: string): void {
|
|
589
|
+
if (this.prefs.snapshots !== true) return;
|
|
590
|
+
|
|
591
|
+
const now = new Date();
|
|
592
|
+
const ts = now.getFullYear().toString()
|
|
593
|
+
+ String(now.getMonth() + 1).padStart(2, "0")
|
|
594
|
+
+ String(now.getDate()).padStart(2, "0")
|
|
595
|
+
+ "-"
|
|
596
|
+
+ String(now.getHours()).padStart(2, "0")
|
|
597
|
+
+ String(now.getMinutes()).padStart(2, "0")
|
|
598
|
+
+ String(now.getSeconds()).padStart(2, "0");
|
|
599
|
+
|
|
600
|
+
const refPath = `refs/gsd/snapshots/${label}/${ts}`;
|
|
601
|
+
this.git(["update-ref", refPath, "HEAD"]);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Run pre-merge verification check. Auto-detects test runner from project
|
|
606
|
+
* files, or uses custom command from prefs.pre_merge_check.
|
|
607
|
+
* Gated on prefs.pre_merge_check (false = skip, string = custom command).
|
|
608
|
+
* Stub: to be implemented in T03.
|
|
609
|
+
*/
|
|
610
|
+
runPreMergeCheck(): PreMergeCheckResult {
|
|
611
|
+
if (this.prefs.pre_merge_check === false || this.prefs.pre_merge_check === undefined) {
|
|
612
|
+
return { passed: true, skipped: true };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Determine command: explicit string or auto-detect from package.json
|
|
616
|
+
let command: string;
|
|
617
|
+
if (typeof this.prefs.pre_merge_check === "string") {
|
|
618
|
+
command = this.prefs.pre_merge_check;
|
|
619
|
+
} else {
|
|
620
|
+
// Auto-detect: look for package.json with a test script
|
|
621
|
+
try {
|
|
622
|
+
const pkg = execSync("cat package.json", { cwd: this.basePath, encoding: "utf-8" });
|
|
623
|
+
const parsed = JSON.parse(pkg);
|
|
624
|
+
if (parsed.scripts?.test) {
|
|
625
|
+
command = "npm test";
|
|
626
|
+
} else {
|
|
627
|
+
return { passed: true, skipped: true };
|
|
628
|
+
}
|
|
629
|
+
} catch {
|
|
630
|
+
return { passed: true, skipped: true };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
execSync(command, { cwd: this.basePath, stdio: "pipe", encoding: "utf-8" });
|
|
636
|
+
return { passed: true, skipped: false, command };
|
|
637
|
+
} catch (err) {
|
|
638
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
639
|
+
return { passed: false, skipped: false, command, error: msg };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ─── Merge ─────────────────────────────────────────────────────────────
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Build a rich squash-commit message with a task list from branch commits.
|
|
647
|
+
*
|
|
648
|
+
* Format:
|
|
649
|
+
* type(scope): title
|
|
650
|
+
*
|
|
651
|
+
* Tasks:
|
|
652
|
+
* - commit subject 1
|
|
653
|
+
* - commit subject 2
|
|
654
|
+
*
|
|
655
|
+
* Branch: gsd/M001/S01
|
|
656
|
+
*/
|
|
657
|
+
private buildRichCommitMessage(
|
|
658
|
+
commitType: string,
|
|
659
|
+
milestoneId: string,
|
|
660
|
+
sliceId: string,
|
|
661
|
+
sliceTitle: string,
|
|
662
|
+
mainBranch: string,
|
|
663
|
+
branch: string,
|
|
664
|
+
): string {
|
|
665
|
+
const subject = `${commitType}(${milestoneId}/${sliceId}): ${sliceTitle}`;
|
|
666
|
+
|
|
667
|
+
// Collect branch commit subjects
|
|
668
|
+
const logOutput = this.git(
|
|
669
|
+
["log", "--oneline", "--format=%s", `${mainBranch}..${branch}`],
|
|
670
|
+
{ allowFailure: true },
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
if (!logOutput) return subject;
|
|
674
|
+
|
|
675
|
+
const subjects = logOutput.split("\n").filter(Boolean);
|
|
676
|
+
const MAX_ENTRIES = 20;
|
|
677
|
+
const truncated = subjects.length > MAX_ENTRIES;
|
|
678
|
+
const displayed = truncated ? subjects.slice(0, MAX_ENTRIES) : subjects;
|
|
679
|
+
|
|
680
|
+
const taskLines = displayed.map(s => `- ${s}`).join("\n");
|
|
681
|
+
const truncationLine = truncated ? `\n- ... and ${subjects.length - MAX_ENTRIES} more` : "";
|
|
682
|
+
|
|
683
|
+
return `${subject}\n\nTasks:\n${taskLines}${truncationLine}\n\nBranch: ${branch}`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Squash-merge a slice branch into the integration branch and delete it.
|
|
688
|
+
*
|
|
689
|
+
* The integration branch is resolved by getMainBranch() — this may be
|
|
690
|
+
* `main`, a feature branch, or a worktree branch depending on context.
|
|
691
|
+
*
|
|
692
|
+
* Flow: snapshot branch HEAD → squash merge → rich commit via stdin →
|
|
693
|
+
* auto-push (if enabled) → delete branch.
|
|
694
|
+
*
|
|
695
|
+
* Must be called from the integration branch. Uses `inferCommitType(sliceTitle)`
|
|
696
|
+
* for the conventional commit type instead of hardcoding `feat`.
|
|
697
|
+
*
|
|
698
|
+
* Throws when:
|
|
699
|
+
* - Not currently on the integration branch
|
|
700
|
+
* - The slice branch does not exist
|
|
701
|
+
* - The slice branch has no commits ahead of the integration branch
|
|
702
|
+
*/
|
|
703
|
+
mergeSliceToMain(milestoneId: string, sliceId: string, sliceTitle: string): MergeSliceResult {
|
|
704
|
+
const mainBranch = this.getMainBranch();
|
|
705
|
+
const current = this.getCurrentBranch();
|
|
706
|
+
|
|
707
|
+
if (current !== mainBranch) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
`mergeSliceToMain must be called from the main branch ("${mainBranch}"), ` +
|
|
710
|
+
`but currently on "${current}"`,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const wtName = detectWorktreeName(this.basePath);
|
|
715
|
+
const branch = getSliceBranchName(milestoneId, sliceId, wtName);
|
|
716
|
+
|
|
717
|
+
if (!this.branchExists(branch)) {
|
|
718
|
+
throw new Error(
|
|
719
|
+
`Slice branch "${branch}" does not exist. Nothing to merge.`,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Check commits ahead — native libgit2 revwalk when available
|
|
724
|
+
const aheadCount = nativeCommitCountBetween(this.basePath, mainBranch, branch);
|
|
725
|
+
if (aheadCount === 0) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
`Slice branch "${branch}" has no commits ahead of "${mainBranch}". Nothing to merge.`,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Snapshot the branch HEAD before merge (gated on prefs)
|
|
732
|
+
// We need to save the ref while the branch still exists
|
|
733
|
+
this.createSnapshot(branch);
|
|
734
|
+
|
|
735
|
+
// Build rich commit message before squash (needs branch history)
|
|
736
|
+
const commitType = inferCommitType(sliceTitle);
|
|
737
|
+
const message = this.buildRichCommitMessage(
|
|
738
|
+
commitType, milestoneId, sliceId, sliceTitle, mainBranch, branch,
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// Pull latest main before merging to avoid conflicts from remote changes
|
|
742
|
+
this.git(["pull", "--rebase", "origin", mainBranch], { allowFailure: true });
|
|
743
|
+
|
|
744
|
+
// Untrack runtime files that may have been manually committed (e.g. via `gsd queue`)
|
|
745
|
+
// to prevent merge conflicts on files that belong in .gitignore (#189)
|
|
746
|
+
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
|
|
747
|
+
this.git(["rm", "--cached", "-r", "--ignore-unmatch", exclusion], { allowFailure: true });
|
|
748
|
+
}
|
|
749
|
+
const untrackDiff = this.git(["diff", "--cached", "--stat"], { allowFailure: true });
|
|
750
|
+
if (untrackDiff && untrackDiff.trim()) {
|
|
751
|
+
this.git(["commit", "--no-verify", "-m", "chore: untrack .gsd/ runtime files before merge"], { allowFailure: true });
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Merge slice branch — strategy is configurable via git.merge_strategy
|
|
755
|
+
// preference. Default: "squash" (preserves existing behavior).
|
|
756
|
+
// "merge" uses --no-ff which is more resilient to conflicts from
|
|
757
|
+
// long-lived branches or frequently-changing .gsd/* artifacts.
|
|
758
|
+
const strategy = this.prefs.merge_strategy ?? "squash";
|
|
759
|
+
const mergeArgs = strategy === "merge"
|
|
760
|
+
? ["merge", "--no-ff", "-m", message, branch]
|
|
761
|
+
: ["merge", "--squash", branch];
|
|
762
|
+
|
|
763
|
+
try {
|
|
764
|
+
this.git(mergeArgs);
|
|
765
|
+
} catch (mergeError) {
|
|
766
|
+
// Check if conflicts can be auto-resolved (#189, #218)
|
|
767
|
+
const conflicted = this.git(["diff", "--name-only", "--diff-filter=U"], { allowFailure: true });
|
|
768
|
+
if (conflicted) {
|
|
769
|
+
const conflictedFiles = conflicted.split("\n").filter(Boolean);
|
|
770
|
+
const isRuntimeConflict = (f: string) =>
|
|
771
|
+
RUNTIME_EXCLUSION_PATHS.some(excl => f.startsWith(excl.replace(/\/$/, "")));
|
|
772
|
+
|
|
773
|
+
const runtimeConflicts = conflictedFiles.filter(isRuntimeConflict);
|
|
774
|
+
const gsdConflicts = conflictedFiles.filter(f => f.startsWith(".gsd/") && !isRuntimeConflict(f));
|
|
775
|
+
const otherConflicts = conflictedFiles.filter(
|
|
776
|
+
f => !isRuntimeConflict(f) && !f.startsWith(".gsd/"),
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
let resolvedAny = false;
|
|
780
|
+
|
|
781
|
+
if (runtimeConflicts.length > 0) {
|
|
782
|
+
// Runtime conflicts: take theirs and remove from index
|
|
783
|
+
for (const f of runtimeConflicts) {
|
|
784
|
+
this.git(["checkout", "--theirs", "--", f], { allowFailure: true });
|
|
785
|
+
this.git(["rm", "--cached", "--ignore-unmatch", f], { allowFailure: true });
|
|
786
|
+
}
|
|
787
|
+
resolvedAny = true;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (gsdConflicts.length > 0) {
|
|
791
|
+
// Non-runtime .gsd/ conflicts (DECISIONS.md, REQUIREMENTS.md, ROADMAP.md, etc.):
|
|
792
|
+
// The slice branch has the authoritative .gsd/ state since the LLM just finished
|
|
793
|
+
// updating these artifacts during complete-slice. Take theirs (the slice branch).
|
|
794
|
+
for (const f of gsdConflicts) {
|
|
795
|
+
this.git(["checkout", "--theirs", "--", f], { allowFailure: true });
|
|
796
|
+
}
|
|
797
|
+
resolvedAny = true;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (resolvedAny) {
|
|
801
|
+
this.git(["add", "-A"], { allowFailure: true });
|
|
802
|
+
|
|
803
|
+
// Re-check remaining conflicts after auto-resolving runtime and .gsd/ files
|
|
804
|
+
const remaining = this.git(["diff", "--name-only", "--diff-filter=U"], {
|
|
805
|
+
allowFailure: true,
|
|
806
|
+
});
|
|
807
|
+
if (remaining) {
|
|
808
|
+
const remainingFiles = remaining
|
|
809
|
+
.split("\n")
|
|
810
|
+
.filter(Boolean)
|
|
811
|
+
.filter(f => !isRuntimeConflict(f) && !f.startsWith(".gsd/"));
|
|
812
|
+
|
|
813
|
+
if (remainingFiles.length > 0) {
|
|
814
|
+
// Non-runtime, non-.gsd/ conflicts: leave working tree in conflicted state and throw
|
|
815
|
+
// MergeConflictError so the caller can dispatch a fix-merge session.
|
|
816
|
+
throw new MergeConflictError(remainingFiles, strategy, branch, mainBranch);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
// No remaining non-runtime, non-.gsd/ conflicts — let the merge proceed
|
|
820
|
+
} else {
|
|
821
|
+
// No runtime or .gsd/ conflicts to auto-resolve; throw with original conflicted files
|
|
822
|
+
// so the caller can dispatch a fix-merge session.
|
|
823
|
+
throw new MergeConflictError(otherConflicts.length ? otherConflicts : conflictedFiles, strategy, branch, mainBranch);
|
|
824
|
+
}
|
|
825
|
+
} else {
|
|
826
|
+
// No conflicted files detected but merge still failed — reset and throw
|
|
827
|
+
this.git(["reset", "--hard", "HEAD"], { allowFailure: true });
|
|
828
|
+
const msg = mergeError instanceof Error ? mergeError.message : String(mergeError);
|
|
829
|
+
throw new Error(
|
|
830
|
+
`${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" failed. ` +
|
|
831
|
+
`Working tree has been reset to a clean state. ` +
|
|
832
|
+
`Resolve manually: git checkout ${mainBranch} && git merge ${strategy === "merge" ? "--no-ff" : "--squash"} ${branch}\n` +
|
|
833
|
+
`Original error: ${msg}`,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Strip runtime files from the merge result before committing (#302).
|
|
839
|
+
// This replaces the old approach of checking out the slice branch to
|
|
840
|
+
// untrack runtime files pre-merge, which failed when the working tree
|
|
841
|
+
// had uncommitted .gsd/ changes that blocked the checkout.
|
|
842
|
+
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
|
|
843
|
+
this.git(["rm", "--cached", "-r", "--ignore-unmatch", exclusion], { allowFailure: true });
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (strategy === "squash") {
|
|
847
|
+
// After stripping runtime files, there may be nothing left to commit.
|
|
848
|
+
// This happens when the only changes in the slice were runtime artifacts.
|
|
849
|
+
const stagedDiff = this.git(["diff", "--cached", "--stat"], { allowFailure: true });
|
|
850
|
+
if (stagedDiff?.trim()) {
|
|
851
|
+
this.git(["commit", "--no-verify", "-F", "-"], { input: message });
|
|
852
|
+
} else {
|
|
853
|
+
// Nothing to commit — clean up the squash-merge state
|
|
854
|
+
this.git(["reset", "HEAD"], { allowFailure: true });
|
|
855
|
+
}
|
|
856
|
+
} else {
|
|
857
|
+
// --no-ff already committed; amend to include runtime file removal
|
|
858
|
+
const runtimeDiff = this.git(["diff", "--cached", "--stat"], { allowFailure: true });
|
|
859
|
+
if (runtimeDiff?.trim()) {
|
|
860
|
+
this.git(["commit", "--amend", "--no-edit", "--no-verify"]);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Delete the merged branch
|
|
865
|
+
this.git(["branch", "-D", branch]);
|
|
866
|
+
|
|
867
|
+
// Auto-push to remote if enabled
|
|
868
|
+
if (this.prefs.auto_push === true) {
|
|
869
|
+
const remote = this.prefs.remote ?? "origin";
|
|
870
|
+
const pushResult = this.git(["push", remote, mainBranch], { allowFailure: true });
|
|
871
|
+
if (pushResult === "") {
|
|
872
|
+
// push succeeded (empty stdout is normal) or failed silently
|
|
873
|
+
// Verify by checking if remote is reachable — the allowFailure handles errors
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return {
|
|
878
|
+
branch,
|
|
879
|
+
mergedCommitMessage: `${commitType}(${milestoneId}/${sliceId}): ${sliceTitle}`,
|
|
880
|
+
deletedBranch: true,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ─── Commit Type Inference ─────────────────────────────────────────────────
|
|
886
|
+
|
|
887
|
+
export function inferCommitType(sliceTitle: string): string {
|
|
888
|
+
const lower = sliceTitle.toLowerCase();
|
|
889
|
+
|
|
890
|
+
for (const [keywords, commitType] of COMMIT_TYPE_RULES) {
|
|
891
|
+
for (const keyword of keywords) {
|
|
892
|
+
// "clean up" is multi-word — use indexOf for it
|
|
893
|
+
if (keyword.includes(" ")) {
|
|
894
|
+
if (lower.includes(keyword)) return commitType;
|
|
895
|
+
} else {
|
|
896
|
+
// Word boundary match: keyword must not be surrounded by word chars
|
|
897
|
+
const re = new RegExp(`\\b${keyword}\\b`, "i");
|
|
898
|
+
if (re.test(lower)) return commitType;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return "feat";
|
|
904
|
+
}
|