@entelligentsia/forgecli 1.0.14 → 1.0.21
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/CHANGELOG.md +185 -0
- package/README.md +7 -1
- package/dist/CHANGELOG-forge-plugin.md +61 -0
- package/dist/bin/config.js +4 -4
- package/dist/bin/config.js.map +1 -1
- package/dist/bin/update-cli.d.ts +1 -1
- package/dist/bin/update-cli.js +1 -1
- package/dist/bin/update-cli.js.map +1 -1
- package/dist/extensions/forgecli/ask-user-tool.js +1 -1
- package/dist/extensions/forgecli/ask-user-tool.js.map +1 -1
- package/dist/extensions/forgecli/commands/add-pipeline.d.ts +19 -0
- package/dist/extensions/forgecli/commands/add-pipeline.js +143 -0
- package/dist/extensions/forgecli/commands/add-pipeline.js.map +1 -0
- package/dist/extensions/forgecli/commands/add-task.d.ts +20 -0
- package/dist/extensions/forgecli/commands/add-task.js +154 -0
- package/dist/extensions/forgecli/commands/add-task.js.map +1 -0
- package/dist/extensions/forgecli/commands/approve.d.ts +22 -0
- package/dist/extensions/forgecli/commands/approve.js +152 -0
- package/dist/extensions/forgecli/commands/approve.js.map +1 -0
- package/dist/extensions/forgecli/commands/collate.d.ts +22 -0
- package/dist/extensions/forgecli/commands/collate.js +134 -0
- package/dist/extensions/forgecli/commands/collate.js.map +1 -0
- package/dist/extensions/forgecli/commands/commit.d.ts +22 -0
- package/dist/extensions/forgecli/commands/commit.js +152 -0
- package/dist/extensions/forgecli/commands/commit.js.map +1 -0
- package/dist/extensions/forgecli/commands/config-command.d.ts +8 -0
- package/dist/extensions/forgecli/commands/config-command.js +67 -0
- package/dist/extensions/forgecli/commands/config-command.js.map +1 -0
- package/dist/extensions/forgecli/commands/enhance.d.ts +45 -0
- package/dist/extensions/forgecli/commands/enhance.js +219 -0
- package/dist/extensions/forgecli/commands/enhance.js.map +1 -0
- package/dist/extensions/forgecli/commands/implement.d.ts +22 -0
- package/dist/extensions/forgecli/commands/implement.js +170 -0
- package/dist/extensions/forgecli/commands/implement.js.map +1 -0
- package/dist/extensions/forgecli/commands/plan.d.ts +22 -0
- package/dist/extensions/forgecli/commands/plan.js +167 -0
- package/dist/extensions/forgecli/commands/plan.js.map +1 -0
- package/dist/extensions/forgecli/commands/quiz-agent.d.ts +17 -0
- package/dist/extensions/forgecli/commands/quiz-agent.js +98 -0
- package/dist/extensions/forgecli/commands/quiz-agent.js.map +1 -0
- package/dist/extensions/forgecli/commands/read-command.d.ts +2 -0
- package/dist/extensions/forgecli/commands/read-command.js +100 -0
- package/dist/extensions/forgecli/commands/read-command.js.map +1 -0
- package/dist/extensions/forgecli/commands/regenerate.d.ts +40 -0
- package/dist/extensions/forgecli/commands/regenerate.js +426 -0
- package/dist/extensions/forgecli/commands/regenerate.js.map +1 -0
- package/dist/extensions/forgecli/commands/remove-command.d.ts +17 -0
- package/dist/extensions/forgecli/commands/remove-command.js +124 -0
- package/dist/extensions/forgecli/commands/remove-command.js.map +1 -0
- package/dist/extensions/forgecli/commands/report-bug.d.ts +25 -0
- package/dist/extensions/forgecli/commands/report-bug.js +159 -0
- package/dist/extensions/forgecli/commands/report-bug.js.map +1 -0
- package/dist/extensions/forgecli/commands/retrospective.d.ts +20 -0
- package/dist/extensions/forgecli/commands/retrospective.js +126 -0
- package/dist/extensions/forgecli/commands/retrospective.js.map +1 -0
- package/dist/extensions/forgecli/commands/review-code.d.ts +35 -0
- package/dist/extensions/forgecli/commands/review-code.js +196 -0
- package/dist/extensions/forgecli/commands/review-code.js.map +1 -0
- package/dist/extensions/forgecli/commands/review-plan.d.ts +35 -0
- package/dist/extensions/forgecli/commands/review-plan.js +200 -0
- package/dist/extensions/forgecli/commands/review-plan.js.map +1 -0
- package/dist/extensions/forgecli/commands/sprint-intake.d.ts +10 -0
- package/dist/extensions/forgecli/commands/sprint-intake.js +91 -0
- package/dist/extensions/forgecli/commands/sprint-intake.js.map +1 -0
- package/dist/extensions/forgecli/commands/sprint-plan.d.ts +14 -0
- package/dist/extensions/forgecli/commands/sprint-plan.js +122 -0
- package/dist/extensions/forgecli/commands/sprint-plan.js.map +1 -0
- package/dist/extensions/forgecli/commands/status-command.d.ts +19 -0
- package/dist/extensions/forgecli/commands/status-command.js +140 -0
- package/dist/extensions/forgecli/commands/status-command.js.map +1 -0
- package/dist/extensions/forgecli/commands/store-query.d.ts +22 -0
- package/dist/extensions/forgecli/commands/store-query.js +107 -0
- package/dist/extensions/forgecli/commands/store-query.js.map +1 -0
- package/dist/extensions/forgecli/commands/store-repair.d.ts +17 -0
- package/dist/extensions/forgecli/commands/store-repair.js +123 -0
- package/dist/extensions/forgecli/commands/store-repair.js.map +1 -0
- package/dist/extensions/forgecli/commands/test-orchestrate.d.ts +2 -0
- package/dist/extensions/forgecli/commands/test-orchestrate.js +182 -0
- package/dist/extensions/forgecli/commands/test-orchestrate.js.map +1 -0
- package/dist/extensions/forgecli/commands/transcripts-command.d.ts +87 -0
- package/dist/extensions/forgecli/commands/transcripts-command.js +418 -0
- package/dist/extensions/forgecli/commands/transcripts-command.js.map +1 -0
- package/dist/extensions/forgecli/commands/validate.d.ts +22 -0
- package/dist/extensions/forgecli/commands/validate.js +152 -0
- package/dist/extensions/forgecli/commands/validate.js.map +1 -0
- package/dist/extensions/forgecli/config/config-layer.d.ts +53 -0
- package/dist/extensions/forgecli/config/config-layer.js +72 -0
- package/dist/extensions/forgecli/config/config-layer.js.map +1 -0
- package/dist/extensions/forgecli/config/config-writer.d.ts +16 -0
- package/dist/extensions/forgecli/config/config-writer.js +69 -0
- package/dist/extensions/forgecli/config/config-writer.js.map +1 -0
- package/dist/extensions/forgecli/config/model-registry.d.ts +61 -0
- package/dist/extensions/forgecli/config/model-registry.js +127 -0
- package/dist/extensions/forgecli/config/model-registry.js.map +1 -0
- package/dist/extensions/forgecli/config/model-resolver.d.ts +32 -0
- package/dist/extensions/forgecli/config/model-resolver.js +65 -0
- package/dist/extensions/forgecli/config/model-resolver.js.map +1 -0
- package/dist/extensions/forgecli/config/model-validator.d.ts +29 -0
- package/dist/extensions/forgecli/config/model-validator.js +107 -0
- package/dist/extensions/forgecli/config/model-validator.js.map +1 -0
- package/dist/extensions/forgecli/config-layer.d.ts +0 -16
- package/dist/extensions/forgecli/config-layer.js +0 -5
- package/dist/extensions/forgecli/config-layer.js.map +1 -1
- package/dist/extensions/forgecli/config-tui/component.js +1 -1
- package/dist/extensions/forgecli/config-tui/component.js.map +1 -1
- package/dist/extensions/forgecli/config-tui/handler.js +1 -1
- package/dist/extensions/forgecli/config-tui/handler.js.map +1 -1
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js +1 -1
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -1
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +1 -1
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -1
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +1 -1
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -1
- package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +2 -2
- package/dist/extensions/forgecli/config-tui/state/model.d.ts +1 -1
- package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -1
- package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +2 -2
- package/dist/extensions/forgecli/config-tui/state/selectors.js +1 -1
- package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -1
- package/dist/extensions/forgecli/context-governor-compaction.d.ts +94 -0
- package/dist/extensions/forgecli/context-governor-compaction.js +327 -0
- package/dist/extensions/forgecli/context-governor-compaction.js.map +1 -0
- package/dist/extensions/forgecli/context-governor.d.ts +169 -0
- package/dist/extensions/forgecli/context-governor.js +592 -0
- package/dist/extensions/forgecli/context-governor.js.map +1 -0
- package/dist/extensions/forgecli/dashboard/component.d.ts +17 -5
- package/dist/extensions/forgecli/dashboard/component.js +160 -115
- package/dist/extensions/forgecli/dashboard/component.js.map +1 -1
- package/dist/extensions/forgecli/dashboard/register.js +7 -21
- package/dist/extensions/forgecli/dashboard/register.js.map +1 -1
- package/dist/extensions/forgecli/dashboard/theme.d.ts +27 -0
- package/dist/extensions/forgecli/dashboard/theme.js +91 -0
- package/dist/extensions/forgecli/dashboard/theme.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.js +59 -5
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-artifact-tool.js +3 -2
- package/dist/extensions/forgecli/forge-artifact-tool.js.map +1 -1
- package/dist/extensions/forgecli/forge-cli-schema.json +0 -4
- package/dist/extensions/forgecli/forge-commands.js +1 -1
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-init/forge-init.d.ts +26 -0
- package/dist/extensions/forgecli/forge-init/forge-init.js +514 -0
- package/dist/extensions/forgecli/forge-init/forge-init.js.map +1 -0
- package/dist/extensions/forgecli/forge-init/init-context.d.ts +99 -0
- package/dist/extensions/forgecli/forge-init/init-context.js +178 -0
- package/dist/extensions/forgecli/forge-init/init-context.js.map +1 -0
- package/dist/extensions/forgecli/forge-init/init-progress.d.ts +39 -0
- package/dist/extensions/forgecli/forge-init/init-progress.js +117 -0
- package/dist/extensions/forgecli/forge-init/init-progress.js.map +1 -0
- package/dist/extensions/forgecli/forge-init/phase4-register.js +1 -1
- package/dist/extensions/forgecli/forge-init/phase4-register.js.map +1 -1
- package/dist/extensions/forgecli/forge-init/run-phases.js +2 -2
- package/dist/extensions/forgecli/forge-init/run-phases.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.d.ts +42 -1
- package/dist/extensions/forgecli/forge-subagent.js +59 -18
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/forge-tools.d.ts +0 -25
- package/dist/extensions/forgecli/forge-tools.js +8 -37
- package/dist/extensions/forgecli/forge-tools.js.map +1 -1
- package/dist/extensions/forgecli/governor-config.d.ts +19 -0
- package/dist/extensions/forgecli/governor-config.js +58 -0
- package/dist/extensions/forgecli/governor-config.js.map +1 -0
- package/dist/extensions/forgecli/health-check.js +1 -1
- package/dist/extensions/forgecli/health-check.js.map +1 -1
- package/dist/extensions/forgecli/hook-dispatcher.d.ts +3 -1
- package/dist/extensions/forgecli/hook-dispatcher.js +39 -5
- package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
- package/dist/extensions/forgecli/hooks/post-init-hook.js +11 -6
- package/dist/extensions/forgecli/hooks/post-init-hook.js.map +1 -1
- package/dist/extensions/forgecli/hooks/post-sprint-hook.js +11 -6
- package/dist/extensions/forgecli/hooks/post-sprint-hook.js.map +1 -1
- package/dist/extensions/forgecli/hooks/write-guard.js +1 -1
- package/dist/extensions/forgecli/hooks/write-guard.js.map +1 -1
- package/dist/extensions/forgecli/index.js +70 -36
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/kickoff.d.ts +9 -0
- package/dist/extensions/forgecli/kickoff.js +15 -0
- package/dist/extensions/forgecli/kickoff.js.map +1 -1
- package/dist/extensions/forgecli/lib/forge-config.d.ts +1 -1
- package/dist/extensions/forgecli/lib/forge-config.js +1 -1
- package/dist/extensions/forgecli/lib/forge-config.js.map +1 -1
- package/dist/extensions/forgecli/lib/forge-root.d.ts +10 -0
- package/dist/extensions/forgecli/lib/forge-root.js +62 -0
- package/dist/extensions/forgecli/lib/forge-root.js.map +1 -0
- package/dist/extensions/forgecli/lib/halt-advisor.d.ts +19 -14
- package/dist/extensions/forgecli/lib/halt-advisor.js +36 -13
- package/dist/extensions/forgecli/lib/halt-advisor.js.map +1 -1
- package/dist/extensions/forgecli/lib/run-cjs.d.ts +26 -0
- package/dist/extensions/forgecli/lib/run-cjs.js +42 -0
- package/dist/extensions/forgecli/lib/run-cjs.js.map +1 -0
- package/dist/extensions/forgecli/orchestrator-status-bar.d.ts +3 -2
- package/dist/extensions/forgecli/orchestrator-status-bar.js +90 -60
- package/dist/extensions/forgecli/orchestrator-status-bar.js.map +1 -1
- package/dist/extensions/forgecli/orchestrator-tree.d.ts +4 -0
- package/dist/extensions/forgecli/orchestrator-tree.js +21 -3
- package/dist/extensions/forgecli/orchestrator-tree.js.map +1 -1
- package/dist/extensions/forgecli/orchestrators/calibrate.d.ts +64 -0
- package/dist/extensions/forgecli/orchestrators/calibrate.js +481 -0
- package/dist/extensions/forgecli/orchestrators/calibrate.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/fix-bug.d.ts +93 -0
- package/dist/extensions/forgecli/orchestrators/fix-bug.js +1705 -0
- package/dist/extensions/forgecli/orchestrators/fix-bug.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/halt-advisor.d.ts +59 -0
- package/dist/extensions/forgecli/orchestrators/halt-advisor.js +113 -0
- package/dist/extensions/forgecli/orchestrators/halt-advisor.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/materialize.d.ts +16 -0
- package/dist/extensions/forgecli/orchestrators/materialize.js +195 -0
- package/dist/extensions/forgecli/orchestrators/materialize.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/migrate.d.ts +22 -0
- package/dist/extensions/forgecli/orchestrators/migrate.js +260 -0
- package/dist/extensions/forgecli/orchestrators/migrate.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/orchestrator-preflight.d.ts +46 -0
- package/dist/extensions/forgecli/orchestrators/orchestrator-preflight.js +64 -0
- package/dist/extensions/forgecli/orchestrators/orchestrator-preflight.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/run-sprint.d.ts +27 -0
- package/dist/extensions/forgecli/orchestrators/run-sprint.js +734 -0
- package/dist/extensions/forgecli/orchestrators/run-sprint.js.map +1 -0
- package/dist/extensions/forgecli/orchestrators/run-task.d.ts +215 -0
- package/dist/extensions/forgecli/orchestrators/run-task.js +1491 -0
- package/dist/extensions/forgecli/orchestrators/run-task.js.map +1 -0
- package/dist/extensions/forgecli/paths/paths.d.ts +8 -0
- package/dist/extensions/forgecli/paths/paths.js +17 -0
- package/dist/extensions/forgecli/paths/paths.js.map +1 -1
- package/dist/extensions/forgecli/phase-vocab.d.ts +31 -0
- package/dist/extensions/forgecli/phase-vocab.js +82 -0
- package/dist/extensions/forgecli/phase-vocab.js.map +1 -0
- package/dist/extensions/forgecli/run-sprint.d.ts +3 -1
- package/dist/extensions/forgecli/run-sprint.js +1 -0
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.d.ts +34 -1
- package/dist/extensions/forgecli/run-task.js +144 -6
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/session-registry.d.ts +2 -2
- package/dist/extensions/forgecli/session-registry.js +6 -2
- package/dist/extensions/forgecli/session-registry.js.map +1 -1
- package/dist/extensions/forgecli/skill-curation/friction-emit.d.ts +99 -0
- package/dist/extensions/forgecli/skill-curation/friction-emit.js +245 -0
- package/dist/extensions/forgecli/skill-curation/friction-emit.js.map +1 -0
- package/dist/extensions/forgecli/skill-curation/skill-curation-flag.d.ts +21 -0
- package/dist/extensions/forgecli/skill-curation/skill-curation-flag.js +71 -0
- package/dist/extensions/forgecli/skill-curation/skill-curation-flag.js.map +1 -0
- package/dist/extensions/forgecli/skill-curation/skill-curator-subagent.d.ts +102 -0
- package/dist/extensions/forgecli/skill-curation/skill-curator-subagent.js +339 -0
- package/dist/extensions/forgecli/skill-curation/skill-curator-subagent.js.map +1 -0
- package/dist/extensions/forgecli/skill-curation/skill-retriever.d.ts +84 -0
- package/dist/extensions/forgecli/skill-curation/skill-retriever.js +246 -0
- package/dist/extensions/forgecli/skill-curation/skill-retriever.js.map +1 -0
- package/dist/extensions/forgecli/skill-curation/skill-usage-tracker.d.ts +91 -0
- package/dist/extensions/forgecli/skill-curation/skill-usage-tracker.js +224 -0
- package/dist/extensions/forgecli/skill-curation/skill-usage-tracker.js.map +1 -0
- package/dist/extensions/forgecli/store/store-error-remediation.d.ts +65 -0
- package/dist/extensions/forgecli/store/store-error-remediation.js +307 -0
- package/dist/extensions/forgecli/store/store-error-remediation.js.map +1 -0
- package/dist/extensions/forgecli/store/store-resolver.d.ts +56 -0
- package/dist/extensions/forgecli/store/store-resolver.js +263 -0
- package/dist/extensions/forgecli/store/store-resolver.js.map +1 -0
- package/dist/extensions/forgecli/store/store-validator.d.ts +16 -0
- package/dist/extensions/forgecli/store/store-validator.js +32 -0
- package/dist/extensions/forgecli/store/store-validator.js.map +1 -0
- package/dist/extensions/forgecli/store/transition-guard.d.ts +20 -0
- package/dist/extensions/forgecli/store/transition-guard.js +89 -0
- package/dist/extensions/forgecli/store/transition-guard.js.map +1 -0
- package/dist/extensions/forgecli/subagent/orchestrator-transcript.js +5 -0
- package/dist/extensions/forgecli/subagent/orchestrator-transcript.js.map +1 -1
- package/dist/extensions/forgecli/thread-switcher.d.ts +4 -1
- package/dist/extensions/forgecli/thread-switcher.js +36 -21
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/transcript-archive-types.d.ts +171 -0
- package/dist/extensions/forgecli/transcript-archive-types.js +130 -0
- package/dist/extensions/forgecli/transcript-archive-types.js.map +1 -0
- package/dist/extensions/forgecli/transcript-archive.d.ts +127 -0
- package/dist/extensions/forgecli/transcript-archive.js +656 -0
- package/dist/extensions/forgecli/transcript-archive.js.map +1 -0
- package/dist/extensions/forgecli/transcript-replay.d.ts +28 -0
- package/dist/extensions/forgecli/transcript-replay.js +153 -0
- package/dist/extensions/forgecli/transcript-replay.js.map +1 -0
- package/dist/extensions/forgecli/transcripts-tui/component.d.ts +36 -0
- package/dist/extensions/forgecli/transcripts-tui/component.js +112 -0
- package/dist/extensions/forgecli/transcripts-tui/component.js.map +1 -0
- package/dist/extensions/forgecli/transcripts-tui/index.d.ts +4 -0
- package/dist/extensions/forgecli/transcripts-tui/index.js +5 -0
- package/dist/extensions/forgecli/transcripts-tui/index.js.map +1 -0
- package/dist/extensions/forgecli/transcripts-tui/screens/browse.d.ts +21 -0
- package/dist/extensions/forgecli/transcripts-tui/screens/browse.js +172 -0
- package/dist/extensions/forgecli/transcripts-tui/screens/browse.js.map +1 -0
- package/dist/extensions/forgecli/transcripts-tui/screens/types.d.ts +22 -0
- package/dist/extensions/forgecli/transcripts-tui/screens/types.js +4 -0
- package/dist/extensions/forgecli/transcripts-tui/screens/types.js.map +1 -0
- package/dist/extensions/forgecli/transcripts-tui/state/index.d.ts +4 -0
- package/dist/extensions/forgecli/transcripts-tui/state/index.js +5 -0
- package/dist/extensions/forgecli/transcripts-tui/state/index.js.map +1 -0
- package/dist/extensions/forgecli/transcripts-tui/state/init.d.ts +8 -0
- package/dist/extensions/forgecli/transcripts-tui/state/init.js +18 -0
- package/dist/extensions/forgecli/transcripts-tui/state/init.js.map +1 -0
- package/dist/extensions/forgecli/transcripts-tui/state/model.d.ts +56 -0
- package/dist/extensions/forgecli/transcripts-tui/state/model.js +6 -0
- package/dist/extensions/forgecli/transcripts-tui/state/model.js.map +1 -0
- package/dist/extensions/forgecli/transcripts-tui/state/reducer.d.ts +2 -0
- package/dist/extensions/forgecli/transcripts-tui/state/reducer.js +51 -0
- package/dist/extensions/forgecli/transcripts-tui/state/reducer.js.map +1 -0
- package/dist/extensions/forgecli/transcripts-tui/state/selectors.d.ts +10 -0
- package/dist/extensions/forgecli/transcripts-tui/state/selectors.js +62 -0
- package/dist/extensions/forgecli/transcripts-tui/state/selectors.js.map +1 -0
- package/dist/extensions/forgecli/transcripts-tui/theme.d.ts +20 -0
- package/dist/extensions/forgecli/transcripts-tui/theme.js +47 -0
- package/dist/extensions/forgecli/transcripts-tui/theme.js.map +1 -0
- package/dist/extensions/forgecli/tui/banner.d.ts +10 -0
- package/dist/extensions/forgecli/tui/banner.js +36 -0
- package/dist/extensions/forgecli/tui/banner.js.map +1 -0
- package/dist/extensions/forgecli/tui/forge-header.d.ts +12 -0
- package/dist/extensions/forgecli/tui/forge-header.js +114 -0
- package/dist/extensions/forgecli/tui/forge-header.js.map +1 -0
- package/dist/extensions/forgecli/tui/input-router.d.ts +33 -0
- package/dist/extensions/forgecli/tui/input-router.js +136 -0
- package/dist/extensions/forgecli/tui/input-router.js.map +1 -0
- package/dist/extensions/forgecli/tui/orchestrator-status-bar.d.ts +26 -0
- package/dist/extensions/forgecli/tui/orchestrator-status-bar.js +213 -0
- package/dist/extensions/forgecli/tui/orchestrator-status-bar.js.map +1 -0
- package/dist/extensions/forgecli/tui/thread-switcher.d.ts +18 -0
- package/dist/extensions/forgecli/tui/thread-switcher.js +194 -0
- package/dist/extensions/forgecli/tui/thread-switcher.js.map +1 -0
- package/dist/extensions/forgecli/update/forge-update-command.d.ts +100 -0
- package/dist/extensions/forgecli/update/forge-update-command.js +435 -0
- package/dist/extensions/forgecli/update/forge-update-command.js.map +1 -0
- package/dist/extensions/forgecli/update/migration-engine.d.ts +117 -0
- package/dist/extensions/forgecli/update/migration-engine.js +563 -0
- package/dist/extensions/forgecli/update/migration-engine.js.map +1 -0
- package/dist/extensions/forgecli/update/update-check.d.ts +37 -0
- package/dist/extensions/forgecli/update/update-check.js +185 -0
- package/dist/extensions/forgecli/update/update-check.js.map +1 -0
- package/dist/extensions/forgecli/update/update-tools.d.ts +23 -0
- package/dist/extensions/forgecli/update/update-tools.js +135 -0
- package/dist/extensions/forgecli/update/update-tools.js.map +1 -0
- package/dist/extensions/forgecli/update/whats-new-widget.d.ts +26 -0
- package/dist/extensions/forgecli/update/whats-new-widget.js +376 -0
- package/dist/extensions/forgecli/update/whats-new-widget.js.map +1 -0
- package/dist/extensions/forgecli/update/whats-new.d.ts +120 -0
- package/dist/extensions/forgecli/update/whats-new.js +470 -0
- package/dist/extensions/forgecli/update/whats-new.js.map +1 -0
- package/dist/extensions/forgecli/viewport/events.d.ts +113 -0
- package/dist/extensions/forgecli/viewport/events.js +290 -0
- package/dist/extensions/forgecli/viewport/events.js.map +1 -0
- package/dist/extensions/forgecli/viewport/renderer.d.ts +102 -0
- package/dist/extensions/forgecli/viewport/renderer.js +277 -0
- package/dist/extensions/forgecli/viewport/renderer.js.map +1 -0
- package/dist/extensions/forgecli/viewport/theme.d.ts +11 -0
- package/dist/extensions/forgecli/viewport/theme.js +131 -0
- package/dist/extensions/forgecli/viewport/theme.js.map +1 -0
- package/dist/extensions/forgecli/wf-engine/engine.js +1 -1
- package/dist/extensions/forgecli/wf-engine/engine.js.map +1 -1
- package/dist/forge-payload/.base-pack/workflows/implement_plan.md +9 -0
- package/dist/forge-payload/.base-pack/workflows/plan_task.md +7 -0
- package/dist/forge-payload/.base-pack/workflows/review_code.md +4 -3
- package/dist/forge-payload/.base-pack/workflows/review_plan.md +4 -3
- package/dist/forge-payload/.base-pack/workflows/validate_task.md +4 -3
- package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
- package/dist/forge-payload/.schemas/migrations.json +132 -27
- package/dist/forge-payload/meta/workflows/meta-review-implementation.md +4 -3
- package/dist/forge-payload/meta/workflows/meta-review-plan.md +4 -3
- package/dist/forge-payload/meta/workflows/meta-validate.md +4 -3
- package/dist/forge-payload/tools/collate.cjs +32 -0
- package/dist/forge-payload/tools/postflight-gate.cjs +56 -10
- package/package.json +5 -3
|
@@ -0,0 +1,1705 @@
|
|
|
1
|
+
// fix-bug.ts — /forge:fix-bug Orchestrator native handler (FORGE-S21-T07).
|
|
2
|
+
//
|
|
3
|
+
// Promotes /forge:fix-bug from stub to a full TS-driven Orchestrator-archetype
|
|
4
|
+
// native handler. Reads `.forge/workflows/fix_bug.md`, chains the bug-specific
|
|
5
|
+
// phase sequence (triage → plan-fix → review-plan → implement → review-code →
|
|
6
|
+
// approve → commit) by spawning a fresh runForgeSubagent per phase (IL10).
|
|
7
|
+
//
|
|
8
|
+
// Iron Laws enforced here:
|
|
9
|
+
// IL1 — code only under forge-cli/src/extensions/forgecli/
|
|
10
|
+
// IL6 — no shell-string interpolation; all external calls via spawnSync argv arrays
|
|
11
|
+
// IL7 — every failure path emits ctx.ui.notify and returns; no silent continuation
|
|
12
|
+
// IL10 — ALL LLM dispatch goes through runForgeSubagent (NO sendKickoff calls here)
|
|
13
|
+
//
|
|
14
|
+
// sendKickoff is NEVER called from this file.
|
|
15
|
+
// Audit-grep: grep -n "sendKickoff(" fix-bug.ts must return empty.
|
|
16
|
+
//
|
|
17
|
+
// N-H-C — bugId dual-assignment lifecycle:
|
|
18
|
+
// Phase 1 (handler entry, ~line 1372): bugId = `PENDING-${Date.now()}`, isNewBug = true.
|
|
19
|
+
// A temporary placeholder; the timestamp is later used to find the real bug record.
|
|
20
|
+
// Phase 2 (pre-init, ~line 1495–1500): preCreateBug() writes a minimal bug record with a
|
|
21
|
+
// real FORGE-BUG-NNN ID so the triage subagent has a stable ID to reference.
|
|
22
|
+
// If preCreateBug fails, the PENDING- placeholder is kept for fallback capture.
|
|
23
|
+
// Phase 3 (post-triage, ~line 962–989): capture real ID from BugCreated events emitted by
|
|
24
|
+
// the triage subagent; fall back to listing the most-recent bug after pipelineStart if
|
|
25
|
+
// event capture fails. The PENDING- prefix is used throughout as a guard for
|
|
26
|
+
// state-write paths (see ~line 176 and CallerContextStore guards).
|
|
27
|
+
// Reference: PENDING- prefix semantics defined in CallerContextStore guards.
|
|
28
|
+
//
|
|
29
|
+
// N-H-H — Preflight gate design (closed by FORGE-S25-T17):
|
|
30
|
+
// Entry-level: runOrchestratorPreflight is called at runBugPipeline entry (~line 523).
|
|
31
|
+
// Validates persona/model config before any LLM dispatch (mirrors run-task.ts design).
|
|
32
|
+
// Per-phase: runPreflightGate (store-cli gate) is called per phase (~line 667).
|
|
33
|
+
// Evaluates declarative gate conditions from the workflow's gate block.
|
|
34
|
+
// This two-level design ensures both structural validity (model/persona config) and
|
|
35
|
+
// store-state validity (predecessor verdicts, status guards) are checked.
|
|
36
|
+
// Reference: orchestrator-preflight.ts (N-H-H, FORGE-S25-T17).
|
|
37
|
+
//
|
|
38
|
+
// N-H-E tag: see inline comment at the materialization skip (~line 707 / checkMaterialization).
|
|
39
|
+
import { spawnSync } from "node:child_process";
|
|
40
|
+
import * as fs from "node:fs";
|
|
41
|
+
import * as path from "node:path";
|
|
42
|
+
import { fileURLToPath } from "node:url";
|
|
43
|
+
import { assertAudience, CallerContextStore } from "../audience-gate.js";
|
|
44
|
+
// ModelRegistry/AuthStorage no longer instantiated here — use ctx.modelRegistry
|
|
45
|
+
// so extension-registered providers (registered against the live session) are
|
|
46
|
+
// visible to validateModelConfig. Creating a fresh registry here would miss
|
|
47
|
+
// them and produce spurious MODEL_UNAVAILABLE warnings (FORGE-BUG-001).
|
|
48
|
+
import { loadLayeredConfig } from "../config/config-layer.js";
|
|
49
|
+
import { resolveModelForPhase } from "../config/model-resolver.js";
|
|
50
|
+
import { loadForgePersona, runForgeSubagent } from "../forge-subagent.js";
|
|
51
|
+
import { getSubagentTools } from "../forge-tools.js";
|
|
52
|
+
import { loadGovernorProjectConfig } from "../governor-config.js";
|
|
53
|
+
import { readPersonaDir as readPersonaDirBug, readPipelineNames as readPipelineNamesBug, } from "../lib/catalog-helpers.js";
|
|
54
|
+
import { discoverForgeConfigCached } from "../lib/forge-config.js";
|
|
55
|
+
import { checkMaterialization } from "../lib/manifest-checker.js";
|
|
56
|
+
import { getOrchestratorTree } from "../orchestrator-tree.js";
|
|
57
|
+
import { loadWorkflow } from "../parsers/workflow-loader.js";
|
|
58
|
+
import { getSessionRegistry } from "../session-registry.js";
|
|
59
|
+
import { resolveToCanonicalId, resolveToolDir } from "../store/store-resolver.js";
|
|
60
|
+
import { OrchestratorTranscriptWriter } from "../subagent/orchestrator-transcript.js";
|
|
61
|
+
import { archiveRun, sweepProjectTranscripts } from "../transcript-archive.js";
|
|
62
|
+
import { attachViewportObserver } from "../viewport/events.js";
|
|
63
|
+
import { fmtPhaseSummary } from "../viewport/renderer.js";
|
|
64
|
+
import { resolveAdvisorModel, runHaltAdvisor } from "./halt-advisor.js";
|
|
65
|
+
import { runOrchestratorPreflight } from "./orchestrator-preflight.js";
|
|
66
|
+
import { buildPhaseEvent, buildSummariesBlock, drainFrictionFile, emitEvent, emitIncompletePhaseEvent, findPredecessorIndex, formatLocalTime, isNonInteractive, judgementFromSummary, runPreflightGateWithData, transcriptOutcomeFor, validateId, } from "./run-task.js";
|
|
67
|
+
// ── Bug phase descriptor table ──────────────────────────────────────────────
|
|
68
|
+
//
|
|
69
|
+
// Decoded from .forge/workflows/fix_bug.md and the task prompt's BUG_PHASES.
|
|
70
|
+
// triage / plan-fix / implement all read the same fix_bug.md body — the
|
|
71
|
+
// workflow handles all three phases through prose.
|
|
72
|
+
// FORGE-S25-T16: readPersonaDirBug / readPipelineNamesBug extracted to
|
|
73
|
+
// lib/catalog-helpers.ts and imported above with aliases (H-4, N-H-G).
|
|
74
|
+
export const BUG_PHASES = [
|
|
75
|
+
// FORGE-BUG-040: each phase points at its own phase-scoped subagent workflow.
|
|
76
|
+
// Previously triage/plan-fix/implement all pointed at fix_bug.md (the
|
|
77
|
+
// orchestrator-only body), which caused the triage subagent to execute
|
|
78
|
+
// the full lifecycle in a single invocation. plan-fix and implement reuse
|
|
79
|
+
// plan_task.md / implement_plan.md (bug-mode) per meta-fix-bug.md
|
|
80
|
+
// § Pipeline Phases — the bug-mode entity-kind detection is built into
|
|
81
|
+
// those workflows already.
|
|
82
|
+
{ role: "triage", workflowFile: "triage", personaNoun: "bug-fixer", isReview: false, maxIterations: 1 },
|
|
83
|
+
{ role: "plan-fix", workflowFile: "plan_task", personaNoun: "engineer", isReview: false, maxIterations: 1 },
|
|
84
|
+
{ role: "review-plan", workflowFile: "review_plan", personaNoun: "supervisor", isReview: true, maxIterations: 3 },
|
|
85
|
+
{ role: "implement", workflowFile: "implement_plan", personaNoun: "engineer", isReview: false, maxIterations: 1 },
|
|
86
|
+
{ role: "review-code", workflowFile: "review_code", personaNoun: "supervisor", isReview: true, maxIterations: 3 },
|
|
87
|
+
{ role: "approve", workflowFile: "architect_approve", personaNoun: "architect", isReview: true, maxIterations: 3 },
|
|
88
|
+
{ role: "commit", workflowFile: "commit_task", personaNoun: "engineer", isReview: false, maxIterations: 1 },
|
|
89
|
+
];
|
|
90
|
+
// FORGE-BUG-040: BUG_SUMMARY_KEY_BY_ROLE lives in
|
|
91
|
+
// subagent/phase-summary-map.ts so the new phase-guard.ts can import
|
|
92
|
+
// it without dragging fix-bug.ts into a forge-tools import cycle.
|
|
93
|
+
// Re-exported here for backwards-compatibility with existing call sites.
|
|
94
|
+
export { BUG_SUMMARY_KEY_BY_ROLE } from "../subagent/phase-summary-map.js";
|
|
95
|
+
import { BUG_SUMMARY_KEY_BY_ROLE } from "../subagent/phase-summary-map.js";
|
|
96
|
+
// Bug-event type tokens — explicit mapping per review finding #3.
|
|
97
|
+
// Non-review phases always emit the pass token. Review phases select
|
|
98
|
+
// pass or fail based on ec.judgement.verdict.
|
|
99
|
+
export const BUG_TYPE_TOKENS = {
|
|
100
|
+
triage: { pass: "bug-triaged", fail: "bug-triaged" },
|
|
101
|
+
"plan-fix": { pass: "fix-planned", fail: "fix-planned" },
|
|
102
|
+
"review-plan": { pass: "fix-review-passed", fail: "fix-review-failed" },
|
|
103
|
+
implement: { pass: "fix-implemented", fail: "fix-implemented" },
|
|
104
|
+
"review-code": { pass: "fix-code-review-passed", fail: "fix-code-review-failed" },
|
|
105
|
+
approve: { pass: "fix-approved", fail: "fix-revision-requested" },
|
|
106
|
+
commit: { pass: "bug-committed", fail: "bug-commit-failed" },
|
|
107
|
+
};
|
|
108
|
+
// ── Bug FSM transitions ────────────────────────────────────────────────────
|
|
109
|
+
// Mirrors store-cli BUG_TRANSITIONS. Terminal: `fixed`.
|
|
110
|
+
// `approved` and `verified` enum values were dropped in forge v0.44.0
|
|
111
|
+
// (FORGE-BUG-002 trap). The canonical source is store-cli.cjs.
|
|
112
|
+
const BUG_TERMINAL_STATES = new Set(["fixed"]);
|
|
113
|
+
function bugStateFilePath(cwd, bugId, sessionId) {
|
|
114
|
+
if (!validateId(bugId)) {
|
|
115
|
+
throw new Error(`Invalid bugId for state file path: ${bugId}`);
|
|
116
|
+
}
|
|
117
|
+
const suffix = sessionId ?? process.env.FORGE_SESSION_ID ?? `${process.pid}`;
|
|
118
|
+
return path.join(cwd, ".forge", "cache", `fix-bug-state-${bugId}-${suffix}.json`);
|
|
119
|
+
}
|
|
120
|
+
export function readBugState(cwd, bugId, sessionId) {
|
|
121
|
+
// If a specific session ID is given, read that file directly.
|
|
122
|
+
if (sessionId || process.env.FORGE_SESSION_ID) {
|
|
123
|
+
const fp = bugStateFilePath(cwd, bugId, sessionId);
|
|
124
|
+
try {
|
|
125
|
+
if (!fs.existsSync(fp))
|
|
126
|
+
return null;
|
|
127
|
+
const raw = fs.readFileSync(fp, "utf8");
|
|
128
|
+
return JSON.parse(raw);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// No specific session — glob for the most recent matching state file.
|
|
135
|
+
// Single-writer assumption: normally only one session per bug.
|
|
136
|
+
const cacheDir = path.join(cwd, ".forge", "cache");
|
|
137
|
+
const prefix = `fix-bug-state-${bugId}-`;
|
|
138
|
+
let bestFile = null;
|
|
139
|
+
let bestMtime = 0;
|
|
140
|
+
try {
|
|
141
|
+
const entries = fs.readdirSync(cacheDir);
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
if (!entry.startsWith(prefix) || !entry.endsWith(".json"))
|
|
144
|
+
continue;
|
|
145
|
+
const fp = path.join(cacheDir, entry);
|
|
146
|
+
try {
|
|
147
|
+
const st = fs.statSync(fp);
|
|
148
|
+
if (st.mtimeMs > bestMtime) {
|
|
149
|
+
bestMtime = st.mtimeMs;
|
|
150
|
+
bestFile = fp;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch { }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (!bestFile)
|
|
160
|
+
return null;
|
|
161
|
+
try {
|
|
162
|
+
const raw = fs.readFileSync(bestFile, "utf8");
|
|
163
|
+
return JSON.parse(raw);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function writeBugState(cwd, state) {
|
|
170
|
+
// Guard: never write state for PENDING bugIds — wait for real bugId capture.
|
|
171
|
+
if (state.bugId.startsWith("PENDING-"))
|
|
172
|
+
return;
|
|
173
|
+
const fp = bugStateFilePath(cwd, state.bugId);
|
|
174
|
+
const dir = path.dirname(fp);
|
|
175
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
176
|
+
fs.writeFileSync(fp, JSON.stringify(state, null, 2), "utf8");
|
|
177
|
+
}
|
|
178
|
+
export function deleteBugState(cwd, bugId) {
|
|
179
|
+
// Clean up all state files for this bug (all sessions)
|
|
180
|
+
const cacheDir = path.join(cwd, ".forge", "cache");
|
|
181
|
+
const statePrefix = `fix-bug-state-${bugId}-`;
|
|
182
|
+
const debugPrefix = `fix-bug-debug-${bugId}`;
|
|
183
|
+
try {
|
|
184
|
+
const entries = fs.readdirSync(cacheDir);
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
if ((entry.startsWith(statePrefix) && entry.endsWith(".json")) || entry.startsWith(debugPrefix)) {
|
|
187
|
+
try {
|
|
188
|
+
fs.unlinkSync(path.join(cacheDir, entry));
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* non-fatal */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// non-fatal
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export function isBugStateStale(state) {
|
|
201
|
+
const savedAt = new Date(state.savedAt).getTime();
|
|
202
|
+
const ageMs = Date.now() - savedAt;
|
|
203
|
+
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
|
204
|
+
return ageMs > sevenDaysMs;
|
|
205
|
+
}
|
|
206
|
+
export function readBugRecord(bugId, storeCli, cwd) {
|
|
207
|
+
const result = spawnSync("node", [storeCli, "read", "bug", bugId], { cwd, encoding: "utf8" });
|
|
208
|
+
if (result.status !== 0)
|
|
209
|
+
return null;
|
|
210
|
+
try {
|
|
211
|
+
const raw = typeof result.stdout === "string" ? result.stdout : String(result.stdout);
|
|
212
|
+
return JSON.parse(raw);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Pure helper: next <PREFIX>-BUG-NNN ID given the existing bug IDs.
|
|
219
|
+
// Only same-prefix bugs participate in the increment — exported for tests.
|
|
220
|
+
// The prefix is config-owned (project.prefix) and identifier-validated by
|
|
221
|
+
// loadGovernorProjectConfig, so it is safe to splice into a RegExp.
|
|
222
|
+
export function computeNextBugId(bugIds, prefix) {
|
|
223
|
+
const idPattern = new RegExp(`^${prefix}-BUG-(\\d+)$`);
|
|
224
|
+
let maxNum = 0;
|
|
225
|
+
for (const id of bugIds) {
|
|
226
|
+
const m = idPattern.exec(id);
|
|
227
|
+
if (m) {
|
|
228
|
+
const n = parseInt(m[1], 10);
|
|
229
|
+
if (n > maxNum)
|
|
230
|
+
maxNum = n;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return `${prefix}-BUG-${String(maxNum + 1).padStart(3, "0")}`;
|
|
234
|
+
}
|
|
235
|
+
// Pre-assigns a real <PREFIX>-BUG-NNN ID by listing existing bugs and
|
|
236
|
+
// incrementing. Prefix comes from .forge/config.json project.prefix — the
|
|
237
|
+
// hardcoded FORGE prefix minted phantom FORGE-BUG-* records in any project
|
|
238
|
+
// with a different prefix (CART testbench incident, FORGE-BUG-043 class).
|
|
239
|
+
export function assignNextBugId(storeCli, cwd, prefix = "FORGE") {
|
|
240
|
+
const result = spawnSync("node", [storeCli, "list", "bug", "--json"], { cwd, encoding: "utf8" });
|
|
241
|
+
let bugIds = [];
|
|
242
|
+
if (result.status === 0 && result.stdout) {
|
|
243
|
+
try {
|
|
244
|
+
const bugs = JSON.parse(result.stdout);
|
|
245
|
+
if (Array.isArray(bugs)) {
|
|
246
|
+
bugIds = bugs.map((b) => String(b.bugId ?? ""));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
/* empty store — start from 1 */
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return computeNextBugId(bugIds, prefix);
|
|
254
|
+
}
|
|
255
|
+
// Extracts the first canonical <PREFIX>-BUG-NNN referenced in a bug-report
|
|
256
|
+
// text (e.g. the "**Bug ID**: CART-BUG-001" header line BUG_REPORT.md files
|
|
257
|
+
// carry). Used by the @file intake path so /forge:fix-bug @BUG_REPORT.md
|
|
258
|
+
// operates on the referenced store record instead of minting a duplicate.
|
|
259
|
+
// Prefix is config-owned and identifier-validated (safe in a RegExp).
|
|
260
|
+
export function extractBugIdFromReportText(text, prefix) {
|
|
261
|
+
const m = text.match(new RegExp(`\\b${prefix}-BUG-\\d+\\b`));
|
|
262
|
+
return m ? m[0] : null;
|
|
263
|
+
}
|
|
264
|
+
// Pre-creates a minimal bug record so the subagent has a real ID to work with.
|
|
265
|
+
export function preCreateBug(bugId, title, storeCli, cwd) {
|
|
266
|
+
const data = {
|
|
267
|
+
bugId,
|
|
268
|
+
title,
|
|
269
|
+
severity: "minor",
|
|
270
|
+
status: "reported",
|
|
271
|
+
path: `engineering/bugs/${bugId}`,
|
|
272
|
+
reportedAt: new Date().toISOString(),
|
|
273
|
+
};
|
|
274
|
+
const result = spawnSync("node", [storeCli, "write", "bug", JSON.stringify(data)], { cwd, encoding: "utf8" });
|
|
275
|
+
return result.status === 0;
|
|
276
|
+
}
|
|
277
|
+
export function readBugVerdict(bugRecord, phaseRole, summaryKeyByRole) {
|
|
278
|
+
if (!bugRecord)
|
|
279
|
+
return "missing";
|
|
280
|
+
// Approve phase: read approve summary verdict (set via set-bug-summary).
|
|
281
|
+
// The forge v0.44.0 contract makes summaries.approve.verdict the canonical
|
|
282
|
+
// approve signal for bugs — `bug.status` does NOT carry an "approved"
|
|
283
|
+
// value (that enum was dropped). See read-verdict.cjs §
|
|
284
|
+
// BUG_PHASE_VERDICT_SOURCE for the matching plugin-side wiring.
|
|
285
|
+
if (phaseRole === "approve") {
|
|
286
|
+
const summaryKey = summaryKeyByRole["approve"];
|
|
287
|
+
if (summaryKey) {
|
|
288
|
+
const summaries = bugRecord.summaries ?? {};
|
|
289
|
+
const blob = summaries[summaryKey];
|
|
290
|
+
if (blob && typeof blob === "object") {
|
|
291
|
+
const verdict = blob?.verdict;
|
|
292
|
+
if (typeof verdict === "string") {
|
|
293
|
+
if (verdict === "approved")
|
|
294
|
+
return "approved";
|
|
295
|
+
if (verdict === "revision")
|
|
296
|
+
return "revision";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return "missing";
|
|
301
|
+
}
|
|
302
|
+
// Commit phase: read bug status directly. Terminal target is `fixed`.
|
|
303
|
+
if (phaseRole === "commit") {
|
|
304
|
+
if (bugRecord.status === "fixed")
|
|
305
|
+
return "approved";
|
|
306
|
+
// in-progress means commit did not advance status — treat as revision-needed.
|
|
307
|
+
if (bugRecord.status === "in-progress")
|
|
308
|
+
return "revision";
|
|
309
|
+
return "missing";
|
|
310
|
+
}
|
|
311
|
+
// Review phases: read from summaries via key map.
|
|
312
|
+
const summaryKey = summaryKeyByRole[phaseRole];
|
|
313
|
+
if (!summaryKey)
|
|
314
|
+
return "missing";
|
|
315
|
+
const summaries = bugRecord.summaries ?? {};
|
|
316
|
+
const blob = summaries[summaryKey];
|
|
317
|
+
if (!blob || typeof blob !== "object")
|
|
318
|
+
return "missing";
|
|
319
|
+
const verdict = blob?.verdict;
|
|
320
|
+
if (typeof verdict !== "string")
|
|
321
|
+
return "missing";
|
|
322
|
+
if (verdict === "approved")
|
|
323
|
+
return "approved";
|
|
324
|
+
if (verdict === "revision")
|
|
325
|
+
return "revision";
|
|
326
|
+
return "missing";
|
|
327
|
+
}
|
|
328
|
+
// ── Bug body composition ──────────────────────────────────────────────────
|
|
329
|
+
export function composeBugBody(subWorkflowMd, bugId, phaseRole, bugStatusBeforePhase, summariesBlock) {
|
|
330
|
+
// Entity-kind override block prepended before workflow body.
|
|
331
|
+
// Conforms to forge v0.44.x meta-fix-bug contract:
|
|
332
|
+
// - bug.status enum is {reported, triaged, in-progress, fixed}; `fixed` is terminal.
|
|
333
|
+
// - `approved` and `verified` are NOT valid bug status values (dropped in v0.44.0).
|
|
334
|
+
// - Approve phase: NO status write. Architect writes summaries.approve.verdict
|
|
335
|
+
// via set-bug-summary; verdict signal IS the summary (read by
|
|
336
|
+
// read-verdict.cjs § BUG_PHASE_VERDICT_SOURCE).
|
|
337
|
+
// - Commit phase: status → fixed (the only status transition post-triage).
|
|
338
|
+
//
|
|
339
|
+
// Earlier revisions of this prompt told the architect to write
|
|
340
|
+
// `update-status bug ... approved` and the engineer to write `... verified`.
|
|
341
|
+
// Those instructions produced the FORGE-BUG-002 trap (LLM-translation of
|
|
342
|
+
// task-shaped approve workflow → illegal transition through a terminal state).
|
|
343
|
+
// The new contract removes the trap at its source.
|
|
344
|
+
const entityKindLines = [
|
|
345
|
+
`Bug ID: ${bugId}`,
|
|
346
|
+
"",
|
|
347
|
+
"⚠ ENTITY KIND OVERRIDE: This is a bug, not a task.",
|
|
348
|
+
"- All `update-status` calls must use entity kind `bug` (not `task`).",
|
|
349
|
+
"- Approve phase: NO status write. Write the approval verdict via set-bug-summary:",
|
|
350
|
+
` node "$FORGE_ROOT/tools/store-cli.cjs" set-bug-summary ${bugId} approve <APPROVE-SUMMARY.json>`,
|
|
351
|
+
` The summary's "verdict" field MUST be "approved" or "revision". The downstream commit gate reads this, not bug.status.`,
|
|
352
|
+
`- Commit phase: on successful git commit, run \`node "$FORGE_ROOT/tools/store-cli.cjs" update-status bug ${bugId} status fixed\` (terminal).`,
|
|
353
|
+
`- Do NOT write "approved" or "verified" to bug.status — those values were removed from the schema in forge v0.44.0.`,
|
|
354
|
+
`- Do NOT reference task-specific status values (e.g., "committed") or task entity kind.`,
|
|
355
|
+
"- CRITICAL: All `set-summary` calls must use `set-bug-summary` (not `set-summary`).",
|
|
356
|
+
` e.g. node "$FORGE_ROOT/tools/store-cli.cjs" set-bug-summary ${bugId} review_plan <jsonFile>`,
|
|
357
|
+
`- Preflight gate: use \`--bug\` flag (not \`--task\`). e.g. node "$FORGE_ROOT/tools/preflight-gate.cjs" --phase review-plan --bug ${bugId}`,
|
|
358
|
+
"- Skip re-running preflight-gate — the orchestrator already checked it. Proceed directly to the review.",
|
|
359
|
+
'Any workflow text that says "task" should be read as "bug" for this context.',
|
|
360
|
+
];
|
|
361
|
+
// Phase-specific reinforcement when the orchestrator can name the current status.
|
|
362
|
+
if (phaseRole === "approve" && bugStatusBeforePhase) {
|
|
363
|
+
entityKindLines.push(`- Approve phase (reinforce): bug.status is currently '${bugStatusBeforePhase}' and MUST NOT change in this phase. Record verdict in summaries.approve only.`);
|
|
364
|
+
}
|
|
365
|
+
if (phaseRole === "commit" && bugStatusBeforePhase) {
|
|
366
|
+
entityKindLines.push(`- Commit phase: after the git commit lands, transition bug.status from '${bugStatusBeforePhase}' to 'fixed'.`);
|
|
367
|
+
}
|
|
368
|
+
// FORGE-BUG-040: the triage-phase hint block previously prepended here
|
|
369
|
+
// compensated for the orchestrator-only fix_bug.md being delivered to
|
|
370
|
+
// the triage subagent. With the new phase-scoped triage.md sub-workflow,
|
|
371
|
+
// the route-field contract and Path A/B criteria are documented natively
|
|
372
|
+
// in the workflow body — no compose-time injection required.
|
|
373
|
+
const parts = [
|
|
374
|
+
`Read the workflow below and follow it. Bug ID: ${bugId}.`,
|
|
375
|
+
"",
|
|
376
|
+
"---",
|
|
377
|
+
"",
|
|
378
|
+
entityKindLines.join("\n"),
|
|
379
|
+
"",
|
|
380
|
+
"---",
|
|
381
|
+
"",
|
|
382
|
+
];
|
|
383
|
+
if (summariesBlock) {
|
|
384
|
+
parts.push(summariesBlock, "", "---", "");
|
|
385
|
+
}
|
|
386
|
+
parts.push(subWorkflowMd.trim());
|
|
387
|
+
return parts.join("\n");
|
|
388
|
+
}
|
|
389
|
+
// ── BugId capture via tool_execution_end ──────────────────────────────────
|
|
390
|
+
const BUG_WRITE_TOOL_NAMES = new Set(["write", "store-cli", "bash", "forge_store"]);
|
|
391
|
+
/**
|
|
392
|
+
* Scan tool_execution_end events to extract the bugId written by a triage
|
|
393
|
+
* subagent. Returns the LAST matching tool call's bugId, or null if none found.
|
|
394
|
+
*
|
|
395
|
+
* In pi runtime, the forge_store tool is registered as "forge_store" (not
|
|
396
|
+
* "store-cli"). In Claude Code runtime, subagents may shell out via Bash.
|
|
397
|
+
* This function covers all three paths.
|
|
398
|
+
*/
|
|
399
|
+
export function extractBugIdFromEvents(events, prefix = "FORGE") {
|
|
400
|
+
// Prefix is config-owned (project.prefix) and identifier-validated by
|
|
401
|
+
// loadGovernorProjectConfig — the previous hardcoded FORGE-BUG- pattern
|
|
402
|
+
// missed every capture in differently-prefixed projects (CART incident).
|
|
403
|
+
const idPattern = new RegExp(`${prefix}-BUG-\\d+`);
|
|
404
|
+
const idPrefix = `${prefix}-BUG-`;
|
|
405
|
+
let lastBugId = null;
|
|
406
|
+
for (const event of events) {
|
|
407
|
+
if (!event.toolName)
|
|
408
|
+
continue;
|
|
409
|
+
// Check for store-cli write bug calls (Claude Code runtime)
|
|
410
|
+
if (event.toolName === "store-cli") {
|
|
411
|
+
const result = event.result;
|
|
412
|
+
if (typeof result === "string") {
|
|
413
|
+
const match = result.match(idPattern);
|
|
414
|
+
if (match)
|
|
415
|
+
lastBugId = match[0];
|
|
416
|
+
}
|
|
417
|
+
else if (result && typeof result === "object") {
|
|
418
|
+
const obj = result;
|
|
419
|
+
if (typeof obj.bugId === "string" && obj.bugId.startsWith(idPrefix)) {
|
|
420
|
+
lastBugId = obj.bugId;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Check for forge_store tool calls (pi runtime)
|
|
425
|
+
// The pi extension registers the tool as "forge_store", not "store-cli".
|
|
426
|
+
if (event.toolName === "forge_store" && event.result != null) {
|
|
427
|
+
const output = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
428
|
+
const match = output.match(idPattern);
|
|
429
|
+
if (match)
|
|
430
|
+
lastBugId = match[0];
|
|
431
|
+
}
|
|
432
|
+
// Also check for write operations to .forge/store/bugs/
|
|
433
|
+
if (event.toolName === "write" && typeof event.result === "string") {
|
|
434
|
+
const match = event.result.match(idPattern);
|
|
435
|
+
if (match)
|
|
436
|
+
lastBugId = match[0];
|
|
437
|
+
}
|
|
438
|
+
// Bash events: subagents shelling out via Bash may run "store-cli write bug".
|
|
439
|
+
// Only match when output includes store-cli, write, and bug together
|
|
440
|
+
// to avoid false positives from unrelated Bash commands that happen to
|
|
441
|
+
// mention a bug ID in a different context.
|
|
442
|
+
if (event.toolName === "bash" && event.result != null) {
|
|
443
|
+
const output = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
444
|
+
if (output.includes("store-cli") && output.includes("write") && output.includes("bug")) {
|
|
445
|
+
const match = output.match(idPattern);
|
|
446
|
+
if (match)
|
|
447
|
+
lastBugId = match[0];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return lastBugId;
|
|
452
|
+
}
|
|
453
|
+
const STATUS_KEY = "forge:fix-bug";
|
|
454
|
+
const MESSAGE_KEY = "forge:fix-bug:message";
|
|
455
|
+
export async function runBugPipeline(opts) {
|
|
456
|
+
// Thin wrapper: capture the orchestrator transcript writer the inner
|
|
457
|
+
// pipeline creates, surface its path on the result (so callers can
|
|
458
|
+
// archive the run), and GUARANTEE a pipeline-end on every outcome —
|
|
459
|
+
// cancel/halt/failure paths that return without recording one left runs
|
|
460
|
+
// archived as "incomplete" instead of their true outcome.
|
|
461
|
+
let orchTranscript;
|
|
462
|
+
const result = await runBugPipelineInner(opts, (writer) => {
|
|
463
|
+
orchTranscript = writer;
|
|
464
|
+
});
|
|
465
|
+
if (orchTranscript) {
|
|
466
|
+
orchTranscript.close(transcriptOutcomeFor(result.status), result.lastError);
|
|
467
|
+
result.orchestratorTranscriptPath = orchTranscript.filePath;
|
|
468
|
+
}
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
async function runBugPipelineInner(opts, onOrchestratorTranscript) {
|
|
472
|
+
const { bugId: initialBugId, originalArg, isNewBug, cwd, ctx, forgeRoot, storeCli, preflightGate, registry, resumeFromState, } = opts;
|
|
473
|
+
const tree = getOrchestratorTree();
|
|
474
|
+
// Mutable bugId — for new bugs, pre-assign a real FORGE-BUG-NNN ID
|
|
475
|
+
// before triage so the subagent never needs to create or discover one.
|
|
476
|
+
// This replaces the fragile PENDING→capture pattern where the subagent was
|
|
477
|
+
// expected to create the bug record and we'd fish the ID from events.
|
|
478
|
+
let bugId = initialBugId;
|
|
479
|
+
let currentPhaseIndex = resumeFromState?.phaseIndex ?? 0;
|
|
480
|
+
const iterationCounts = resumeFromState?.iterationCounts ?? {};
|
|
481
|
+
// Per-role dispatch counter for OrchestratorTree node identity. Distinct
|
|
482
|
+
// from iterationCounts (which only tracks review-verdict revisions): every
|
|
483
|
+
// dispatch of a role — including a plan-fix re-run after a review
|
|
484
|
+
// loopback — gets its own `<bugId>:<role>:<attempt>` node, so the
|
|
485
|
+
// dashboard renders one leaf per run in dispatch order instead of merging
|
|
486
|
+
// attempts into one node (CART-BUG-003 dashboard regression). Seeded from
|
|
487
|
+
// resume state so resumed runs continue numbering past prior attempts.
|
|
488
|
+
const dispatchCounts = { ...(resumeFromState?.iterationCounts ?? {}) };
|
|
489
|
+
let lastModel;
|
|
490
|
+
let lastProvider;
|
|
491
|
+
// ── Per-persona model routing (Plan 16) ─────────────────────────────────
|
|
492
|
+
// Load layered routing config once at bug-pipeline entry. Empty / absent
|
|
493
|
+
// config produces inherit for every phase — no behaviour change. Pipeline
|
|
494
|
+
// name "fix-bug" lets users configure per-phase overrides distinctly from
|
|
495
|
+
// task pipelines under pipelines["fix-bug"] in their routing config.
|
|
496
|
+
// N-B-E: surface schema errors to caller (Decision 9 — orchestrators fail-fast).
|
|
497
|
+
// See doc/decisions/layered-config-error-policy.md.
|
|
498
|
+
const { merged: modelRoutingConfig, errors: layeredConfigErrors } = loadLayeredConfig(cwd);
|
|
499
|
+
if (layeredConfigErrors.length > 0) {
|
|
500
|
+
for (const e of layeredConfigErrors) {
|
|
501
|
+
ctx.ui.notify(`× forge:fix-bug — forge-cli config schema error: ${e}`, "error");
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
status: "failed",
|
|
505
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
506
|
+
iterationCounts,
|
|
507
|
+
lastError: `forge-cli config schema errors: ${layeredConfigErrors.join("; ")}`,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
// Pre-flight validation — same shape as run-task / run-sprint.
|
|
511
|
+
// FORGE-S25-T17: delegated to orchestrator-preflight.ts (H-13).
|
|
512
|
+
{
|
|
513
|
+
const personasDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "forge-payload", ".base-pack", "personas");
|
|
514
|
+
const personaCatalogue = readPersonaDirBug(personasDir);
|
|
515
|
+
const forgeCfgPath = path.join(cwd, ".forge", "config.json");
|
|
516
|
+
const pipelineCatalogue = readPipelineNamesBug(forgeCfgPath);
|
|
517
|
+
const availableModels = ctx.modelRegistry?.getAvailable?.() ?? [];
|
|
518
|
+
const preflightResult = runOrchestratorPreflight({
|
|
519
|
+
mode: "task",
|
|
520
|
+
ctx,
|
|
521
|
+
notifyPrefix: "forge:fix-bug",
|
|
522
|
+
personaCatalogue,
|
|
523
|
+
pipelineCatalogue,
|
|
524
|
+
modelRoutingConfig,
|
|
525
|
+
availableModels: availableModels.map((m) => ({ provider: m.provider, id: m.id })),
|
|
526
|
+
});
|
|
527
|
+
if (!preflightResult.proceed) {
|
|
528
|
+
return {
|
|
529
|
+
...preflightResult.result,
|
|
530
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
531
|
+
iterationCounts,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// ── Orchestrator transcript ──────────────────────────────────────────
|
|
536
|
+
// One JSONL file per pipeline run, ISO-prefixed in its filename so
|
|
537
|
+
// review-loop iterations (plan → review → plan → review) preserve
|
|
538
|
+
// their own logs instead of overwriting each other. Captures every
|
|
539
|
+
// ctx.ui.notify line plus structured phase-boundary events.
|
|
540
|
+
const orchTranscript = new OrchestratorTranscriptWriter({
|
|
541
|
+
cwd,
|
|
542
|
+
entityKind: "bug",
|
|
543
|
+
entityId: bugId,
|
|
544
|
+
});
|
|
545
|
+
onOrchestratorTranscript(orchTranscript);
|
|
546
|
+
const __origNotify = ctx.ui.notify.bind(ctx.ui);
|
|
547
|
+
ctx.ui.notify = ((msg, level) => {
|
|
548
|
+
__origNotify(msg, level);
|
|
549
|
+
orchTranscript.record({
|
|
550
|
+
kind: "notify",
|
|
551
|
+
ts: new Date().toISOString(),
|
|
552
|
+
level: (level ?? "info"),
|
|
553
|
+
message: typeof msg === "string" ? msg : String(msg),
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
const pipelineStartMs = Date.now();
|
|
557
|
+
try {
|
|
558
|
+
while (currentPhaseIndex < BUG_PHASES.length) {
|
|
559
|
+
// ── Between-phase cancellation gate ────────────────────────────
|
|
560
|
+
if (opts.signal?.aborted) {
|
|
561
|
+
ctx.ui.notify(`⊘ forge:fix-bug — ${bugId} cancelled by user.`, "info");
|
|
562
|
+
registry.completePhase(bugId, BUG_PHASES[currentPhaseIndex]?.role ?? "unknown", "cancelled");
|
|
563
|
+
registry.confirmCancelled(bugId);
|
|
564
|
+
// ADR-S21-01: preserve state file so cancelled runs are resumable
|
|
565
|
+
writeBugState(cwd, {
|
|
566
|
+
bugId,
|
|
567
|
+
phaseIndex: currentPhaseIndex,
|
|
568
|
+
iterationCounts,
|
|
569
|
+
halted: false,
|
|
570
|
+
status: "cancelled",
|
|
571
|
+
lastError: undefined,
|
|
572
|
+
savedAt: new Date().toISOString(),
|
|
573
|
+
});
|
|
574
|
+
return { status: "cancelled", lastPhaseIndex: currentPhaseIndex, iterationCounts };
|
|
575
|
+
}
|
|
576
|
+
const phase = BUG_PHASES[currentPhaseIndex];
|
|
577
|
+
if (!phase) {
|
|
578
|
+
ctx.ui.notify(`× forge:fix-bug — invalid phase index ${currentPhaseIndex}`, "error");
|
|
579
|
+
return {
|
|
580
|
+
status: "failed",
|
|
581
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
582
|
+
iterationCounts,
|
|
583
|
+
lastError: `invalid phase index ${currentPhaseIndex}`,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
ctx.ui.setStatus?.(STATUS_KEY, `fix-bug ${bugId}: phase ${currentPhaseIndex + 1}/${BUG_PHASES.length} (${phase.role})`);
|
|
587
|
+
ctx.ui.notify(`→ ${bugId}: ${phase.role} (phase ${currentPhaseIndex + 1}/${BUG_PHASES.length})`, "info");
|
|
588
|
+
orchTranscript.record({
|
|
589
|
+
kind: "phase-start",
|
|
590
|
+
ts: new Date().toISOString(),
|
|
591
|
+
phase: phase.role,
|
|
592
|
+
phaseIndex: currentPhaseIndex,
|
|
593
|
+
phaseCount: BUG_PHASES.length,
|
|
594
|
+
attempt: (iterationCounts[phase.role] ?? 0) + 1,
|
|
595
|
+
workflowFile: phase.workflowFile,
|
|
596
|
+
persona: phase.personaNoun,
|
|
597
|
+
});
|
|
598
|
+
const subWorkflowPath = path.join(cwd, ".forge", "workflows", `${phase.workflowFile}.md`);
|
|
599
|
+
// ── Read sub-workflow ─────────────────────────────────────────
|
|
600
|
+
let subWorkflowMd;
|
|
601
|
+
let subWorkflowAudience = "any";
|
|
602
|
+
try {
|
|
603
|
+
const loaded = loadWorkflow(subWorkflowPath);
|
|
604
|
+
subWorkflowMd = loaded.rawMarkdown;
|
|
605
|
+
subWorkflowAudience = loaded.audience;
|
|
606
|
+
}
|
|
607
|
+
catch (err) {
|
|
608
|
+
const e = err;
|
|
609
|
+
ctx.ui.notify(`× forge:fix-bug — failed to read sub-workflow for ${phase.role}: ${e.message ?? "unknown"}`, "error");
|
|
610
|
+
writeBugState(cwd, {
|
|
611
|
+
bugId,
|
|
612
|
+
phaseIndex: currentPhaseIndex,
|
|
613
|
+
iterationCounts,
|
|
614
|
+
halted: true,
|
|
615
|
+
lastError: `sub-workflow read failed: ${e.message ?? "unknown"}`,
|
|
616
|
+
savedAt: new Date().toISOString(),
|
|
617
|
+
});
|
|
618
|
+
return {
|
|
619
|
+
status: "failed",
|
|
620
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
621
|
+
iterationCounts,
|
|
622
|
+
lastError: `sub-workflow read failed: ${e.message ?? "unknown"}`,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
// ── 6a. Phase skip (state-aware, defense-in-depth) ─────────────
|
|
626
|
+
// Belt-and-suspenders alongside the explicit summaries.triage.route
|
|
627
|
+
// branch (handled in section 6c below). Some subagents in some
|
|
628
|
+
// runtimes still go end-to-end during triage instead of just triaging
|
|
629
|
+
// — rather than roll back the work they did, skip non-review phases
|
|
630
|
+
// whose output is already reflected in the bug status. Review phases
|
|
631
|
+
// are never skipped — they are quality gates that must always run.
|
|
632
|
+
//
|
|
633
|
+
// Post-v0.44.0: terminal status is `fixed` only. `approved` and
|
|
634
|
+
// `verified` are no longer valid bug status values; references
|
|
635
|
+
// removed.
|
|
636
|
+
const PHASE_SKIP_STATES = {
|
|
637
|
+
"plan-fix": new Set(["fixed"]),
|
|
638
|
+
implement: new Set(["fixed"]),
|
|
639
|
+
commit: new Set(["fixed"]), // commit writes the terminal status; skip if already there
|
|
640
|
+
};
|
|
641
|
+
const bugNow = readBugRecord(bugId, storeCli, cwd);
|
|
642
|
+
const skipStates = PHASE_SKIP_STATES[phase.role];
|
|
643
|
+
if (skipStates && bugNow?.status && skipStates.has(bugNow.status) && !phase.isReview) {
|
|
644
|
+
ctx.ui.notify(`⊘ forge:fix-bug — skipping ${phase.role}: bug ${bugId} is already '${bugNow.status}' (work already done).`, "info");
|
|
645
|
+
// Write a synthetic "approved" summary so downstream `after` predecessor
|
|
646
|
+
// verdict checks find a verdict and don't block review phases.
|
|
647
|
+
const summaryKey = BUG_SUMMARY_KEY_BY_ROLE[phase.role];
|
|
648
|
+
if (summaryKey) {
|
|
649
|
+
const synthSummary = {
|
|
650
|
+
objective: `Phase ${phase.role} skipped — bug already ${bugNow.status}`,
|
|
651
|
+
findings: ["Subagent completed fix during triage (Path A); phase output implicitly satisfied."],
|
|
652
|
+
// Non-review phases should have verdict "n/a" — the phase
|
|
653
|
+
// didn't produce a gate verdict. This matches the `after
|
|
654
|
+
// <phase> = n/a` preflight gate contract. Review phases
|
|
655
|
+
// use "approved" since they are gate phases.
|
|
656
|
+
verdict: phase.isReview ? "approved" : "n/a",
|
|
657
|
+
written_at: new Date().toISOString(),
|
|
658
|
+
};
|
|
659
|
+
const synthFile = path.join(cwd, ".forge", "cache", `synthetic-summary-${bugId}-${summaryKey}.json`);
|
|
660
|
+
fs.writeFileSync(synthFile, JSON.stringify(synthSummary, null, 2), "utf8");
|
|
661
|
+
const synthResult = spawnSync("node", [storeCli, "set-bug-summary", bugId, summaryKey, synthFile], {
|
|
662
|
+
cwd,
|
|
663
|
+
encoding: "utf8",
|
|
664
|
+
});
|
|
665
|
+
if (synthResult.status !== 0) {
|
|
666
|
+
ctx.ui.notify(`⚠ forge:fix-bug — synthetic summary write failed for ${phase.role}: ${String(synthResult.stderr).trim()}`, "warning");
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
fs.unlinkSync(synthFile);
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
/* non-fatal */
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
currentPhaseIndex++;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
// ── 6b. Preflight gate ────────────────────────────────────────
|
|
679
|
+
// Skip preflight gate for triage phase of new bugs (PENDING- placeholder)
|
|
680
|
+
// because the bug record doesn't exist yet — gates referencing bug fields
|
|
681
|
+
// would always fail.
|
|
682
|
+
//
|
|
683
|
+
// Also skip for review phases when the bug is already in a terminal
|
|
684
|
+
// state ("fixed"). Path A bugs get fixed during triage, then the
|
|
685
|
+
// preflight gate's `forbid bug.status == fixed` and `after implement
|
|
686
|
+
// = n/a` checks block review-code/review-plan even though we
|
|
687
|
+
// deliberately want to run those reviews. The review subagent handles
|
|
688
|
+
// the already-fixed scenario internally.
|
|
689
|
+
const pendingBugId = bugId.startsWith("PENDING-");
|
|
690
|
+
const bugAlreadyFixed = bugNow?.status === "fixed" && phase.isReview;
|
|
691
|
+
if (!pendingBugId && !bugAlreadyFixed && fs.existsSync(preflightGate)) {
|
|
692
|
+
const preflightOutcome = runPreflightGateWithData(preflightGate, phase.role, bugId, cwd, "bug");
|
|
693
|
+
if (preflightOutcome.result === "halt") {
|
|
694
|
+
// Render structured failure reason if available.
|
|
695
|
+
if (preflightOutcome.gateFailure) {
|
|
696
|
+
ctx.ui.notify(`× forge:fix-bug — preflight gate failed for phase ${phase.role} ` +
|
|
697
|
+
`[${preflightOutcome.gateFailure.reasonCode}]: ${preflightOutcome.gateFailure.detail}`, "error");
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
ctx.ui.notify(`× forge:fix-bug — preflight gate failed for phase ${phase.role} (exit 1); halting.`, "error");
|
|
701
|
+
}
|
|
702
|
+
writeBugState(cwd, {
|
|
703
|
+
bugId,
|
|
704
|
+
phaseIndex: currentPhaseIndex,
|
|
705
|
+
iterationCounts,
|
|
706
|
+
halted: true,
|
|
707
|
+
lastError: `preflight gate exit 1 for ${phase.role}`,
|
|
708
|
+
savedAt: new Date().toISOString(),
|
|
709
|
+
});
|
|
710
|
+
// Spawn halt-recovery advisor (Tier 1, best-effort — non-fatal).
|
|
711
|
+
if (preflightOutcome.gateFailure) {
|
|
712
|
+
const advisorModel = resolveAdvisorModel(modelRoutingConfig, ctx.model);
|
|
713
|
+
void runHaltAdvisor({
|
|
714
|
+
gateFailure: preflightOutcome.gateFailure,
|
|
715
|
+
advisorModel,
|
|
716
|
+
taskId: bugId,
|
|
717
|
+
cwd,
|
|
718
|
+
ctx: { ui: ctx.ui },
|
|
719
|
+
forgeRoot,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
status: "halted",
|
|
724
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
725
|
+
iterationCounts,
|
|
726
|
+
lastError: `preflight gate exit 1 for ${phase.role}`,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if (preflightOutcome.result === "escalate") {
|
|
730
|
+
ctx.ui.notify(`× forge:fix-bug — preflight gate escalated for phase ${phase.role} (exit 2); manual intervention required.`, "error");
|
|
731
|
+
writeBugState(cwd, {
|
|
732
|
+
bugId,
|
|
733
|
+
phaseIndex: currentPhaseIndex,
|
|
734
|
+
iterationCounts,
|
|
735
|
+
halted: true,
|
|
736
|
+
lastError: `preflight gate exit 2 (escalate) for ${phase.role}`,
|
|
737
|
+
savedAt: new Date().toISOString(),
|
|
738
|
+
});
|
|
739
|
+
return {
|
|
740
|
+
status: "escalated",
|
|
741
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
742
|
+
iterationCounts,
|
|
743
|
+
lastError: `preflight gate exit 2 (escalate) for ${phase.role}`,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// ── 6. Materialization-marker check ───────────────────────────
|
|
748
|
+
// FORGE-BUG-040: every BUG phase is now a true `audience: subagent`
|
|
749
|
+
// sub-workflow — triage / plan-fix / implement no longer alias to
|
|
750
|
+
// fix_bug.md. The marker check is therefore unconditional; a missing
|
|
751
|
+
// marker is a hard failure on the first dispatch.
|
|
752
|
+
{
|
|
753
|
+
const markerCheck = checkMaterialization(subWorkflowPath, subWorkflowMd);
|
|
754
|
+
if (!markerCheck.ok) {
|
|
755
|
+
for (const marker of markerCheck.missing) {
|
|
756
|
+
ctx.ui.notify(`× workflow regression: ${marker} not found in ${subWorkflowPath}`, "error");
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
status: "failed",
|
|
760
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
761
|
+
iterationCounts,
|
|
762
|
+
lastError: `materialization markers missing: ${markerCheck.missing.join(", ")}`,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// ── 5. Audience check ─────────────────────────────────────────
|
|
767
|
+
// FORGE-BUG-040: every BUG phase is a true `audience: subagent`
|
|
768
|
+
// workflow now; the previous `fix_bug.md` audience-bypass is gone.
|
|
769
|
+
const audienceOk = CallerContextStore.asSubagent(phase.role, () => assertAudience({ workflowName: phase.workflowFile, audience: subWorkflowAudience }, ctx));
|
|
770
|
+
if (!audienceOk) {
|
|
771
|
+
writeBugState(cwd, {
|
|
772
|
+
bugId,
|
|
773
|
+
phaseIndex: currentPhaseIndex,
|
|
774
|
+
iterationCounts,
|
|
775
|
+
halted: true,
|
|
776
|
+
lastError: `audience check failed for ${phase.workflowFile}`,
|
|
777
|
+
savedAt: new Date().toISOString(),
|
|
778
|
+
});
|
|
779
|
+
return {
|
|
780
|
+
status: "failed",
|
|
781
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
782
|
+
iterationCounts,
|
|
783
|
+
lastError: `audience check failed for ${phase.workflowFile}`,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
// ── Persona load ──────────────────────────────────────────────
|
|
787
|
+
let persona;
|
|
788
|
+
try {
|
|
789
|
+
persona = loadForgePersona(phase.personaNoun, cwd);
|
|
790
|
+
}
|
|
791
|
+
catch (err) {
|
|
792
|
+
const e = err;
|
|
793
|
+
ctx.ui.notify(`× forge:fix-bug — persona '${phase.personaNoun}' not found for phase ${phase.role}: ${e.message ?? "unknown"}. ` +
|
|
794
|
+
"Run /forge:regenerate to materialize persona files.", "error");
|
|
795
|
+
writeBugState(cwd, {
|
|
796
|
+
bugId,
|
|
797
|
+
phaseIndex: currentPhaseIndex,
|
|
798
|
+
iterationCounts,
|
|
799
|
+
halted: true,
|
|
800
|
+
lastError: `persona load failed: ${e.message ?? "unknown"}`,
|
|
801
|
+
savedAt: new Date().toISOString(),
|
|
802
|
+
});
|
|
803
|
+
return {
|
|
804
|
+
status: "failed",
|
|
805
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
806
|
+
iterationCounts,
|
|
807
|
+
lastError: `persona load failed: ${e.message ?? "unknown"}`,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
// ── Read bug record for current status ────────────────────────
|
|
811
|
+
// Skip for PENDING bugIds (bug doesn't exist yet).
|
|
812
|
+
const bugRecordBefore = pendingBugId ? null : readBugRecord(bugId, storeCli, cwd);
|
|
813
|
+
const bugStatusBeforePhase = bugRecordBefore?.status;
|
|
814
|
+
// ── 4. Dispatch via runForgeSubagent (IL10) ───────────────────
|
|
815
|
+
// NEVER sendKickoff here — that would reproduce issue #30.
|
|
816
|
+
// Carry forward prior phase summaries (forge-cli#19).
|
|
817
|
+
const bugSummariesBlock = currentPhaseIndex > 0 ? buildSummariesBlock(bugRecordBefore?.summaries) || undefined : undefined;
|
|
818
|
+
let bugBody = composeBugBody(subWorkflowMd, bugId, phase.role, bugStatusBeforePhase, bugSummariesBlock);
|
|
819
|
+
// For new bugs in triage, prepend the original free-form text so the
|
|
820
|
+
// subagent knows the user-provided bug description to triage.
|
|
821
|
+
// The bug record already exists (pre-created with status "reported"),
|
|
822
|
+
// so the subagent should update it, not create a new one.
|
|
823
|
+
if (phase.role === "triage" && isNewBug && originalArg) {
|
|
824
|
+
bugBody = `Bug description: ${originalArg}\n\n---\n\n${bugBody}`;
|
|
825
|
+
}
|
|
826
|
+
// Phase-scoped progress counters
|
|
827
|
+
const phaseStart = Date.now();
|
|
828
|
+
// Track tool_execution_end events for bugId capture (Findings #1, #2).
|
|
829
|
+
const toolExecutionEvents = [];
|
|
830
|
+
// Stabilization debug log
|
|
831
|
+
// Skip for PENDING bugIds — create after real bugId is captured.
|
|
832
|
+
// Disable entirely with FORGE_DEBUG_LOG=0.
|
|
833
|
+
const debugLogDisabled = process.env.FORGE_DEBUG_LOG === "0";
|
|
834
|
+
let debugLogPath = null;
|
|
835
|
+
let writeDebug = () => { };
|
|
836
|
+
if (!pendingBugId && !debugLogDisabled) {
|
|
837
|
+
debugLogPath = path.join(cwd, ".forge", "cache", `fix-bug-debug-${bugId}.jsonl`);
|
|
838
|
+
writeDebug = (rec) => {
|
|
839
|
+
try {
|
|
840
|
+
fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
|
|
841
|
+
// Cap at 10 MB: truncate head when size exceeds the cap.
|
|
842
|
+
try {
|
|
843
|
+
const st = fs.statSync(debugLogPath);
|
|
844
|
+
if (st.size > 10 * 1024 * 1024) {
|
|
845
|
+
const all = fs.readFileSync(debugLogPath, "utf8");
|
|
846
|
+
const lines = all.split("\n");
|
|
847
|
+
// Keep last 80% of lines
|
|
848
|
+
const keep = Math.floor(lines.length * 0.8);
|
|
849
|
+
fs.writeFileSync(debugLogPath, lines.slice(-keep).join("\n"), "utf8");
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
/* file may not exist yet */
|
|
854
|
+
}
|
|
855
|
+
fs.appendFileSync(debugLogPath, `${JSON.stringify({ ts: new Date().toISOString(), phase: phase.role, ...rec })}\n`, "utf8");
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
// non-fatal; debug log is best-effort
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
writeDebug({ kind: "phase_start", phaseIndex: currentPhaseIndex });
|
|
863
|
+
registry.startPhase(bugId, phase.role, currentPhaseIndex);
|
|
864
|
+
// Bridge: register phase in OrchestratorTree. Node identity is
|
|
865
|
+
// per-dispatch (see dispatchCounts above) — never reuse an ID for
|
|
866
|
+
// a re-dispatched role.
|
|
867
|
+
const iteration = (dispatchCounts[phase.role] = (dispatchCounts[phase.role] ?? 0) + 1);
|
|
868
|
+
const phaseNodeId = `${bugId}:${phase.role}:${iteration}`;
|
|
869
|
+
tree.startNode(phaseNodeId, {
|
|
870
|
+
parentId: bugId,
|
|
871
|
+
label: `${phase.role}:${iteration}`,
|
|
872
|
+
kind: "leaf",
|
|
873
|
+
// Full body — display clamping/expansion is the view's decision
|
|
874
|
+
// (the tree applies only a storage cap).
|
|
875
|
+
promptPreview: bugBody,
|
|
876
|
+
});
|
|
877
|
+
const refreshStatus = () => {
|
|
878
|
+
if (process.env.FORGE_VERBOSE !== "1")
|
|
879
|
+
return;
|
|
880
|
+
const elapsed = Math.floor((Date.now() - phaseStart) / 1000);
|
|
881
|
+
const tail = observer.state.lastTool ? ` · ${observer.state.lastTool}` : "";
|
|
882
|
+
ctx.ui.setStatus?.(STATUS_KEY, `fix-bug ${bugId}: ${phase.role} · t${observer.state.turn} · tools ${observer.state.toolCount}${observer.state.errCount ? ` · err ${observer.state.errCount}` : ""} · ${elapsed}s${tail}`);
|
|
883
|
+
};
|
|
884
|
+
const observer = attachViewportObserver({
|
|
885
|
+
registry,
|
|
886
|
+
sessionId: bugId,
|
|
887
|
+
phaseRole: phase.role,
|
|
888
|
+
nodeId: phaseNodeId,
|
|
889
|
+
beginHeader: `─── phase ${phase.role} begin ───`,
|
|
890
|
+
writeDebug,
|
|
891
|
+
notify: (msg, level) => ctx.ui.notify(msg, level),
|
|
892
|
+
setStatusVerbose: process.env.FORGE_VERBOSE === "1" ? (k, v) => ctx.ui.setStatus?.(k, v) : undefined,
|
|
893
|
+
verboseKeys: { messageKey: `${STATUS_KEY}:message` },
|
|
894
|
+
afterEach: refreshStatus,
|
|
895
|
+
});
|
|
896
|
+
// Wrap the observer's onEvent to also capture tool_execution_end events
|
|
897
|
+
// for bugId capture downstream (findings #1, #2), plus the first turn_end
|
|
898
|
+
// per phase (IL10 visibility — stream-observed model id).
|
|
899
|
+
let modelObservedLogged = false;
|
|
900
|
+
const onSubagentEvent = (event) => {
|
|
901
|
+
if (event?.type === "tool_execution_end") {
|
|
902
|
+
toolExecutionEvents.push({ toolName: event.toolName, result: event.result });
|
|
903
|
+
}
|
|
904
|
+
if (!modelObservedLogged && event?.type === "turn_end" && event.message?.model) {
|
|
905
|
+
modelObservedLogged = true;
|
|
906
|
+
writeDebug({
|
|
907
|
+
kind: "model_observed",
|
|
908
|
+
provider: event.message.provider ?? null,
|
|
909
|
+
model: event.message.model,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
observer.onEvent(event);
|
|
913
|
+
};
|
|
914
|
+
// Per-phase model resolution. When config is absent or cascade bottoms
|
|
915
|
+
// out, resolves to inherit (model: undefined) — setModel is skipped and
|
|
916
|
+
// pi's current model is used. IL10 still holds: result.model below is
|
|
917
|
+
// the stream-observed runtime model, not whatever we requested here.
|
|
918
|
+
const modelResolution = resolveModelForPhase("fix-bug", phase.role, phase.personaNoun, modelRoutingConfig);
|
|
919
|
+
writeDebug({
|
|
920
|
+
kind: "requested_model",
|
|
921
|
+
requested: modelResolution.model ?? null,
|
|
922
|
+
source: modelResolution.source,
|
|
923
|
+
persona: phase.personaNoun,
|
|
924
|
+
});
|
|
925
|
+
let result;
|
|
926
|
+
try {
|
|
927
|
+
// FORGE-BUG-040: wrap the runForgeSubagent dispatch in the phase
|
|
928
|
+
// caller context so downstream tool calls (forge_preflight,
|
|
929
|
+
// forge_store update-status / set-bug-summary / set-summary / emit)
|
|
930
|
+
// can verify the caller's phase matches the phase named in the
|
|
931
|
+
// tool's arguments. This is the single setter of phase context
|
|
932
|
+
// for the bug pipeline; the audience-test wrap above is a
|
|
933
|
+
// short-lived test, not the canonical dispatch context.
|
|
934
|
+
result = await CallerContextStore.asSubagent(phase.role, () => runForgeSubagent({
|
|
935
|
+
persona,
|
|
936
|
+
task: bugBody,
|
|
937
|
+
cwd,
|
|
938
|
+
exportTag: `${bugId}__${phase.role}`,
|
|
939
|
+
tailLog: observer.state.tailLog,
|
|
940
|
+
// Sprint-scoped if the bug is attached to one, else bug-scoped.
|
|
941
|
+
// Keeps every phase of this bug-fix pipeline in a single cache
|
|
942
|
+
// namespace so the system-prompt + persona prefix stays warm
|
|
943
|
+
// across the ~10-minute phases.
|
|
944
|
+
cacheSessionId: typeof bugRecordBefore?.sprintId === "string"
|
|
945
|
+
? `forge:${bugRecordBefore.sprintId}`
|
|
946
|
+
: `forge:bug:${bugId}`,
|
|
947
|
+
onEvent: onSubagentEvent,
|
|
948
|
+
requestedModel: modelResolution.model,
|
|
949
|
+
modelRegistry: ctx.modelRegistry,
|
|
950
|
+
signal: opts.signal,
|
|
951
|
+
customTools: opts.forgeToolDefs ? getSubagentTools(opts.forgeToolDefs, persona.name) : undefined,
|
|
952
|
+
}));
|
|
953
|
+
}
|
|
954
|
+
catch (err) {
|
|
955
|
+
const e = err;
|
|
956
|
+
ctx.ui.notify(`× forge:fix-bug — runForgeSubagent threw for phase ${phase.role}: ${e.message ?? "unknown"}`, "error");
|
|
957
|
+
tree.completeNode(phaseNodeId, "failed");
|
|
958
|
+
writeBugState(cwd, {
|
|
959
|
+
bugId,
|
|
960
|
+
phaseIndex: currentPhaseIndex,
|
|
961
|
+
iterationCounts,
|
|
962
|
+
halted: true,
|
|
963
|
+
lastError: `runForgeSubagent threw: ${e.message ?? "unknown"}`,
|
|
964
|
+
savedAt: new Date().toISOString(),
|
|
965
|
+
});
|
|
966
|
+
return {
|
|
967
|
+
status: "failed",
|
|
968
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
969
|
+
iterationCounts,
|
|
970
|
+
lastError: `runForgeSubagent threw: ${e.message ?? "unknown"}`,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
// Close this dispatch's tree node with final usage/model — MUST be
|
|
974
|
+
// called on every exit path below (failure, halt, escalation,
|
|
975
|
+
// loopback, advance). A node left `running` keeps a live spinner
|
|
976
|
+
// in the dashboard forever AND absorbs the next same-role
|
|
977
|
+
// dispatch's telemetry via the observer's legacy role-prefix scan.
|
|
978
|
+
const finishPhaseNode = (status) => {
|
|
979
|
+
tree.setNodeUsage(phaseNodeId, {
|
|
980
|
+
input: result.usage.input,
|
|
981
|
+
output: result.usage.output,
|
|
982
|
+
cacheRead: result.usage.cacheRead,
|
|
983
|
+
});
|
|
984
|
+
if (result.model)
|
|
985
|
+
tree.setNodeModel(phaseNodeId, result.model, result.provider ?? "");
|
|
986
|
+
tree.completeNode(phaseNodeId, status);
|
|
987
|
+
};
|
|
988
|
+
// ── Post-subagent abort detection ─────────────────────────────────
|
|
989
|
+
if (result.stopReason === "aborted" || opts.signal?.aborted) {
|
|
990
|
+
ctx.ui.notify(`⊘ forge:fix-bug — ${bugId} phase ${phase.role} cancelled.`, "info");
|
|
991
|
+
registry.completePhase(bugId, phase.role, "cancelled");
|
|
992
|
+
tree.completeNode(phaseNodeId, "cancelled");
|
|
993
|
+
registry.confirmCancelled(bugId);
|
|
994
|
+
// Bug B parity with run-task: account billed tokens of the aborted attempt.
|
|
995
|
+
// sprintId "bugs" = routing key for bug events (matches success path).
|
|
996
|
+
// The optional `type` token is omitted — verdict carries the outcome.
|
|
997
|
+
emitIncompletePhaseEvent({
|
|
998
|
+
emitCtx: {
|
|
999
|
+
entityType: "bug",
|
|
1000
|
+
bugId,
|
|
1001
|
+
sprintId: "bugs",
|
|
1002
|
+
phase,
|
|
1003
|
+
iteration: (iterationCounts[phase.role] ?? 0) + 1,
|
|
1004
|
+
startMs: phaseStart,
|
|
1005
|
+
endMs: Date.now(),
|
|
1006
|
+
model: result.model ?? "unknown",
|
|
1007
|
+
provider: result.provider ?? "unknown",
|
|
1008
|
+
usage: {
|
|
1009
|
+
input: result.usage.input,
|
|
1010
|
+
output: result.usage.output,
|
|
1011
|
+
cacheRead: result.usage.cacheRead,
|
|
1012
|
+
cacheWrite: result.usage.cacheWrite,
|
|
1013
|
+
},
|
|
1014
|
+
judgement: undefined,
|
|
1015
|
+
storeCli,
|
|
1016
|
+
cwd,
|
|
1017
|
+
},
|
|
1018
|
+
outcome: "aborted",
|
|
1019
|
+
notes: result.errorMessage ?? result.stopReason ?? undefined,
|
|
1020
|
+
onDebug: writeDebug,
|
|
1021
|
+
});
|
|
1022
|
+
// ADR-S21-01: preserve state file so cancelled runs are resumable
|
|
1023
|
+
writeBugState(cwd, {
|
|
1024
|
+
bugId,
|
|
1025
|
+
phaseIndex: currentPhaseIndex,
|
|
1026
|
+
iterationCounts,
|
|
1027
|
+
halted: false,
|
|
1028
|
+
status: "cancelled",
|
|
1029
|
+
lastError: undefined,
|
|
1030
|
+
savedAt: new Date().toISOString(),
|
|
1031
|
+
});
|
|
1032
|
+
return { status: "cancelled", lastPhaseIndex: currentPhaseIndex, iterationCounts };
|
|
1033
|
+
}
|
|
1034
|
+
// ── Halt-on-failure ───────────────────────────────────────────
|
|
1035
|
+
if (result.exitCode !== 0) {
|
|
1036
|
+
ctx.ui.notify(`× forge:fix-bug — phase ${phase.role} failed (exit ${result.exitCode})` +
|
|
1037
|
+
(result.errorMessage ? `: ${result.errorMessage}` : "") +
|
|
1038
|
+
(result.stopReason ? ` [${result.stopReason}]` : ""), "error");
|
|
1039
|
+
finishPhaseNode("failed");
|
|
1040
|
+
// Bug B parity with run-task: account billed tokens of the failed attempt.
|
|
1041
|
+
emitIncompletePhaseEvent({
|
|
1042
|
+
emitCtx: {
|
|
1043
|
+
entityType: "bug",
|
|
1044
|
+
bugId,
|
|
1045
|
+
sprintId: "bugs",
|
|
1046
|
+
phase,
|
|
1047
|
+
iteration: (iterationCounts[phase.role] ?? 0) + 1,
|
|
1048
|
+
startMs: phaseStart,
|
|
1049
|
+
endMs: Date.now(),
|
|
1050
|
+
model: result.model ?? "unknown",
|
|
1051
|
+
provider: result.provider ?? "unknown",
|
|
1052
|
+
usage: {
|
|
1053
|
+
input: result.usage.input,
|
|
1054
|
+
output: result.usage.output,
|
|
1055
|
+
cacheRead: result.usage.cacheRead,
|
|
1056
|
+
cacheWrite: result.usage.cacheWrite,
|
|
1057
|
+
},
|
|
1058
|
+
judgement: undefined,
|
|
1059
|
+
storeCli,
|
|
1060
|
+
cwd,
|
|
1061
|
+
},
|
|
1062
|
+
outcome: "failed",
|
|
1063
|
+
notes: result.errorMessage ?? result.stopReason ?? undefined,
|
|
1064
|
+
onDebug: writeDebug,
|
|
1065
|
+
});
|
|
1066
|
+
writeBugState(cwd, {
|
|
1067
|
+
bugId,
|
|
1068
|
+
phaseIndex: currentPhaseIndex,
|
|
1069
|
+
iterationCounts,
|
|
1070
|
+
halted: true,
|
|
1071
|
+
lastError: result.errorMessage ?? result.stopReason ?? "subagent exit non-zero",
|
|
1072
|
+
savedAt: new Date().toISOString(),
|
|
1073
|
+
});
|
|
1074
|
+
return {
|
|
1075
|
+
status: "failed",
|
|
1076
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
1077
|
+
iterationCounts,
|
|
1078
|
+
lastError: result.errorMessage ?? result.stopReason ?? "subagent exit non-zero",
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
// Capture model/provider from subagent result.
|
|
1082
|
+
if (result.model)
|
|
1083
|
+
lastModel = result.model;
|
|
1084
|
+
if (result.provider)
|
|
1085
|
+
lastProvider = result.provider;
|
|
1086
|
+
// ── BugId capture after triage phase (Finding #1, #2) ──────────
|
|
1087
|
+
// For new bugs, the triage subagent creates the bug record via store-cli.
|
|
1088
|
+
// We capture the bugId by scanning tool_execution_end events.
|
|
1089
|
+
if (phase.role === "triage" && isNewBug && bugId.startsWith("PENDING-")) {
|
|
1090
|
+
const capturedBugId = extractBugIdFromEvents(toolExecutionEvents, loadGovernorProjectConfig(cwd).prefix);
|
|
1091
|
+
if (capturedBugId) {
|
|
1092
|
+
ctx.ui.notify(`forge:fix-bug — captured bug ID: ${capturedBugId}`, "info");
|
|
1093
|
+
bugId = capturedBugId;
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
// Fallback: list bugs and find the most recent one created after pipeline start.
|
|
1097
|
+
const listResult = spawnSync("node", [storeCli, "list", "bug", "--json"], { cwd, encoding: "utf8" });
|
|
1098
|
+
if (listResult.status === 0 && listResult.stdout) {
|
|
1099
|
+
try {
|
|
1100
|
+
const bugs = JSON.parse(listResult.stdout);
|
|
1101
|
+
if (Array.isArray(bugs)) {
|
|
1102
|
+
// Find most recent bug whose reportedAt is after the pipeline start
|
|
1103
|
+
const pipelineStartIso = new Date(parseInt(bugId.replace("PENDING-", ""))).toISOString();
|
|
1104
|
+
const recent = bugs
|
|
1105
|
+
.filter((b) => b.reportedAt && b.reportedAt >= pipelineStartIso)
|
|
1106
|
+
.sort((a, b) => String(b.reportedAt).localeCompare(String(a.reportedAt)))[0];
|
|
1107
|
+
if (recent &&
|
|
1108
|
+
recent.bugId &&
|
|
1109
|
+
typeof recent.bugId === "string" &&
|
|
1110
|
+
recent.bugId.startsWith("FORGE-BUG-")) {
|
|
1111
|
+
bugId = recent.bugId;
|
|
1112
|
+
ctx.ui.notify(`forge:fix-bug — captured bug ID via store fallback: ${bugId}`, "info");
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
catch {
|
|
1117
|
+
/* parse failure — fall through to assertion */
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
// Defensive guard: if bugId is still PENDING after triage, pipeline cannot proceed.
|
|
1122
|
+
if (bugId.startsWith("PENDING-")) {
|
|
1123
|
+
ctx.ui.notify("× forge:fix-bug — failed to capture real bug ID after triage. Cannot proceed with PENDING placeholder.", "error");
|
|
1124
|
+
return {
|
|
1125
|
+
status: "failed",
|
|
1126
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
1127
|
+
iterationCounts,
|
|
1128
|
+
lastError: "bugId still PENDING after triage",
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
// Re-initialize debug log now that real bugId is available.
|
|
1132
|
+
if (!debugLogDisabled) {
|
|
1133
|
+
debugLogPath = path.join(cwd, ".forge", "cache", `fix-bug-debug-${bugId}.jsonl`);
|
|
1134
|
+
const savedWriteDebug = writeDebug;
|
|
1135
|
+
writeDebug = (rec) => {
|
|
1136
|
+
try {
|
|
1137
|
+
fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
|
|
1138
|
+
try {
|
|
1139
|
+
const st = fs.statSync(debugLogPath);
|
|
1140
|
+
if (st.size > 10 * 1024 * 1024) {
|
|
1141
|
+
const all = fs.readFileSync(debugLogPath, "utf8");
|
|
1142
|
+
const lines = all.split("\n");
|
|
1143
|
+
const keep = Math.floor(lines.length * 0.8);
|
|
1144
|
+
fs.writeFileSync(debugLogPath, lines.slice(-keep).join("\n"), "utf8");
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
/* file may not exist yet */
|
|
1149
|
+
}
|
|
1150
|
+
fs.appendFileSync(debugLogPath, `${JSON.stringify({ ts: new Date().toISOString(), phase: phase.role, ...rec })}\n`, "utf8");
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
// non-fatal
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
writeDebug({ kind: "bugid_captured", bugId });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
{
|
|
1160
|
+
const elapsed = Math.floor((Date.now() - phaseStart) / 1000);
|
|
1161
|
+
const { turn, toolCount, errCount, cumUsage, cumCompression } = observer.state;
|
|
1162
|
+
ctx.ui.notify(`✓ ${phase.role}: ${turn} turn${turn === 1 ? "" : "s"} · ${toolCount} tool call${toolCount === 1 ? "" : "s"}${errCount ? ` · ${errCount} err` : ""} · ${elapsed}s`, "info");
|
|
1163
|
+
orchTranscript.record({
|
|
1164
|
+
kind: "phase-end",
|
|
1165
|
+
ts: new Date().toISOString(),
|
|
1166
|
+
phase: phase.role,
|
|
1167
|
+
phaseIndex: currentPhaseIndex,
|
|
1168
|
+
attempt: (iterationCounts[phase.role] ?? 0) + 1,
|
|
1169
|
+
verdict: "n/a",
|
|
1170
|
+
elapsedMs: Date.now() - phaseStart,
|
|
1171
|
+
turns: turn,
|
|
1172
|
+
toolCount,
|
|
1173
|
+
errCount,
|
|
1174
|
+
subagentTranscriptPath: result.subagentTranscriptPath,
|
|
1175
|
+
});
|
|
1176
|
+
registry.appendTail(bugId, phase.role, fmtPhaseSummary({
|
|
1177
|
+
role: phase.role,
|
|
1178
|
+
turns: turn,
|
|
1179
|
+
tools: toolCount,
|
|
1180
|
+
errors: errCount,
|
|
1181
|
+
wallSeconds: elapsed,
|
|
1182
|
+
usage: cumUsage,
|
|
1183
|
+
model: result.model,
|
|
1184
|
+
provider: result.provider,
|
|
1185
|
+
compression: cumCompression.tokensSaved > 0 ? cumCompression : undefined,
|
|
1186
|
+
}));
|
|
1187
|
+
}
|
|
1188
|
+
// ── Slice-2: orchestrator emits phase event ──────────────────
|
|
1189
|
+
// sprintId for bug event emission is the literal "bugs" (routing key),
|
|
1190
|
+
// matching the convention in .forge/workflows/fix_bug.md.
|
|
1191
|
+
const phaseEndMs = Date.now();
|
|
1192
|
+
const bugRecord = readBugRecord(bugId, storeCli, cwd);
|
|
1193
|
+
const sprintId = "bugs"; // routing key for bug events — not a sprint reference
|
|
1194
|
+
const phaseIteration = (iterationCounts[phase.role] ?? 0) + 1;
|
|
1195
|
+
// Read summary judgement for review phases (using bug summary key map)
|
|
1196
|
+
const judgement = phase.isReview
|
|
1197
|
+
? judgementFromSummary(bugRecord ?? null, phase.role, BUG_SUMMARY_KEY_BY_ROLE)
|
|
1198
|
+
: undefined;
|
|
1199
|
+
const emitCtx = {
|
|
1200
|
+
entityType: "bug",
|
|
1201
|
+
bugId,
|
|
1202
|
+
sprintId, // routing key "bugs" — not a sprint reference
|
|
1203
|
+
phase,
|
|
1204
|
+
iteration: phaseIteration,
|
|
1205
|
+
startMs: phaseStart,
|
|
1206
|
+
endMs: phaseEndMs,
|
|
1207
|
+
model: result.model ?? "unknown",
|
|
1208
|
+
provider: result.provider ?? "unknown",
|
|
1209
|
+
usage: {
|
|
1210
|
+
input: result.usage.input,
|
|
1211
|
+
output: result.usage.output,
|
|
1212
|
+
cacheRead: result.usage.cacheRead,
|
|
1213
|
+
cacheWrite: result.usage.cacheWrite,
|
|
1214
|
+
},
|
|
1215
|
+
judgement,
|
|
1216
|
+
storeCli,
|
|
1217
|
+
cwd,
|
|
1218
|
+
};
|
|
1219
|
+
const phaseEvent = buildPhaseEvent(emitCtx);
|
|
1220
|
+
// Set bug event type based on BUG_TYPE_TOKENS mapping.
|
|
1221
|
+
const typeTokenEntry = BUG_TYPE_TOKENS[phase.role];
|
|
1222
|
+
if (typeTokenEntry) {
|
|
1223
|
+
if (phase.isReview && judgement?.verdict === "revision") {
|
|
1224
|
+
phaseEvent.type = typeTokenEntry.fail;
|
|
1225
|
+
}
|
|
1226
|
+
else {
|
|
1227
|
+
phaseEvent.type = typeTokenEntry.pass;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
const emitResult = emitEvent(storeCli, cwd, sprintId, phaseEvent);
|
|
1231
|
+
if (!emitResult.ok) {
|
|
1232
|
+
ctx.ui.notify(`⚠ forge:fix-bug — phase event emit failed for ${phase.role}: ${emitResult.stderr.trim()}`, "warning");
|
|
1233
|
+
writeDebug({ kind: "emit_failed", stderr: emitResult.stderr });
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
writeDebug({ kind: "emit_ok", eventId: phaseEvent.eventId });
|
|
1237
|
+
}
|
|
1238
|
+
// Drain friction file for this phase.
|
|
1239
|
+
const frictionPath = path.join(cwd, ".forge", "cache", `FRICTION-${phase.role}.jsonl`);
|
|
1240
|
+
const drain = drainFrictionFile(frictionPath, emitCtx);
|
|
1241
|
+
if (drain.emitted + drain.failed > 0) {
|
|
1242
|
+
writeDebug({ kind: "friction_drain", ...drain });
|
|
1243
|
+
if (drain.failed > 0) {
|
|
1244
|
+
ctx.ui.notify(`⚠ forge:fix-bug — friction drain for ${phase.role}: ${drain.emitted} ok, ${drain.failed} failed`, "warning");
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
// ── AC §C.16: Bug FSM canonical-enum assertion ────────────────
|
|
1248
|
+
// After each phase that could transition bug status, validate the new
|
|
1249
|
+
// status via store-cli (single source of truth). Surface a warning (not halt) if invalid.
|
|
1250
|
+
const currentBugRecordForAssert = readBugRecord(bugId, storeCli, cwd);
|
|
1251
|
+
if (currentBugRecordForAssert && currentBugRecordForAssert.status) {
|
|
1252
|
+
// Defer to store-cli's isLegalTransition as authoritative guard.
|
|
1253
|
+
// Only warn on statuses store-cli itself would reject.
|
|
1254
|
+
const validateResult = spawnSync("node", [storeCli, "validate", "bug", JSON.stringify(currentBugRecordForAssert)], { cwd, encoding: "utf8" });
|
|
1255
|
+
if (validateResult.status !== 0) {
|
|
1256
|
+
const detail = typeof validateResult.stderr === "string" ? validateResult.stderr.trim() : "unknown";
|
|
1257
|
+
ctx.ui.notify(`⚠ forge:fix-bug — bug ${bugId} validation warning: ${detail}`, "warning");
|
|
1258
|
+
writeDebug({ kind: "fsm_assertion_warning", bugId, status: currentBugRecordForAssert.status, detail });
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// ── 6b. Verdict check (review phases only) ────────────────────
|
|
1262
|
+
if (phase.isReview) {
|
|
1263
|
+
// Re-read bug record for latest status after subagent ran
|
|
1264
|
+
const updatedBugRecord = readBugRecord(bugId, storeCli, cwd);
|
|
1265
|
+
const verdict = readBugVerdict(updatedBugRecord, phase.role, BUG_SUMMARY_KEY_BY_ROLE);
|
|
1266
|
+
if (verdict === "missing") {
|
|
1267
|
+
ctx.ui.notify(`× forge:fix-bug — verdict missing for phase ${phase.role} after subagent completed. Halting for advisory.`, "error");
|
|
1268
|
+
finishPhaseNode("failed");
|
|
1269
|
+
writeBugState(cwd, {
|
|
1270
|
+
bugId,
|
|
1271
|
+
phaseIndex: currentPhaseIndex,
|
|
1272
|
+
iterationCounts,
|
|
1273
|
+
halted: true,
|
|
1274
|
+
lastError: `verdict missing for ${phase.role}`,
|
|
1275
|
+
savedAt: new Date().toISOString(),
|
|
1276
|
+
});
|
|
1277
|
+
// A missing verdict IS a postflight-outputs failure: the canonical
|
|
1278
|
+
// phase summary the subagent must write (e.g. summaries.code_review,
|
|
1279
|
+
// linked via set-bug-summary) was never recorded, so there is no
|
|
1280
|
+
// verdict to route on. Route it through the halt-recovery advisor
|
|
1281
|
+
// (FORGE-S26-T18) — the same hand-off the preflight/postflight gate
|
|
1282
|
+
// failures use — instead of a bare escalation. Best-effort, non-fatal.
|
|
1283
|
+
const advisorModel = resolveAdvisorModel(modelRoutingConfig, ctx.model);
|
|
1284
|
+
void runHaltAdvisor({
|
|
1285
|
+
gateFailure: {
|
|
1286
|
+
phase: phase.role,
|
|
1287
|
+
reasonCode: "verdict-missing",
|
|
1288
|
+
detail: `Phase '${phase.role}' completed but no verdict was found in the store. ` +
|
|
1289
|
+
"The canonical phase summary was not written, so the orchestrator has no verdict to route on.",
|
|
1290
|
+
remediation: "Re-run the phase and ensure the subagent's forge_store set-bug-summary call " +
|
|
1291
|
+
'uses args:["<bugId>", "<phaseKey>"] with the literal phase key as args[1] ' +
|
|
1292
|
+
"(e.g. code_review), and that the call exits zero before the subagent returns.",
|
|
1293
|
+
},
|
|
1294
|
+
advisorModel,
|
|
1295
|
+
taskId: bugId,
|
|
1296
|
+
cwd,
|
|
1297
|
+
ctx: { ui: ctx.ui },
|
|
1298
|
+
forgeRoot,
|
|
1299
|
+
});
|
|
1300
|
+
return {
|
|
1301
|
+
status: "halted",
|
|
1302
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
1303
|
+
iterationCounts,
|
|
1304
|
+
lastError: `verdict missing for ${phase.role}`,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
if (verdict === "revision") {
|
|
1308
|
+
iterationCounts[phase.role] = (iterationCounts[phase.role] ?? 0) + 1;
|
|
1309
|
+
if (iterationCounts[phase.role] >= phase.maxIterations) {
|
|
1310
|
+
ctx.ui.notify(`× forge:fix-bug — revision cap reached for phase ${phase.role} ` +
|
|
1311
|
+
`(${iterationCounts[phase.role]}/${phase.maxIterations} iterations). Escalating.`, "error");
|
|
1312
|
+
finishPhaseNode("escalated");
|
|
1313
|
+
writeBugState(cwd, {
|
|
1314
|
+
bugId,
|
|
1315
|
+
phaseIndex: currentPhaseIndex,
|
|
1316
|
+
iterationCounts,
|
|
1317
|
+
halted: true,
|
|
1318
|
+
lastError: `revision cap reached for ${phase.role}`,
|
|
1319
|
+
savedAt: new Date().toISOString(),
|
|
1320
|
+
});
|
|
1321
|
+
return {
|
|
1322
|
+
status: "escalated",
|
|
1323
|
+
lastPhaseIndex: currentPhaseIndex,
|
|
1324
|
+
iterationCounts,
|
|
1325
|
+
lastError: `revision cap reached for ${phase.role}`,
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
// Transition bug back to in-progress before re-dispatching implement.
|
|
1329
|
+
// This is required for review-code → implement and approve → implement loops.
|
|
1330
|
+
const currentBugStatus = updatedBugRecord?.status;
|
|
1331
|
+
if (currentBugStatus === "fixed" || currentBugStatus === "approved") {
|
|
1332
|
+
const transitionResult = spawnSync("node", [storeCli, "update-status", "bug", bugId, "status", "in-progress"], { cwd, encoding: "utf8" });
|
|
1333
|
+
if (transitionResult.status !== 0) {
|
|
1334
|
+
ctx.ui.notify(`⚠ forge:fix-bug — failed to transition bug ${bugId} from ${currentBugStatus} to in-progress: ${transitionResult.stderr ?? "unknown"}`, "warning");
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
ctx.ui.notify(`⟳ forge:fix-bug — transitioned bug ${bugId}: ${currentBugStatus} → in-progress`, "info");
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
// The review subagent itself finished cleanly — `revision`
|
|
1341
|
+
// is its verdict, not a failure — so its node closes as
|
|
1342
|
+
// completed before the predecessor re-dispatches under a
|
|
1343
|
+
// fresh node ID.
|
|
1344
|
+
finishPhaseNode("completed");
|
|
1345
|
+
const predIndex = findPredecessorIndex(BUG_PHASES, currentPhaseIndex);
|
|
1346
|
+
ctx.ui.notify(`⟳ forge:fix-bug — ${phase.role} returned revision; looping to ${BUG_PHASES[predIndex]?.role ?? predIndex} ` +
|
|
1347
|
+
`(attempt ${iterationCounts[phase.role]}/${phase.maxIterations})`, "info");
|
|
1348
|
+
orchTranscript.record({
|
|
1349
|
+
kind: "phase-loopback",
|
|
1350
|
+
ts: new Date().toISOString(),
|
|
1351
|
+
fromPhase: phase.role,
|
|
1352
|
+
toPhase: BUG_PHASES[predIndex]?.role ?? String(predIndex),
|
|
1353
|
+
fromPhaseIndex: currentPhaseIndex,
|
|
1354
|
+
toPhaseIndex: predIndex,
|
|
1355
|
+
reason: `${phase.role} returned revision (attempt ${iterationCounts[phase.role]}/${phase.maxIterations})`,
|
|
1356
|
+
});
|
|
1357
|
+
writeBugState(cwd, {
|
|
1358
|
+
bugId,
|
|
1359
|
+
phaseIndex: predIndex,
|
|
1360
|
+
iterationCounts,
|
|
1361
|
+
halted: false,
|
|
1362
|
+
savedAt: new Date().toISOString(),
|
|
1363
|
+
});
|
|
1364
|
+
currentPhaseIndex = predIndex;
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
// verdict === "approved": fall through to advance
|
|
1368
|
+
}
|
|
1369
|
+
// ── Advance to next phase ─────────────────────────────────────
|
|
1370
|
+
registry.completePhase(bugId, phase.role, "completed");
|
|
1371
|
+
finishPhaseNode("completed");
|
|
1372
|
+
writeBugState(cwd, {
|
|
1373
|
+
bugId,
|
|
1374
|
+
phaseIndex: currentPhaseIndex,
|
|
1375
|
+
iterationCounts,
|
|
1376
|
+
halted: false,
|
|
1377
|
+
savedAt: new Date().toISOString(),
|
|
1378
|
+
});
|
|
1379
|
+
// ── 6c. Path A / Path B branch (post-triage) ──────────────────
|
|
1380
|
+
// Per meta-fix-bug.md § Triage Judgement (forge v0.44.0+), the
|
|
1381
|
+
// triage subagent records the route decision in
|
|
1382
|
+
// bug.summaries.triage.route. The orchestrator reads it after
|
|
1383
|
+
// triage returns and selects the downstream phase list:
|
|
1384
|
+
// Path A (short-circuit): skip plan-fix + review-plan
|
|
1385
|
+
// Path B (default, full loop): run all phases
|
|
1386
|
+
//
|
|
1387
|
+
// If route is missing or malformed, default to Path B (the safe
|
|
1388
|
+
// choice — running extra phases never produces an unsafe outcome).
|
|
1389
|
+
// The PHASE_SKIP_STATES heuristic at section 6a remains as
|
|
1390
|
+
// defense-in-depth for cases where the field is missing but the
|
|
1391
|
+
// bug status proves the work happened.
|
|
1392
|
+
if (phase.role === "triage") {
|
|
1393
|
+
const bugAfterTriage = readBugRecord(bugId, storeCli, cwd);
|
|
1394
|
+
const triageSummary = bugAfterTriage?.summaries?.triage;
|
|
1395
|
+
const route = triageSummary?.route;
|
|
1396
|
+
if (route === "A") {
|
|
1397
|
+
const skipUntilIndex = BUG_PHASES.findIndex((p) => p.role === "implement");
|
|
1398
|
+
if (skipUntilIndex > currentPhaseIndex + 1) {
|
|
1399
|
+
ctx.ui.notify(`⊘ forge:fix-bug — Path A selected by triage; skipping plan-fix and review-plan.`, "info");
|
|
1400
|
+
currentPhaseIndex = skipUntilIndex;
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
// route === "B", missing, or any other value → fall through to standard advance
|
|
1405
|
+
}
|
|
1406
|
+
currentPhaseIndex++;
|
|
1407
|
+
}
|
|
1408
|
+
// ── All phases complete ───────────────────────────────────────────
|
|
1409
|
+
deleteBugState(cwd, bugId);
|
|
1410
|
+
orchTranscript.record({
|
|
1411
|
+
kind: "pipeline-end",
|
|
1412
|
+
ts: new Date().toISOString(),
|
|
1413
|
+
outcome: "complete",
|
|
1414
|
+
elapsedMs: Date.now() - pipelineStartMs,
|
|
1415
|
+
});
|
|
1416
|
+
return {
|
|
1417
|
+
status: "completed",
|
|
1418
|
+
lastPhaseIndex: BUG_PHASES.length - 1,
|
|
1419
|
+
iterationCounts,
|
|
1420
|
+
model: lastModel,
|
|
1421
|
+
provider: lastProvider,
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
finally {
|
|
1425
|
+
ctx.ui.notify = __origNotify;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
export function registerFixBug(pi, options = {}) {
|
|
1429
|
+
pi.registerCommand("forge:fix-bug", {
|
|
1430
|
+
description: "Run the full bug-fix pipeline (triage → plan-fix → review-plan → implement → review-code → approve → commit). " +
|
|
1431
|
+
"Usage: /forge:fix-bug <BUG_ID_OR_SUMMARY>. " +
|
|
1432
|
+
"Orchestrator archetype: each phase is an isolated subagent session (IL10).",
|
|
1433
|
+
async handler(args, ctx) {
|
|
1434
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1435
|
+
const rawArg = args.trim();
|
|
1436
|
+
if (!rawArg) {
|
|
1437
|
+
ctx.ui.notify("× forge:fix-bug — bug ID or summary required. Usage: /forge:fix-bug <BUG_ID_OR_SUMMARY>", "error");
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
ctx.ui.setStatus?.(STATUS_KEY, `fix-bug: initializing…`);
|
|
1441
|
+
// ── Discover forge config ────────────────────────────────────────
|
|
1442
|
+
const forgeConfig = discoverForgeConfigCached(cwd);
|
|
1443
|
+
if (!forgeConfig) {
|
|
1444
|
+
ctx.ui.notify("× forge:fix-bug — no Forge project found at cwd. Run /forge:init first.", "error");
|
|
1445
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1446
|
+
ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
const forgeRoot = forgeConfig.forgeRoot;
|
|
1450
|
+
// Best-effort transcript-archive sweep: adopt any project-local runs
|
|
1451
|
+
// not yet in the central index (crash recovery + pre-existing
|
|
1452
|
+
// history). Runs BEFORE this pipeline creates its own transcript
|
|
1453
|
+
// writer, so the in-flight run is never swept half-written.
|
|
1454
|
+
sweepProjectTranscripts(cwd);
|
|
1455
|
+
// Tool paths
|
|
1456
|
+
const storeCli = path.join(forgeRoot, "tools", "store-cli.cjs");
|
|
1457
|
+
const preflightGate = path.join(forgeRoot, "tools", "preflight-gate.cjs");
|
|
1458
|
+
// ── Determine bugId ────────────────────────────────────────────
|
|
1459
|
+
let bugId;
|
|
1460
|
+
let isNewBug = false;
|
|
1461
|
+
// Check if arg looks like it could be a bug ID (prefixed or unprefixed).
|
|
1462
|
+
// Covers: FORGE-BUG-042, BUG-042, B042.
|
|
1463
|
+
const looksLikeBugId = /^(?:[A-Z0-9]+-)?(?:BUG-?\d+|B\d+)$/i.test(rawArg) || /^BUG-\d+$/i.test(rawArg);
|
|
1464
|
+
if (/^[A-Z][A-Z0-9]*-BUG-\d+$/.test(rawArg)) {
|
|
1465
|
+
// Canonical prefixed bug ID (any project prefix, e.g. FORGE-BUG-042,
|
|
1466
|
+
// CART-BUG-001) — verify it exists. Previously hardcoded to FORGE-,
|
|
1467
|
+
// which pushed other-prefix canonical IDs through the resolver.
|
|
1468
|
+
bugId = rawArg;
|
|
1469
|
+
const bugRecord = readBugRecord(bugId, storeCli, cwd);
|
|
1470
|
+
if (!bugRecord) {
|
|
1471
|
+
ctx.ui.notify(`× forge:fix-bug — bug ${bugId} not found in store.`, "error");
|
|
1472
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
// Check if bug is already in a terminal state
|
|
1476
|
+
if (BUG_TERMINAL_STATES.has(bugRecord.status ?? "")) {
|
|
1477
|
+
ctx.ui.notify(`× forge:fix-bug — bug ${bugId} is already in terminal state '${bugRecord.status}'. No further processing.`, "error");
|
|
1478
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
else if (looksLikeBugId) {
|
|
1483
|
+
// Unprefixed bug ID — resolve through the store cascade.
|
|
1484
|
+
// Issue #20: unprefixed entity IDs silently poisoned substitutions.
|
|
1485
|
+
const toolDir = resolveToolDir(forgeRoot);
|
|
1486
|
+
const resolvedBugId = await resolveToCanonicalId(rawArg, toolDir, cwd, "bug", {
|
|
1487
|
+
ctx,
|
|
1488
|
+
commandLabel: "forge:fix-bug",
|
|
1489
|
+
});
|
|
1490
|
+
if (!resolvedBugId) {
|
|
1491
|
+
// Error already emitted by resolver
|
|
1492
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1493
|
+
ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
bugId = resolvedBugId;
|
|
1497
|
+
// Re-verify the resolved bug exists
|
|
1498
|
+
const bugRecord = readBugRecord(bugId, storeCli, cwd);
|
|
1499
|
+
if (!bugRecord) {
|
|
1500
|
+
ctx.ui.notify(`× forge:fix-bug — bug ${bugId} not found in store.`, "error");
|
|
1501
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (BUG_TERMINAL_STATES.has(bugRecord.status ?? "")) {
|
|
1505
|
+
ctx.ui.notify(`× forge:fix-bug — bug ${bugId} is already in terminal state '${bugRecord.status}'. No further processing.`, "error");
|
|
1506
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
else {
|
|
1511
|
+
// @file or free-form text. If an @file report references a canonical
|
|
1512
|
+
// <PREFIX>-BUG-NNN that already exists in the store, fix THAT record —
|
|
1513
|
+
// minting a new bug here duplicated CART-BUG-001 as a phantom in the
|
|
1514
|
+
// CART incident (the report header carried the real ID all along).
|
|
1515
|
+
let reportBugId = null;
|
|
1516
|
+
if (rawArg.startsWith("@")) {
|
|
1517
|
+
const rel = rawArg.slice(1).trim();
|
|
1518
|
+
const reportPath = path.isAbsolute(rel) ? rel : path.join(cwd, rel);
|
|
1519
|
+
try {
|
|
1520
|
+
// 256 KB cap — bug reports are small; never slurp arbitrary files.
|
|
1521
|
+
if (fs.existsSync(reportPath) && fs.statSync(reportPath).size <= 262144) {
|
|
1522
|
+
const projectPrefix = loadGovernorProjectConfig(cwd).prefix;
|
|
1523
|
+
reportBugId = extractBugIdFromReportText(fs.readFileSync(reportPath, "utf8"), projectPrefix);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
catch {
|
|
1527
|
+
/* unreadable report — fall through to new-bug intake */
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
const reportRecord = reportBugId ? readBugRecord(reportBugId, storeCli, cwd) : null;
|
|
1531
|
+
if (reportBugId && reportRecord) {
|
|
1532
|
+
if (BUG_TERMINAL_STATES.has(reportRecord.status ?? "")) {
|
|
1533
|
+
ctx.ui.notify(`× forge:fix-bug — ${rawArg} references ${reportBugId}, which is already in terminal state ` +
|
|
1534
|
+
`'${reportRecord.status}'. No further processing.`, "error");
|
|
1535
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
bugId = reportBugId;
|
|
1539
|
+
ctx.ui.notify(`forge:fix-bug — ${rawArg} references existing bug ${bugId}; fixing it (no new record minted).`, "info");
|
|
1540
|
+
}
|
|
1541
|
+
else {
|
|
1542
|
+
// Free-form text (or report with no resolvable ID) — defer bug
|
|
1543
|
+
// creation to the triage-phase subagent via a PENDING placeholder.
|
|
1544
|
+
bugId = `PENDING-${Date.now()}`;
|
|
1545
|
+
isNewBug = true;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
// ── Pre-flight confirm ───────────────────────────────────────────
|
|
1549
|
+
if (!isNonInteractive()) {
|
|
1550
|
+
const confirmMsg = isNewBug
|
|
1551
|
+
? `Fix bug: "${rawArg.slice(0, 80)}"? A bug record will be created during triage.`
|
|
1552
|
+
: `Fix bug ${bugId}?`;
|
|
1553
|
+
const proceed = await ctx.ui.confirm(`Fix bug?`, confirmMsg);
|
|
1554
|
+
if (!proceed) {
|
|
1555
|
+
ctx.ui.notify("forge:fix-bug — cancelled.", "info");
|
|
1556
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
// ── Resume detection ─────────────────────────────────────────────
|
|
1561
|
+
const registry = getSessionRegistry();
|
|
1562
|
+
const existing = isNewBug ? null : readBugState(cwd, bugId);
|
|
1563
|
+
let resumeFromState;
|
|
1564
|
+
if (existing) {
|
|
1565
|
+
if (isBugStateStale(existing)) {
|
|
1566
|
+
ctx.ui.notify(`⚠ forge:fix-bug — cached state for ${bugId} is stale (>7 days old, saved at ${formatLocalTime(existing.savedAt)}). Offering purge.`, "warning");
|
|
1567
|
+
if (!isNonInteractive()) {
|
|
1568
|
+
const purge = await ctx.ui.confirm(`Purge stale state for ${bugId}?`, "The cached state is older than 7 days. Purge and restart from the beginning?");
|
|
1569
|
+
if (purge) {
|
|
1570
|
+
deleteBugState(cwd, bugId);
|
|
1571
|
+
}
|
|
1572
|
+
else {
|
|
1573
|
+
ctx.ui.notify("forge:fix-bug — stale state kept; aborting.", "info");
|
|
1574
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1575
|
+
ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
else {
|
|
1580
|
+
ctx.ui.notify("forge:fix-bug — stale state; non-interactive mode auto-aborting.", "info");
|
|
1581
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1582
|
+
ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
else {
|
|
1587
|
+
// ADR-S21-01: offer resume for ALL non-stale states — halted=true
|
|
1588
|
+
// (explicit failure), halted=false (cancelled/interrupted), and
|
|
1589
|
+
// any state with existing.status set.
|
|
1590
|
+
const stateStatus = existing.status ?? (existing.halted ? "halted" : "interrupted");
|
|
1591
|
+
const statusLabel = stateStatus === "cancelled" ? "cancelled" : stateStatus === "halted" ? "halted" : "interrupted";
|
|
1592
|
+
const phaseRole = BUG_PHASES[existing.phaseIndex]?.role ?? existing.phaseIndex;
|
|
1593
|
+
if (!isNonInteractive()) {
|
|
1594
|
+
const resume = await ctx.ui.confirm(`Resume ${bugId}?`, `Cached state — phase ${existing.phaseIndex} (${phaseRole}), ${statusLabel}, ` +
|
|
1595
|
+
`saved at ${formatLocalTime(existing.savedAt)}. Resume from here?`);
|
|
1596
|
+
if (resume) {
|
|
1597
|
+
resumeFromState = existing;
|
|
1598
|
+
ctx.ui.notify(`forge:fix-bug — resuming ${bugId} from phase ${phaseRole} (${statusLabel})`, "info");
|
|
1599
|
+
}
|
|
1600
|
+
else {
|
|
1601
|
+
deleteBugState(cwd, bugId);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
else {
|
|
1605
|
+
// Non-interactive: auto-resume from state (no confirmation).
|
|
1606
|
+
// Cancelled/interrupted states are valid resume points.
|
|
1607
|
+
resumeFromState = existing;
|
|
1608
|
+
ctx.ui.notify(`forge:fix-bug — resuming ${bugId} from phase ${phaseRole} (${statusLabel})`, "info");
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
// For new bugs, triage phase will create the bug record.
|
|
1613
|
+
// After triage, we need to capture the bugId from the subagent events.
|
|
1614
|
+
// This is handled inside runBugPipeline via onEvent interception.
|
|
1615
|
+
// For now, we pass the temporary bugId; runBugPipeline will update it.
|
|
1616
|
+
// ── Materialization check (top-level workflow) ──────────────────
|
|
1617
|
+
const workflowPath = path.join(cwd, ".forge", "workflows", "fix_bug.md");
|
|
1618
|
+
if (fs.existsSync(workflowPath)) {
|
|
1619
|
+
try {
|
|
1620
|
+
const loaded = loadWorkflow(workflowPath);
|
|
1621
|
+
// AC#12: Top-level audience check for the fix_bug.md workflow.
|
|
1622
|
+
// The orchestrator ITSELF runs fix_bug.md (not a subagent), so check
|
|
1623
|
+
// from orchestrator context. Using asSubagent would falsely reject
|
|
1624
|
+
// orchestrator-only workflows called by the orchestrator.
|
|
1625
|
+
const topAudienceOk = CallerContextStore.asOrchestrator(() => assertAudience({ workflowName: "fix_bug", audience: loaded.audience }, ctx));
|
|
1626
|
+
if (!topAudienceOk) {
|
|
1627
|
+
ctx.ui.notify("× forge:fix-bug — audience check failed for top-level fix_bug workflow.", "error");
|
|
1628
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1629
|
+
ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
// Note: no materialization-marker check here. fix_bug.md is the
|
|
1633
|
+
// orchestrator workflow (prose algorithm), not a sub-workflow that
|
|
1634
|
+
// subagents run directly. Per-phase sub-workflows (architect_approve,
|
|
1635
|
+
// review_code, etc.) each get their own materialization check inside
|
|
1636
|
+
// runBugPipeline at line ~481, which is the correct guard layer.
|
|
1637
|
+
}
|
|
1638
|
+
catch {
|
|
1639
|
+
// Workflow file exists but couldn't be read — non-fatal, continue
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
// ── Pre-assign real bug ID for new bugs ────────────────────────
|
|
1643
|
+
// Previously this was done inside runBugPipeline, but the session registry
|
|
1644
|
+
// needs the real ID before startSession is called.
|
|
1645
|
+
if (isNewBug && bugId.startsWith("PENDING-")) {
|
|
1646
|
+
const realBugId = assignNextBugId(storeCli, cwd, loadGovernorProjectConfig(cwd).prefix);
|
|
1647
|
+
const title = rawArg && !rawArg.startsWith("@") ? rawArg.slice(0, 120) : "New bug (pending triage)";
|
|
1648
|
+
if (preCreateBug(realBugId, title, storeCli, cwd)) {
|
|
1649
|
+
ctx.ui.notify(`forge:fix-bug — pre-assigned bug ID: ${realBugId}`, "info");
|
|
1650
|
+
bugId = realBugId;
|
|
1651
|
+
}
|
|
1652
|
+
else {
|
|
1653
|
+
ctx.ui.notify("× forge:fix-bug — failed to pre-create bug record. Falling back to PENDING capture.", "error");
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
// Register session
|
|
1657
|
+
registry.startSession(bugId);
|
|
1658
|
+
// Bridge: register bug in OrchestratorTree.
|
|
1659
|
+
const tree = getOrchestratorTree();
|
|
1660
|
+
tree.startNode(bugId, { label: `fix-bug ${bugId}`, kind: "orchestrator" });
|
|
1661
|
+
// ── Delegate to pipeline ─────────────────────────────────────────
|
|
1662
|
+
// ── Delegate to pipeline ─────────────────────────────────────────
|
|
1663
|
+
const signal = registry.getAbortSignal(bugId);
|
|
1664
|
+
const pipelineResult = await runBugPipeline({
|
|
1665
|
+
bugId,
|
|
1666
|
+
originalArg: isNewBug ? rawArg : undefined,
|
|
1667
|
+
isNewBug,
|
|
1668
|
+
cwd,
|
|
1669
|
+
ctx,
|
|
1670
|
+
forgeRoot,
|
|
1671
|
+
storeCli,
|
|
1672
|
+
preflightGate,
|
|
1673
|
+
registry,
|
|
1674
|
+
resumeFromState,
|
|
1675
|
+
signal,
|
|
1676
|
+
forgeToolDefs: options.forgeToolDefs,
|
|
1677
|
+
});
|
|
1678
|
+
// ── Handle result ────────────────────────────────────────────────
|
|
1679
|
+
if (pipelineResult.status === "completed") {
|
|
1680
|
+
registry.completeSession(bugId, "completed");
|
|
1681
|
+
tree.completeNode(bugId, "completed");
|
|
1682
|
+
ctx.ui.notify(`〇 forge:fix-bug — ${bugId} pipeline complete (${BUG_PHASES.length} phases).`, "info");
|
|
1683
|
+
}
|
|
1684
|
+
else if (pipelineResult.status === "cancelled") {
|
|
1685
|
+
registry.completeSession(bugId, "cancelled");
|
|
1686
|
+
tree.completeNode(bugId, "cancelled");
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
registry.completeSession(bugId, "failed");
|
|
1690
|
+
tree.completeNode(bugId, "failed");
|
|
1691
|
+
}
|
|
1692
|
+
// Mirror this run into the central transcript archive (best-effort —
|
|
1693
|
+
// archiveRun never throws).
|
|
1694
|
+
if (pipelineResult.orchestratorTranscriptPath) {
|
|
1695
|
+
archiveRun({
|
|
1696
|
+
cwd,
|
|
1697
|
+
orchestratorJsonlPath: pipelineResult.orchestratorTranscriptPath,
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
1701
|
+
ctx.ui.setStatus?.(MESSAGE_KEY, undefined);
|
|
1702
|
+
},
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
//# sourceMappingURL=fix-bug.js.map
|