all-hands-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.allhands/README.md +75 -0
- package/.allhands/agents/compounder.yaml +15 -0
- package/.allhands/agents/coordinator.yaml +17 -0
- package/.allhands/agents/documentor.yaml +15 -0
- package/.allhands/agents/e2e-test-planner.yaml +17 -0
- package/.allhands/agents/emergent.yaml +22 -0
- package/.allhands/agents/executor.yaml +14 -0
- package/.allhands/agents/ideation.yaml +11 -0
- package/.allhands/agents/initiative-steering.yaml +19 -0
- package/.allhands/agents/judge.yaml +13 -0
- package/.allhands/agents/planner.yaml +19 -0
- package/.allhands/agents/pr-reviewer.yaml +15 -0
- package/.allhands/docs.json +5 -0
- package/.allhands/docs.local.json +26 -0
- package/.allhands/flows/COMPOUNDING.md +203 -0
- package/.allhands/flows/COORDINATION.md +89 -0
- package/.allhands/flows/CORE.md +87 -0
- package/.allhands/flows/DOCUMENTATION.md +218 -0
- package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
- package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
- package/.allhands/flows/IDEATION_SCOPING.md +154 -0
- package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
- package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
- package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
- package/.allhands/flows/PR_REVIEWING.md +43 -0
- package/.allhands/flows/SPEC_PLANNING.md +216 -0
- package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
- package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
- package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
- package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
- package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
- package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
- package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
- package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
- package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
- package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
- package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
- package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
- package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
- package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
- package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
- package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
- package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
- package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
- package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
- package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
- package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
- package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
- package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
- package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
- package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
- package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
- package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
- package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
- package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
- package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
- package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
- package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
- package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
- package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
- package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
- package/.allhands/harness/ah +131 -0
- package/.allhands/harness/package-lock.json +5292 -0
- package/.allhands/harness/package.json +52 -0
- package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
- package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
- package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
- package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
- package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
- package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
- package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
- package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
- package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
- package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
- package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
- package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
- package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
- package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
- package/.allhands/harness/src/cli.ts +36 -0
- package/.allhands/harness/src/commands/complexity.ts +177 -0
- package/.allhands/harness/src/commands/context7.ts +202 -0
- package/.allhands/harness/src/commands/docs.ts +557 -0
- package/.allhands/harness/src/commands/hooks.ts +24 -0
- package/.allhands/harness/src/commands/index.ts +51 -0
- package/.allhands/harness/src/commands/knowledge.ts +382 -0
- package/.allhands/harness/src/commands/memories.ts +302 -0
- package/.allhands/harness/src/commands/notify.ts +61 -0
- package/.allhands/harness/src/commands/oracle.ts +158 -0
- package/.allhands/harness/src/commands/perplexity.ts +220 -0
- package/.allhands/harness/src/commands/planning.ts +245 -0
- package/.allhands/harness/src/commands/schema.ts +73 -0
- package/.allhands/harness/src/commands/skills.ts +128 -0
- package/.allhands/harness/src/commands/solutions.ts +353 -0
- package/.allhands/harness/src/commands/spawn.ts +158 -0
- package/.allhands/harness/src/commands/specs.ts +532 -0
- package/.allhands/harness/src/commands/tavily.ts +226 -0
- package/.allhands/harness/src/commands/tools.ts +579 -0
- package/.allhands/harness/src/commands/trace.ts +327 -0
- package/.allhands/harness/src/commands/tui.ts +960 -0
- package/.allhands/harness/src/commands/validate.ts +143 -0
- package/.allhands/harness/src/commands/validation-tools.ts +108 -0
- package/.allhands/harness/src/hooks/context.ts +1442 -0
- package/.allhands/harness/src/hooks/enforcement.ts +170 -0
- package/.allhands/harness/src/hooks/index.ts +54 -0
- package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
- package/.allhands/harness/src/hooks/notification.ts +104 -0
- package/.allhands/harness/src/hooks/observability.ts +551 -0
- package/.allhands/harness/src/hooks/session.ts +88 -0
- package/.allhands/harness/src/hooks/shared.ts +815 -0
- package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
- package/.allhands/harness/src/hooks/validation.ts +617 -0
- package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
- package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
- package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
- package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
- package/.allhands/harness/src/lib/base-command.ts +198 -0
- package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
- package/.allhands/harness/src/lib/compaction.ts +313 -0
- package/.allhands/harness/src/lib/ctags.ts +497 -0
- package/.allhands/harness/src/lib/docs-validation.ts +907 -0
- package/.allhands/harness/src/lib/event-loop.ts +662 -0
- package/.allhands/harness/src/lib/flows.ts +155 -0
- package/.allhands/harness/src/lib/git.ts +276 -0
- package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
- package/.allhands/harness/src/lib/knowledge.ts +810 -0
- package/.allhands/harness/src/lib/llm.ts +255 -0
- package/.allhands/harness/src/lib/mcp-client.ts +432 -0
- package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
- package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
- package/.allhands/harness/src/lib/notification.ts +115 -0
- package/.allhands/harness/src/lib/opencode/index.ts +70 -0
- package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
- package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
- package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
- package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
- package/.allhands/harness/src/lib/oracle.ts +926 -0
- package/.allhands/harness/src/lib/planning-utils.ts +150 -0
- package/.allhands/harness/src/lib/planning.ts +605 -0
- package/.allhands/harness/src/lib/pr-review.ts +225 -0
- package/.allhands/harness/src/lib/prompts.ts +522 -0
- package/.allhands/harness/src/lib/schema.ts +418 -0
- package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
- package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
- package/.allhands/harness/src/lib/session.ts +164 -0
- package/.allhands/harness/src/lib/specs.ts +348 -0
- package/.allhands/harness/src/lib/tldr.ts +829 -0
- package/.allhands/harness/src/lib/tmux.ts +1051 -0
- package/.allhands/harness/src/lib/trace-store.ts +714 -0
- package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
- package/.allhands/harness/src/mcp/_template.ts +47 -0
- package/.allhands/harness/src/mcp/filesystem.ts +33 -0
- package/.allhands/harness/src/mcp/index.ts +69 -0
- package/.allhands/harness/src/mcp/playwright.ts +34 -0
- package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
- package/.allhands/harness/src/schemas/docs.schema.json +44 -0
- package/.allhands/harness/src/schemas/settings.schema.json +214 -0
- package/.allhands/harness/src/tui/actions.ts +227 -0
- package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
- package/.allhands/harness/src/tui/index.ts +1574 -0
- package/.allhands/harness/src/tui/modal.ts +232 -0
- package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
- package/.allhands/harness/src/tui/status-pane.ts +434 -0
- package/.allhands/harness/tsconfig.json +22 -0
- package/.allhands/harness/vitest.config.ts +13 -0
- package/.allhands/pillars.md +33 -0
- package/.allhands/principles.md +88 -0
- package/.allhands/schemas/alignment.yaml +51 -0
- package/.allhands/schemas/documentation.yaml +10 -0
- package/.allhands/schemas/prompt.yaml +92 -0
- package/.allhands/schemas/skill.yaml +34 -0
- package/.allhands/schemas/solution.yaml +131 -0
- package/.allhands/schemas/spec.yaml +67 -0
- package/.allhands/schemas/validation-suite.yaml +49 -0
- package/.allhands/schemas/workflow.yaml +51 -0
- package/.allhands/settings.json +57 -0
- package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
- package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
- package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
- package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
- package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
- package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
- package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
- package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
- package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
- package/.allhands/validation/browser-automation.md +109 -0
- package/.allhands/validation/xcode-automation.md +195 -0
- package/.allhands/workflows/documentation.md +86 -0
- package/.allhands/workflows/investigation.md +81 -0
- package/.allhands/workflows/milestone.md +91 -0
- package/.allhands/workflows/optimization.md +85 -0
- package/.allhands/workflows/refactor.md +99 -0
- package/.allhands/workflows/triage.md +81 -0
- package/.claude/README.md +1 -0
- package/.claude/agents/explorer.md +10 -0
- package/.claude/agents/researcher.md +11 -0
- package/.claude/agents/task-runner.md +8 -0
- package/.claude/settings.json +231 -0
- package/.env.ai.example +7 -0
- package/.github/workflows/npm-publish.yml +69 -0
- package/.internal.json +45 -0
- package/.tldr/config.json +11 -0
- package/.tldrignore +90 -0
- package/CLAUDE.md +6 -0
- package/README.md +98 -0
- package/bin/sync-cli.js +7552 -0
- package/concerns.md +7 -0
- package/docs/README.md +41 -0
- package/docs/agents/README.md +24 -0
- package/docs/agents/agent-configuration-system.md +86 -0
- package/docs/agents/execution-agents.md +50 -0
- package/docs/agents/knowledge-agents.md +61 -0
- package/docs/agents/orchestration-agent.md +57 -0
- package/docs/agents/planning-agents.md +84 -0
- package/docs/agents/quality-review-agents.md +67 -0
- package/docs/agents/workflow-agent-orchestration.md +69 -0
- package/docs/flows/README.md +44 -0
- package/docs/flows/compounding.md +126 -0
- package/docs/flows/coordination.md +72 -0
- package/docs/flows/core-harness-integration.md +63 -0
- package/docs/flows/documentation-orchestration.md +98 -0
- package/docs/flows/e2e-test-plan-building.md +83 -0
- package/docs/flows/emergent-refinement.md +104 -0
- package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
- package/docs/flows/judge-reviewing.md +112 -0
- package/docs/flows/plan-deepening-and-research.md +107 -0
- package/docs/flows/plan-review-jury.md +114 -0
- package/docs/flows/pr-reviewing.md +54 -0
- package/docs/flows/prompt-task-execution.md +119 -0
- package/docs/flows/spec-planning.md +162 -0
- package/docs/flows/type-specific-scoping-flows.md +49 -0
- package/docs/flows/validation-and-skills-integration.md +145 -0
- package/docs/flows/wip/wip-flows.md +102 -0
- package/docs/harness/README.md +23 -0
- package/docs/harness/agent-profiles.md +84 -0
- package/docs/harness/cli/README.md +24 -0
- package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
- package/docs/harness/cli/docs-command.md +87 -0
- package/docs/harness/cli/knowledge-command.md +91 -0
- package/docs/harness/cli/minor-cli-commands.md +65 -0
- package/docs/harness/cli/oracle-command.md +113 -0
- package/docs/harness/cli/planning-command.md +95 -0
- package/docs/harness/cli/schema-and-validation-commands.md +154 -0
- package/docs/harness/cli/search-commands.md +97 -0
- package/docs/harness/cli/spawn-command.md +136 -0
- package/docs/harness/cli/specs-command.md +102 -0
- package/docs/harness/cli/tools-command.md +122 -0
- package/docs/harness/cli/trace-command.md +122 -0
- package/docs/harness/cli-daemon.md +92 -0
- package/docs/harness/event-loop.md +184 -0
- package/docs/harness/hooks/README.md +15 -0
- package/docs/harness/hooks/context-hooks.md +96 -0
- package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
- package/docs/harness/hooks/validation-hooks.md +97 -0
- package/docs/harness/test-harness.md +149 -0
- package/docs/harness/tui.md +176 -0
- package/docs/memories.md +20 -0
- package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
- package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
- package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
- package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
- package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
- package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
- package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
- package/docs/sync-cli/README.md +19 -0
- package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
- package/docs/sync-cli/commands/README.md +11 -0
- package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
- package/docs/sync-cli/commands/push-command.md +84 -0
- package/docs/sync-cli/commands/sync-command.md +71 -0
- package/docs/sync-cli/systems/README.md +14 -0
- package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
- package/docs/sync-cli/systems/interactive-ui.md +43 -0
- package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
- package/docs/sync-cli/systems/path-resolution.md +42 -0
- package/package.json +46 -0
- package/scripts/install-shim.sh +40 -0
- package/scripts/pre-pack.sh +25 -0
- package/specs/harness-maintenance-skill.spec.md +138 -0
- package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
- package/specs/sync-init-flag.spec.md +117 -0
- package/specs/unified-workflow-orchestration.spec.md +250 -0
- package/specs/validation-tooling-practice.spec.md +98 -0
- package/specs/workflow-domain-configuration.spec.md +265 -0
- package/src/commands/pull-manifest.ts +31 -0
- package/src/commands/push.ts +344 -0
- package/src/commands/sync.ts +289 -0
- package/src/lib/constants.ts +10 -0
- package/src/lib/dotfiles.ts +36 -0
- package/src/lib/fs-utils.ts +18 -0
- package/src/lib/gh.ts +40 -0
- package/src/lib/git.ts +63 -0
- package/src/lib/gitignore.ts +167 -0
- package/src/lib/manifest.ts +121 -0
- package/src/lib/marker-sync.ts +39 -0
- package/src/lib/paths.ts +38 -0
- package/src/lib/target-lines.ts +66 -0
- package/src/lib/ui.ts +78 -0
- package/src/sync-cli.ts +120 -0
- package/target-lines.json +23 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oracle - Harness-Specific AI Tasks
|
|
3
|
+
*
|
|
4
|
+
* High-level AI functions specific to the All Hands harness.
|
|
5
|
+
* These are INTERNAL functions - not exposed to agents via CLI.
|
|
6
|
+
*
|
|
7
|
+
* Uses llm.ts for the underlying provider integration.
|
|
8
|
+
*
|
|
9
|
+
* Functions:
|
|
10
|
+
* - generatePRDescription() - Generate PR content from spec + alignment
|
|
11
|
+
* - analyzeConversation() - Analyze agent conversation for compaction
|
|
12
|
+
* - recommendAction() - Recommend continue vs scratch based on analysis
|
|
13
|
+
* - buildPR() - Create PR via gh CLI with generated description
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawnSync } from 'child_process';
|
|
17
|
+
import { existsSync, readFileSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { ask, getCompactionProvider } from './llm.js';
|
|
21
|
+
import {
|
|
22
|
+
readAlignment,
|
|
23
|
+
readAlignmentFrontmatter,
|
|
24
|
+
readStatus,
|
|
25
|
+
updatePRStatus,
|
|
26
|
+
getPlanningPaths,
|
|
27
|
+
getGitRoot,
|
|
28
|
+
sanitizeBranchForDir,
|
|
29
|
+
getCurrentBranch,
|
|
30
|
+
} from './planning.js';
|
|
31
|
+
import { getBaseBranch, gitExec, validateGitRef, syncWithOriginMain } from './git.js';
|
|
32
|
+
import { logEvent } from './trace-store.js';
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Zod Schemas for LLM Response Validation
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
const PRContentSchema = z.object({
|
|
39
|
+
title: z.string(),
|
|
40
|
+
body: z.string(),
|
|
41
|
+
reviewSteps: z.string(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Use coerce to handle LLMs returning strings instead of proper types
|
|
45
|
+
// e.g., "true" -> true, "65" -> 65
|
|
46
|
+
const ConversationAnalysisSchema = z.object({
|
|
47
|
+
wasGettingClose: z.coerce.boolean(),
|
|
48
|
+
progressPercentage: z.coerce.number().min(0).max(100),
|
|
49
|
+
keyLearnings: z.array(z.string()),
|
|
50
|
+
blockers: z.array(z.string()),
|
|
51
|
+
partialWork: z.array(z.string()),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const ActionRecommendationSchema = z.object({
|
|
55
|
+
action: z.enum(['scratch', 'continue']),
|
|
56
|
+
reasoning: z.string(),
|
|
57
|
+
preserveFiles: z.array(z.string()),
|
|
58
|
+
discardFiles: z.array(z.string()),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extract JSON from LLM response text.
|
|
63
|
+
* Handles responses wrapped in markdown code blocks or bare JSON.
|
|
64
|
+
*/
|
|
65
|
+
function extractJSON(text: string): string | null {
|
|
66
|
+
// Try markdown code block first (```json ... ``` or ``` ... ```)
|
|
67
|
+
const codeBlockMatch = text.match(/```(?:json)?\n?([\s\S]*?)\n?```/);
|
|
68
|
+
if (codeBlockMatch) {
|
|
69
|
+
return codeBlockMatch[1].trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fall back to finding first complete JSON object
|
|
73
|
+
// Find the first { and match to its closing }
|
|
74
|
+
const startIdx = text.indexOf('{');
|
|
75
|
+
if (startIdx === -1) return null;
|
|
76
|
+
|
|
77
|
+
let depth = 0;
|
|
78
|
+
let inString = false;
|
|
79
|
+
let escape = false;
|
|
80
|
+
|
|
81
|
+
for (let i = startIdx; i < text.length; i++) {
|
|
82
|
+
const char = text[i];
|
|
83
|
+
|
|
84
|
+
if (escape) {
|
|
85
|
+
escape = false;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (char === '\\' && inString) {
|
|
90
|
+
escape = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (char === '"' && !escape) {
|
|
95
|
+
inString = !inString;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!inString) {
|
|
100
|
+
if (char === '{') depth++;
|
|
101
|
+
if (char === '}') {
|
|
102
|
+
depth--;
|
|
103
|
+
if (depth === 0) {
|
|
104
|
+
return text.slice(startIdx, i + 1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Types
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
export interface PRContent {
|
|
118
|
+
title: string;
|
|
119
|
+
body: string;
|
|
120
|
+
reviewSteps: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ConversationAnalysis {
|
|
124
|
+
wasGettingClose: boolean;
|
|
125
|
+
progressPercentage: number;
|
|
126
|
+
keyLearnings: string[];
|
|
127
|
+
blockers: string[];
|
|
128
|
+
partialWork: string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ActionRecommendation {
|
|
132
|
+
action: 'scratch' | 'continue';
|
|
133
|
+
reasoning: string;
|
|
134
|
+
preserveFiles: string[];
|
|
135
|
+
discardFiles: string[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface BuildPRResult {
|
|
139
|
+
success: boolean;
|
|
140
|
+
prUrl?: string;
|
|
141
|
+
prNumber?: number;
|
|
142
|
+
title: string;
|
|
143
|
+
body: string;
|
|
144
|
+
reviewSteps?: string;
|
|
145
|
+
existingPR?: boolean; // True if PR already existed and was reused
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// PR Generation (Internal)
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get git diff from base branch to current branch
|
|
154
|
+
*/
|
|
155
|
+
function getGitDiffFromBase(cwd?: string, maxLines: number = 300): string {
|
|
156
|
+
const workingDir = cwd || process.cwd();
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// Use the configured base branch
|
|
160
|
+
const baseBranch = getBaseBranch();
|
|
161
|
+
validateGitRef(baseBranch, 'baseBranch');
|
|
162
|
+
|
|
163
|
+
// Get diff stat summary
|
|
164
|
+
const diffStatResult = gitExec(['diff', `${baseBranch}...HEAD`, '--stat'], workingDir);
|
|
165
|
+
const diffStat = diffStatResult.success ? diffStatResult.stdout : '';
|
|
166
|
+
|
|
167
|
+
// Get actual diff (truncated)
|
|
168
|
+
const diffResult = gitExec(['diff', `${baseBranch}...HEAD`], workingDir);
|
|
169
|
+
const diff = diffResult.stdout;
|
|
170
|
+
|
|
171
|
+
const lines = diff.split('\n');
|
|
172
|
+
const truncatedDiff = lines.length > maxLines
|
|
173
|
+
? lines.slice(0, maxLines).join('\n') + '\n... (truncated)'
|
|
174
|
+
: diff;
|
|
175
|
+
|
|
176
|
+
return `### Summary\n${diffStat}\n\n### Changes\n${truncatedDiff}`;
|
|
177
|
+
} catch {
|
|
178
|
+
return 'Unable to get git diff';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Generate a PR description from prompts and alignment doc
|
|
184
|
+
*
|
|
185
|
+
* INTERNAL ONLY - Not exposed via CLI to agents.
|
|
186
|
+
* Used by TUI for create-pr functionality.
|
|
187
|
+
* Uses Gemini provider for generation.
|
|
188
|
+
*/
|
|
189
|
+
export async function generatePRDescription(
|
|
190
|
+
alignmentContent: string,
|
|
191
|
+
specName: string,
|
|
192
|
+
cwd?: string,
|
|
193
|
+
specContent?: string
|
|
194
|
+
): Promise<PRContent> {
|
|
195
|
+
const gitDiff = getGitDiffFromBase(cwd);
|
|
196
|
+
|
|
197
|
+
// Parse changed files from git diff for review steps grouping
|
|
198
|
+
const changedFiles = parseChangedFilesFromDiff(gitDiff);
|
|
199
|
+
|
|
200
|
+
const prompt = `Generate a pull request title and description.
|
|
201
|
+
|
|
202
|
+
## Original Requirements (Spec File):
|
|
203
|
+
${specContent || 'Not provided'}
|
|
204
|
+
|
|
205
|
+
## Implementation Summary (Alignment Document):
|
|
206
|
+
${alignmentContent}
|
|
207
|
+
|
|
208
|
+
## Changed Files:
|
|
209
|
+
${changedFiles.join('\n')}
|
|
210
|
+
|
|
211
|
+
## Instructions:
|
|
212
|
+
Write a standard, concise PR description like you would see on any professional open source project.
|
|
213
|
+
- PR title: Clear, under 72 chars, describes what was implemented
|
|
214
|
+
- PR body: Brief summary of what was built and why, followed by a test plan
|
|
215
|
+
- Do NOT reference individual prompts, tasks, or implementation steps
|
|
216
|
+
- Focus on the end result and value delivered
|
|
217
|
+
- Keep it concise - this is a PR description, not documentation
|
|
218
|
+
|
|
219
|
+
## Review Steps Requirements:
|
|
220
|
+
Group the changed files into logical review buckets based on the alignment document and file relationships.
|
|
221
|
+
- Group related files together (e.g., API + its tests, component + styles)
|
|
222
|
+
- Order by review priority: Core logic → API/interfaces → Utilities → Tests → Config/docs
|
|
223
|
+
- For each bucket, briefly note what to look for
|
|
224
|
+
- Keep it scannable - bullet points, not paragraphs
|
|
225
|
+
|
|
226
|
+
## Response Format (JSON only):
|
|
227
|
+
{
|
|
228
|
+
"title": "Short PR title",
|
|
229
|
+
"body": "## Summary\\n\\nBrief description.\\n\\n## Test Plan\\n\\n- How to test",
|
|
230
|
+
"reviewSteps": "## Review Steps\\n\\n### 1. Core Logic\\n- file1.ts\\n- file2.ts\\n\\nLook for: X, Y\\n\\n### 2. Tests\\n..."
|
|
231
|
+
}`;
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const result = await ask(prompt, {
|
|
235
|
+
provider: 'gemini',
|
|
236
|
+
context: 'You must respond with valid JSON only. No markdown code blocks.',
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const jsonStr = extractJSON(result.text);
|
|
240
|
+
if (!jsonStr) {
|
|
241
|
+
throw new Error('No JSON found in response');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const parsed = PRContentSchema.parse(JSON.parse(jsonStr));
|
|
245
|
+
return parsed;
|
|
246
|
+
} catch {
|
|
247
|
+
// Fallback: Extract a summary from alignment doc instead of listing prompts
|
|
248
|
+
const alignmentSummary = extractAlignmentSummary(alignmentContent);
|
|
249
|
+
return {
|
|
250
|
+
title: `${specName}`,
|
|
251
|
+
body: `## Summary\n\n${alignmentSummary}\n\n## Test Plan\n\n- Run the test suite\n- Manual verification of core functionality`,
|
|
252
|
+
reviewSteps: generateFallbackReviewSteps(changedFiles),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Parse changed file paths from git diff output
|
|
259
|
+
*/
|
|
260
|
+
function parseChangedFilesFromDiff(gitDiff: string): string[] {
|
|
261
|
+
const files: string[] = [];
|
|
262
|
+
const lines = gitDiff.split('\n');
|
|
263
|
+
for (const line of lines) {
|
|
264
|
+
// Match "diff --git a/path b/path" format
|
|
265
|
+
const match = line.match(/^diff --git a\/(.+) b\//);
|
|
266
|
+
if (match) {
|
|
267
|
+
files.push(match[1]);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return files;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Extract a brief summary from the alignment document
|
|
275
|
+
*/
|
|
276
|
+
function extractAlignmentSummary(alignmentContent: string): string {
|
|
277
|
+
if (!alignmentContent) {
|
|
278
|
+
return 'Implementation complete.';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Try to find an overview or summary section
|
|
282
|
+
const overviewMatch = alignmentContent.match(/## Overview\s*\n([\s\S]*?)(?=\n##|$)/i);
|
|
283
|
+
if (overviewMatch) {
|
|
284
|
+
const overview = overviewMatch[1].trim();
|
|
285
|
+
// Take first paragraph or first 500 chars
|
|
286
|
+
const firstParagraph = overview.split('\n\n')[0];
|
|
287
|
+
return firstParagraph.slice(0, 500);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Fallback: take content after frontmatter, first paragraph
|
|
291
|
+
const withoutFrontmatter = alignmentContent.replace(/^---[\s\S]*?---\n/, '');
|
|
292
|
+
const firstParagraph = withoutFrontmatter.trim().split('\n\n')[0];
|
|
293
|
+
return firstParagraph.slice(0, 500) || 'Implementation complete.';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Generate fallback review steps grouped by file type
|
|
298
|
+
*/
|
|
299
|
+
function generateFallbackReviewSteps(files: string[]): string {
|
|
300
|
+
const groups: Record<string, string[]> = {
|
|
301
|
+
'Core Logic': [],
|
|
302
|
+
'API/Routes': [],
|
|
303
|
+
'Components': [],
|
|
304
|
+
'Tests': [],
|
|
305
|
+
'Configuration': [],
|
|
306
|
+
'Other': [],
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
for (const file of files) {
|
|
310
|
+
if (file.includes('.test.') || file.includes('.spec.') || file.includes('__tests__')) {
|
|
311
|
+
groups['Tests'].push(file);
|
|
312
|
+
} else if (file.includes('/api/') || file.includes('/routes/') || file.includes('router')) {
|
|
313
|
+
groups['API/Routes'].push(file);
|
|
314
|
+
} else if (file.includes('/components/') || file.includes('.tsx')) {
|
|
315
|
+
groups['Components'].push(file);
|
|
316
|
+
} else if (file.match(/\.(json|yaml|yml|config\.|rc\.)/) || file.includes('config')) {
|
|
317
|
+
groups['Configuration'].push(file);
|
|
318
|
+
} else if (file.match(/\.(ts|js|py|go|rs)$/)) {
|
|
319
|
+
groups['Core Logic'].push(file);
|
|
320
|
+
} else {
|
|
321
|
+
groups['Other'].push(file);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let steps = '## Review Steps\n\n';
|
|
326
|
+
let stepNum = 1;
|
|
327
|
+
|
|
328
|
+
for (const [groupName, groupFiles] of Object.entries(groups)) {
|
|
329
|
+
if (groupFiles.length > 0) {
|
|
330
|
+
steps += `### ${stepNum}. ${groupName}\n`;
|
|
331
|
+
for (const f of groupFiles) {
|
|
332
|
+
steps += `- ${f}\n`;
|
|
333
|
+
}
|
|
334
|
+
steps += '\n';
|
|
335
|
+
stepNum++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return steps || '## Review Steps\n\nReview all changed files.';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// Conversation Analysis (Internal)
|
|
344
|
+
// ============================================================================
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Analyze an agent conversation for compaction
|
|
348
|
+
*
|
|
349
|
+
* INTERNAL ONLY - Called by compaction handler after agent session.
|
|
350
|
+
* Examines conversation logs to understand progress and extract learnings.
|
|
351
|
+
*/
|
|
352
|
+
export async function analyzeConversation(
|
|
353
|
+
logs: string,
|
|
354
|
+
promptContent: string,
|
|
355
|
+
alignmentContent: string,
|
|
356
|
+
gitDiff: string
|
|
357
|
+
): Promise<ConversationAnalysis> {
|
|
358
|
+
const prompt = `Analyze this agent conversation and extract useful information for the next attempt.
|
|
359
|
+
|
|
360
|
+
## Task Prompt:
|
|
361
|
+
${promptContent}
|
|
362
|
+
|
|
363
|
+
## Alignment Document:
|
|
364
|
+
${alignmentContent}
|
|
365
|
+
|
|
366
|
+
## Git Diff Summary:
|
|
367
|
+
${gitDiff}
|
|
368
|
+
|
|
369
|
+
## Conversation Logs:
|
|
370
|
+
${logs}
|
|
371
|
+
|
|
372
|
+
## Analysis Required:
|
|
373
|
+
|
|
374
|
+
1. Was the agent making meaningful progress toward the goal? (yes/no)
|
|
375
|
+
2. Estimate the percentage of the task that was completed (0-100)
|
|
376
|
+
3. What key learnings would help a fresh agent? (specific patterns, APIs, approaches that work)
|
|
377
|
+
4. What blocked the agent from completion? (missing deps, wrong approach, unclear requirements)
|
|
378
|
+
5. What partial work is valuable and should be preserved? (list specific files or code)
|
|
379
|
+
|
|
380
|
+
## Response Format (JSON only):
|
|
381
|
+
{
|
|
382
|
+
"wasGettingClose": true,
|
|
383
|
+
"progressPercentage": 65,
|
|
384
|
+
"keyLearnings": ["Pattern X works well for...", "The API requires..."],
|
|
385
|
+
"blockers": ["Missing dependency Z", "Unclear requirement about..."],
|
|
386
|
+
"partialWork": ["src/lib/foo.ts", "src/commands/bar.ts"]
|
|
387
|
+
}`;
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const result = await ask(prompt, {
|
|
391
|
+
context: 'You must respond with valid JSON only. No markdown code blocks. Be concise.',
|
|
392
|
+
provider: getCompactionProvider(),
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const jsonStr = extractJSON(result.text);
|
|
396
|
+
if (!jsonStr) {
|
|
397
|
+
throw new Error('No JSON found in response');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return ConversationAnalysisSchema.parse(JSON.parse(jsonStr));
|
|
401
|
+
} catch (error) {
|
|
402
|
+
// Log the actual error for debugging
|
|
403
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
404
|
+
logEvent('harness.error', {
|
|
405
|
+
source: 'oracle.analyzeConversation',
|
|
406
|
+
error: errorMsg,
|
|
407
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Conservative fallback - assume some progress was made
|
|
411
|
+
return {
|
|
412
|
+
wasGettingClose: true,
|
|
413
|
+
progressPercentage: 50,
|
|
414
|
+
keyLearnings: [],
|
|
415
|
+
blockers: [`Analysis failed: ${errorMsg}`],
|
|
416
|
+
partialWork: [],
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Recommend whether to continue or scratch based on analysis
|
|
423
|
+
*
|
|
424
|
+
* INTERNAL ONLY - Called by compaction handler.
|
|
425
|
+
* Uses analysis and git diff to decide if code should be kept.
|
|
426
|
+
*/
|
|
427
|
+
export async function recommendAction(
|
|
428
|
+
analysis: ConversationAnalysis,
|
|
429
|
+
attemptNumber: number,
|
|
430
|
+
gitDiff: string
|
|
431
|
+
): Promise<ActionRecommendation> {
|
|
432
|
+
const prompt = `Based on this analysis, recommend whether to continue with the existing code or scratch and start fresh.
|
|
433
|
+
|
|
434
|
+
## Analysis:
|
|
435
|
+
- Progress: ${analysis.progressPercentage}%
|
|
436
|
+
- Was getting close: ${analysis.wasGettingClose}
|
|
437
|
+
- Key learnings: ${analysis.keyLearnings.join('; ')}
|
|
438
|
+
- Blockers: ${analysis.blockers.join('; ')}
|
|
439
|
+
- Partial work: ${analysis.partialWork.join(', ')}
|
|
440
|
+
- Attempt number: ${attemptNumber}
|
|
441
|
+
|
|
442
|
+
## Git Diff:
|
|
443
|
+
${gitDiff}
|
|
444
|
+
|
|
445
|
+
## Considerations:
|
|
446
|
+
1. Code stability - Does it compile/run? Is there test coverage?
|
|
447
|
+
2. Boilerplate vs logic - Is this mostly setup code or actual implementation?
|
|
448
|
+
3. Complexity of remaining work - How much is left to do?
|
|
449
|
+
4. Risk of starting fresh - Would we lose valuable progress?
|
|
450
|
+
|
|
451
|
+
## Decision Guidelines:
|
|
452
|
+
- CONTINUE if: >40% progress, code compiles, meaningful logic exists
|
|
453
|
+
- SCRATCH if: <20% progress, code broken, mostly boilerplate, wrong approach
|
|
454
|
+
|
|
455
|
+
## Response Format (JSON only):
|
|
456
|
+
{
|
|
457
|
+
"action": "continue",
|
|
458
|
+
"reasoning": "Brief explanation of the decision",
|
|
459
|
+
"preserveFiles": ["files to keep if scratching"],
|
|
460
|
+
"discardFiles": ["files that should be removed"]
|
|
461
|
+
}`;
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const result = await ask(prompt, {
|
|
465
|
+
context: 'You must respond with valid JSON only. No markdown code blocks.',
|
|
466
|
+
provider: getCompactionProvider(),
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const jsonStr = extractJSON(result.text);
|
|
470
|
+
if (!jsonStr) {
|
|
471
|
+
throw new Error('No JSON found in response');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Zod validates action is 'scratch' | 'continue'
|
|
475
|
+
return ActionRecommendationSchema.parse(JSON.parse(jsonStr));
|
|
476
|
+
} catch (error) {
|
|
477
|
+
// Log the actual error for debugging
|
|
478
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
479
|
+
logEvent('harness.error', {
|
|
480
|
+
source: 'oracle.recommendAction',
|
|
481
|
+
error: errorMsg,
|
|
482
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Default to continue to avoid losing code
|
|
486
|
+
return {
|
|
487
|
+
action: 'continue',
|
|
488
|
+
reasoning: `Defaulting to continue: ${errorMsg}`,
|
|
489
|
+
preserveFiles: [],
|
|
490
|
+
discardFiles: [],
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ============================================================================
|
|
496
|
+
// PR Building (Internal)
|
|
497
|
+
// ============================================================================
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Build and create a PR with generated description
|
|
501
|
+
*
|
|
502
|
+
* INTERNAL ONLY - Called by pr-build command.
|
|
503
|
+
* Uses generatePRDescription and gh CLI to create PR.
|
|
504
|
+
*
|
|
505
|
+
* @param spec - The spec name to build PR for
|
|
506
|
+
* @param cwd - Working directory
|
|
507
|
+
* @param dryRun - If true, don't actually create the PR
|
|
508
|
+
*/
|
|
509
|
+
export async function buildPR(
|
|
510
|
+
spec: string,
|
|
511
|
+
cwd?: string,
|
|
512
|
+
dryRun: boolean = false
|
|
513
|
+
): Promise<BuildPRResult> {
|
|
514
|
+
const workingDir = cwd || process.cwd();
|
|
515
|
+
|
|
516
|
+
// Check if PR already exists in status.yaml
|
|
517
|
+
const branch = getCurrentBranch(workingDir);
|
|
518
|
+
const planningKey = sanitizeBranchForDir(branch);
|
|
519
|
+
const status = readStatus(planningKey, workingDir);
|
|
520
|
+
const existingPR = status?.pr;
|
|
521
|
+
|
|
522
|
+
// Load alignment and spec content
|
|
523
|
+
const alignmentContent = readAlignment(spec, workingDir);
|
|
524
|
+
|
|
525
|
+
if (!alignmentContent) {
|
|
526
|
+
return {
|
|
527
|
+
success: false,
|
|
528
|
+
title: '',
|
|
529
|
+
body: 'No alignment document found for this spec',
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Load spec content from the spec file path in alignment frontmatter
|
|
534
|
+
const alignmentFrontmatter = readAlignmentFrontmatter(spec, workingDir);
|
|
535
|
+
let specContent: string | undefined;
|
|
536
|
+
if (alignmentFrontmatter?.spec) {
|
|
537
|
+
try {
|
|
538
|
+
const specPath = join(getGitRoot(workingDir), alignmentFrontmatter.spec);
|
|
539
|
+
specContent = readFileSync(specPath, 'utf-8');
|
|
540
|
+
} catch {
|
|
541
|
+
// Non-fatal: spec file might not exist
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Generate PR content
|
|
546
|
+
const prContent = await generatePRDescription(
|
|
547
|
+
alignmentContent,
|
|
548
|
+
spec,
|
|
549
|
+
workingDir,
|
|
550
|
+
specContent
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
if (dryRun) {
|
|
554
|
+
return {
|
|
555
|
+
success: true,
|
|
556
|
+
title: prContent.title,
|
|
557
|
+
body: prContent.body,
|
|
558
|
+
reviewSteps: prContent.reviewSteps,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// If PR already exists, UPDATE instead of creating
|
|
563
|
+
if (existingPR?.url && existingPR?.number) {
|
|
564
|
+
try {
|
|
565
|
+
// Sync with origin/main before push
|
|
566
|
+
const syncResult = syncWithOriginMain(workingDir);
|
|
567
|
+
if (!syncResult.success) {
|
|
568
|
+
const failureReason = syncResult.conflicts.length > 0
|
|
569
|
+
? `Merge conflicts with main must be resolved before updating PR:\n${syncResult.conflicts.join('\n')}`
|
|
570
|
+
: 'Failed to sync with main. This can be caused by uncommitted changes or network issues. Please resolve and try again.';
|
|
571
|
+
return {
|
|
572
|
+
success: false,
|
|
573
|
+
title: prContent.title,
|
|
574
|
+
body: failureReason,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Push any new changes first
|
|
579
|
+
gitExec(['push', '-u', 'origin', 'HEAD'], workingDir);
|
|
580
|
+
|
|
581
|
+
// Update PR description
|
|
582
|
+
updatePRDescription(existingPR.number, prContent.body, workingDir);
|
|
583
|
+
|
|
584
|
+
// Update comments (review steps and E2E test plan)
|
|
585
|
+
updatePRComments(existingPR.number, prContent.reviewSteps, spec, workingDir);
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
success: true,
|
|
589
|
+
prUrl: existingPR.url,
|
|
590
|
+
prNumber: existingPR.number,
|
|
591
|
+
title: prContent.title,
|
|
592
|
+
body: prContent.body,
|
|
593
|
+
reviewSteps: prContent.reviewSteps,
|
|
594
|
+
existingPR: true,
|
|
595
|
+
};
|
|
596
|
+
} catch (error) {
|
|
597
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
598
|
+
return {
|
|
599
|
+
success: false,
|
|
600
|
+
title: prContent.title,
|
|
601
|
+
body: `PR update failed: ${errorMsg}`,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// No existing PR - create new one
|
|
607
|
+
try {
|
|
608
|
+
// Sync with origin/main before push
|
|
609
|
+
const syncResult = syncWithOriginMain(workingDir);
|
|
610
|
+
if (!syncResult.success) {
|
|
611
|
+
const failureReason = syncResult.conflicts.length > 0
|
|
612
|
+
? `Merge conflicts with main must be resolved before creating PR:\n${syncResult.conflicts.join('\n')}`
|
|
613
|
+
: 'Failed to sync with main. This can be caused by uncommitted changes or network issues. Please resolve and try again.';
|
|
614
|
+
return {
|
|
615
|
+
success: false,
|
|
616
|
+
title: prContent.title,
|
|
617
|
+
body: failureReason,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Push branch to remote before creating PR
|
|
622
|
+
const pushResult = gitExec(['push', '-u', 'origin', 'HEAD'], workingDir);
|
|
623
|
+
if (!pushResult.success && !pushResult.stderr.includes('Everything up-to-date')) {
|
|
624
|
+
return {
|
|
625
|
+
success: false,
|
|
626
|
+
title: prContent.title,
|
|
627
|
+
body: `Failed to push branch: ${pushResult.stderr}`,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Create PR via gh CLI with argument arrays — no shell interpolation
|
|
632
|
+
const prCreateResult = spawnSync('gh', [
|
|
633
|
+
'pr', 'create',
|
|
634
|
+
'--title', prContent.title,
|
|
635
|
+
'--body-file', '-',
|
|
636
|
+
], {
|
|
637
|
+
encoding: 'utf-8',
|
|
638
|
+
cwd: workingDir,
|
|
639
|
+
input: prContent.body,
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
if (prCreateResult.status !== 0) {
|
|
643
|
+
const errOutput = prCreateResult.stderr || '';
|
|
644
|
+
// Check if PR already exists (race condition) - update instead
|
|
645
|
+
if (errOutput.includes('already exists') || errOutput.includes('pull request already')) {
|
|
646
|
+
return handleExistingPRRace(prContent, spec, workingDir);
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
success: false,
|
|
650
|
+
title: prContent.title,
|
|
651
|
+
body: `PR creation failed: ${errOutput}`,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const output = prCreateResult.stdout || '';
|
|
656
|
+
|
|
657
|
+
// Parse PR URL from output
|
|
658
|
+
const urlMatch = output.match(/https:\/\/github\.com\/[^\s]+\/pull\/(\d+)/);
|
|
659
|
+
const prUrl = urlMatch ? urlMatch[0] : undefined;
|
|
660
|
+
const prNumber = urlMatch ? parseInt(urlMatch[1], 10) : undefined;
|
|
661
|
+
|
|
662
|
+
// Update status.yaml with PR info and post comments
|
|
663
|
+
if (prUrl && prNumber) {
|
|
664
|
+
updatePRStatus(prUrl, prNumber, spec, workingDir);
|
|
665
|
+
postPRComments(prNumber, prContent.reviewSteps, spec, workingDir);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
success: true,
|
|
670
|
+
prUrl,
|
|
671
|
+
prNumber,
|
|
672
|
+
title: prContent.title,
|
|
673
|
+
body: prContent.body,
|
|
674
|
+
reviewSteps: prContent.reviewSteps,
|
|
675
|
+
};
|
|
676
|
+
} catch (error) {
|
|
677
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
678
|
+
return {
|
|
679
|
+
success: false,
|
|
680
|
+
title: prContent.title,
|
|
681
|
+
body: `PR creation failed: ${errorMsg}`,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Handle race condition where PR already exists during creation.
|
|
688
|
+
* Fetches existing PR info and updates it instead.
|
|
689
|
+
*/
|
|
690
|
+
function handleExistingPRRace(
|
|
691
|
+
prContent: PRContent,
|
|
692
|
+
spec: string,
|
|
693
|
+
workingDir: string
|
|
694
|
+
): BuildPRResult {
|
|
695
|
+
try {
|
|
696
|
+
const prViewResult = spawnSync('gh', ['pr', 'view', '--json', 'url,number'], {
|
|
697
|
+
encoding: 'utf-8',
|
|
698
|
+
cwd: workingDir,
|
|
699
|
+
});
|
|
700
|
+
if (prViewResult.status !== 0) {
|
|
701
|
+
return { success: false, title: prContent.title, body: 'PR already exists but could not retrieve info' };
|
|
702
|
+
}
|
|
703
|
+
const prInfo = JSON.parse(prViewResult.stdout || '{}');
|
|
704
|
+
const raceExistingUrl = prInfo.url;
|
|
705
|
+
const raceExistingNumber = prInfo.number;
|
|
706
|
+
|
|
707
|
+
if (raceExistingUrl && raceExistingNumber) {
|
|
708
|
+
updatePRStatus(raceExistingUrl, raceExistingNumber, spec, workingDir);
|
|
709
|
+
updatePRDescription(raceExistingNumber, prContent.body, workingDir);
|
|
710
|
+
updatePRComments(raceExistingNumber, prContent.reviewSteps, spec, workingDir);
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
success: true,
|
|
714
|
+
prUrl: raceExistingUrl,
|
|
715
|
+
prNumber: raceExistingNumber,
|
|
716
|
+
title: prContent.title,
|
|
717
|
+
body: prContent.body,
|
|
718
|
+
reviewSteps: prContent.reviewSteps,
|
|
719
|
+
existingPR: true,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
} catch {
|
|
723
|
+
// Couldn't get existing PR info
|
|
724
|
+
}
|
|
725
|
+
return { success: false, title: prContent.title, body: 'PR already exists but could not retrieve info' };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Post review steps and E2E test plan comments to a PR
|
|
730
|
+
* Order: Review steps first, then E2E test plan
|
|
731
|
+
*/
|
|
732
|
+
function postPRComments(
|
|
733
|
+
prNumber: number,
|
|
734
|
+
reviewSteps: string | undefined,
|
|
735
|
+
spec: string,
|
|
736
|
+
workingDir: string
|
|
737
|
+
): void {
|
|
738
|
+
// Post review steps FIRST
|
|
739
|
+
if (reviewSteps) {
|
|
740
|
+
const result = spawnSync('gh', ['pr', 'comment', String(prNumber), '--body-file', '-'], {
|
|
741
|
+
encoding: 'utf-8',
|
|
742
|
+
cwd: workingDir,
|
|
743
|
+
input: reviewSteps,
|
|
744
|
+
});
|
|
745
|
+
if (result.status !== 0) {
|
|
746
|
+
console.error('Warning: Could not add review steps comment to PR');
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Post E2E test plan AFTER review steps
|
|
751
|
+
const planningPaths = getPlanningPaths(spec, workingDir);
|
|
752
|
+
const e2eTestPlanPath = join(planningPaths.root, 'e2e-test-plan.md');
|
|
753
|
+
if (existsSync(e2eTestPlanPath)) {
|
|
754
|
+
let e2eTestPlan = readFileSync(e2eTestPlanPath, 'utf-8');
|
|
755
|
+
// Strip YAML frontmatter if present
|
|
756
|
+
e2eTestPlan = e2eTestPlan.replace(/^---\n[\s\S]*?\n---\n*/, '').trim();
|
|
757
|
+
const result = spawnSync('gh', ['pr', 'comment', String(prNumber), '--body-file', '-'], {
|
|
758
|
+
encoding: 'utf-8',
|
|
759
|
+
cwd: workingDir,
|
|
760
|
+
input: `## E2E Test Plan\n\n${e2eTestPlan}`,
|
|
761
|
+
});
|
|
762
|
+
if (result.status !== 0) {
|
|
763
|
+
console.error('Warning: Could not add e2e test plan comment to PR');
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Update PR description via gh CLI
|
|
770
|
+
*/
|
|
771
|
+
function updatePRDescription(
|
|
772
|
+
prNumber: number,
|
|
773
|
+
body: string,
|
|
774
|
+
workingDir: string
|
|
775
|
+
): boolean {
|
|
776
|
+
const result = spawnSync('gh', ['pr', 'edit', String(prNumber), '--body-file', '-'], {
|
|
777
|
+
encoding: 'utf-8',
|
|
778
|
+
cwd: workingDir,
|
|
779
|
+
input: body,
|
|
780
|
+
});
|
|
781
|
+
if (result.status !== 0) {
|
|
782
|
+
console.error('Warning: Could not update PR description');
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
interface PRComment {
|
|
789
|
+
id: number;
|
|
790
|
+
body: string;
|
|
791
|
+
author: { login: string };
|
|
792
|
+
createdAt: string;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Get all issue comments on a PR (these are the top-level comments, not review comments)
|
|
797
|
+
*/
|
|
798
|
+
function getPRComments(prNumber: number, workingDir: string): PRComment[] {
|
|
799
|
+
try {
|
|
800
|
+
// Get repo owner/name
|
|
801
|
+
const repoResult = spawnSync('gh', ['repo', 'view', '--json', 'owner,name'], {
|
|
802
|
+
encoding: 'utf-8',
|
|
803
|
+
cwd: workingDir,
|
|
804
|
+
});
|
|
805
|
+
if (repoResult.status !== 0) return [];
|
|
806
|
+
const { owner, name } = JSON.parse(repoResult.stdout || '{}');
|
|
807
|
+
|
|
808
|
+
// Get issue comments (top-level PR comments)
|
|
809
|
+
const commentsResult = spawnSync('gh', [
|
|
810
|
+
'api', `repos/${owner.login}/${name}/issues/${prNumber}/comments`,
|
|
811
|
+
'--jq', '[.[] | {id: .id, body: .body, author: {login: .user.login}, createdAt: .created_at}]',
|
|
812
|
+
], {
|
|
813
|
+
encoding: 'utf-8',
|
|
814
|
+
cwd: workingDir,
|
|
815
|
+
});
|
|
816
|
+
if (commentsResult.status !== 0) return [];
|
|
817
|
+
return JSON.parse(commentsResult.stdout || '[]');
|
|
818
|
+
} catch {
|
|
819
|
+
return [];
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Update a specific comment on a PR
|
|
825
|
+
*/
|
|
826
|
+
function updatePRComment(
|
|
827
|
+
commentId: number,
|
|
828
|
+
body: string,
|
|
829
|
+
workingDir: string
|
|
830
|
+
): boolean {
|
|
831
|
+
try {
|
|
832
|
+
// Get repo owner/name
|
|
833
|
+
const repoResult = spawnSync('gh', ['repo', 'view', '--json', 'owner,name'], {
|
|
834
|
+
encoding: 'utf-8',
|
|
835
|
+
cwd: workingDir,
|
|
836
|
+
});
|
|
837
|
+
if (repoResult.status !== 0) return false;
|
|
838
|
+
const { owner, name } = JSON.parse(repoResult.stdout || '{}');
|
|
839
|
+
|
|
840
|
+
const patchResult = spawnSync('gh', [
|
|
841
|
+
'api', '--method', 'PATCH',
|
|
842
|
+
`repos/${owner.login}/${name}/issues/comments/${commentId}`,
|
|
843
|
+
'-f', 'body=@-',
|
|
844
|
+
], {
|
|
845
|
+
encoding: 'utf-8',
|
|
846
|
+
cwd: workingDir,
|
|
847
|
+
input: body,
|
|
848
|
+
});
|
|
849
|
+
return patchResult.status === 0;
|
|
850
|
+
} catch {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Update existing PR comments (review steps and E2E test plan)
|
|
857
|
+
* Finds comments by pattern matching and updates them
|
|
858
|
+
*/
|
|
859
|
+
function updatePRComments(
|
|
860
|
+
prNumber: number,
|
|
861
|
+
reviewSteps: string | undefined,
|
|
862
|
+
spec: string,
|
|
863
|
+
workingDir: string
|
|
864
|
+
): void {
|
|
865
|
+
const comments = getPRComments(prNumber, workingDir);
|
|
866
|
+
if (comments.length === 0) {
|
|
867
|
+
// No existing comments, post new ones
|
|
868
|
+
postPRComments(prNumber, reviewSteps, spec, workingDir);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Find and update review steps comment (first comment or one with ## File Walkthrough)
|
|
873
|
+
if (reviewSteps) {
|
|
874
|
+
// Look for comment containing file walkthrough patterns
|
|
875
|
+
const reviewComment = comments.find(c =>
|
|
876
|
+
c.body.includes('## File Walkthrough') ||
|
|
877
|
+
c.body.includes('## Review Steps') ||
|
|
878
|
+
c.body.includes('## Changes Overview')
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
if (reviewComment) {
|
|
882
|
+
updatePRComment(reviewComment.id, reviewSteps, workingDir);
|
|
883
|
+
} else {
|
|
884
|
+
// No matching comment found, post new one
|
|
885
|
+
const result = spawnSync('gh', ['pr', 'comment', String(prNumber), '--body-file', '-'], {
|
|
886
|
+
encoding: 'utf-8',
|
|
887
|
+
cwd: workingDir,
|
|
888
|
+
input: reviewSteps,
|
|
889
|
+
});
|
|
890
|
+
if (result.status !== 0) {
|
|
891
|
+
console.error('Warning: Could not add review steps comment to PR');
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Find and update E2E test plan comment
|
|
897
|
+
const planningPaths = getPlanningPaths(spec, workingDir);
|
|
898
|
+
const e2eTestPlanPath = join(planningPaths.root, 'e2e-test-plan.md');
|
|
899
|
+
if (existsSync(e2eTestPlanPath)) {
|
|
900
|
+
let e2eTestPlan = readFileSync(e2eTestPlanPath, 'utf-8');
|
|
901
|
+
// Strip YAML frontmatter if present
|
|
902
|
+
e2eTestPlan = e2eTestPlan.replace(/^---\n[\s\S]*?\n---\n*/, '').trim();
|
|
903
|
+
const e2eBody = `## E2E Test Plan\n\n${e2eTestPlan}`;
|
|
904
|
+
|
|
905
|
+
// Look for existing E2E comment
|
|
906
|
+
const e2eComment = comments.find(c => c.body.includes('## E2E Test Plan'));
|
|
907
|
+
|
|
908
|
+
if (e2eComment) {
|
|
909
|
+
// Check if content is different (compare without the header)
|
|
910
|
+
const existingContent = e2eComment.body.replace(/^## E2E Test Plan\s*\n*/, '').trim();
|
|
911
|
+
if (existingContent !== e2eTestPlan) {
|
|
912
|
+
updatePRComment(e2eComment.id, e2eBody, workingDir);
|
|
913
|
+
}
|
|
914
|
+
} else {
|
|
915
|
+
// No E2E comment exists, post new one
|
|
916
|
+
const result = spawnSync('gh', ['pr', 'comment', String(prNumber), '--body-file', '-'], {
|
|
917
|
+
encoding: 'utf-8',
|
|
918
|
+
cwd: workingDir,
|
|
919
|
+
input: e2eBody,
|
|
920
|
+
});
|
|
921
|
+
if (result.status !== 0) {
|
|
922
|
+
console.error('Warning: Could not add e2e test plan comment to PR');
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|