headlesscode 1.0.2
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/ATTRIBUTION.md +53 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +107 -0
- package/LICENSE +202 -0
- package/README.md +486 -0
- package/SECURITY.md +211 -0
- package/bin/headlesscode.mjs +83 -0
- package/package.json +63 -0
- package/shared/prompts/review-mode-prompt-short.md +93 -0
- package/shared/prompts/review-mode-prompt.md +281 -0
- package/shared/rules-code/rules.md +22 -0
- package/shared/stacks/cpp/rules.md +30 -0
- package/shared/stacks/fastapi/rules.md +30 -0
- package/shared/stacks/javascript/rules.md +37 -0
- package/shared/stacks/postgresql/rules.md +31 -0
- package/shared/stacks/python/rules.md +35 -0
- package/shared/stacks/react/rules.md +11 -0
- package/shared/stacks/typescript/rules.md +10 -0
- package/src/budget/budget.ts +221 -0
- package/src/budget/concurrency.ts +126 -0
- package/src/budget/cost.ts +309 -0
- package/src/budget/index.ts +8 -0
- package/src/checkpoints/cli.ts +256 -0
- package/src/checkpoints/service.ts +227 -0
- package/src/cli.ts +1535 -0
- package/src/cloud/docker-provider.ts +334 -0
- package/src/cloud/provider.ts +300 -0
- package/src/codeintel/call-graph.ts +78 -0
- package/src/codeintel/find-references.ts +123 -0
- package/src/codeintel/go-to-definition.ts +193 -0
- package/src/codeintel/handlers.ts +190 -0
- package/src/codeintel/import-graph.ts +173 -0
- package/src/codeintel/outline.ts +180 -0
- package/src/codeintel/position.ts +77 -0
- package/src/codeintel/program.ts +350 -0
- package/src/codeintel/rename-symbol.ts +213 -0
- package/src/codeintel/tools.ts +280 -0
- package/src/codemap/build.ts +135 -0
- package/src/codemap/cli.ts +190 -0
- package/src/codemap/extract.ts +339 -0
- package/src/codemap/files.ts +236 -0
- package/src/codemap/fingerprint.ts +65 -0
- package/src/codemap/flows.ts +62 -0
- package/src/codemap/html.ts +451 -0
- package/src/codemap/lock.ts +80 -0
- package/src/codemap/types.ts +101 -0
- package/src/codesearch/airunner-embedder.ts +185 -0
- package/src/codesearch/chunk.ts +339 -0
- package/src/codesearch/cli.ts +223 -0
- package/src/codesearch/embedder.ts +332 -0
- package/src/codesearch/files.ts +280 -0
- package/src/codesearch/index.ts +469 -0
- package/src/codesearch/ollama-embedder.ts +205 -0
- package/src/codesearch/search.ts +141 -0
- package/src/codesearch/types.ts +100 -0
- package/src/config/mode-models.ts +218 -0
- package/src/dashboard/aggregate.ts +364 -0
- package/src/dashboard/chat-thread.ts +141 -0
- package/src/dashboard/checkpoints.ts +124 -0
- package/src/dashboard/cli.ts +193 -0
- package/src/dashboard/codemap.ts +44 -0
- package/src/dashboard/files.ts +121 -0
- package/src/dashboard/page.ts +2803 -0
- package/src/dashboard/self-improvement-metrics.ts +282 -0
- package/src/dashboard/server.ts +1103 -0
- package/src/dashboard/session-launch.ts +310 -0
- package/src/dashboard/timeline.ts +273 -0
- package/src/dashboard/tool-exec.ts +107 -0
- package/src/dashboard/trend-cli.ts +141 -0
- package/src/dashboard/trend.ts +413 -0
- package/src/decision-proxy/cli.ts +261 -0
- package/src/decision-proxy/proxy.ts +569 -0
- package/src/deploy/gate-cli.ts +147 -0
- package/src/deploy/gate.ts +254 -0
- package/src/engine/condense.ts +512 -0
- package/src/engine/events.ts +428 -0
- package/src/engine/handoff.ts +71 -0
- package/src/engine/lazy-tools.ts +160 -0
- package/src/engine/local-explore.ts +653 -0
- package/src/engine/logger.ts +96 -0
- package/src/engine/loop.ts +5517 -0
- package/src/engine/parser.ts +347 -0
- package/src/engine/prompt.ts +860 -0
- package/src/engine/reports.ts +47 -0
- package/src/engine/stacks.ts +448 -0
- package/src/engine/types.ts +291 -0
- package/src/engine/usage.ts +186 -0
- package/src/github/app-auth.ts +161 -0
- package/src/github/cli.ts +448 -0
- package/src/github/installations.ts +133 -0
- package/src/github/pr.ts +321 -0
- package/src/github/provision.ts +118 -0
- package/src/github/push.ts +122 -0
- package/src/index-util.ts +50 -0
- package/src/index.ts +81 -0
- package/src/init/cli.ts +248 -0
- package/src/init/gitignore.ts +74 -0
- package/src/llm/ollama.ts +308 -0
- package/src/llm/openrouter.ts +868 -0
- package/src/llm/preflight.ts +367 -0
- package/src/llm/transcript-capture.ts +84 -0
- package/src/memory/embed.ts +110 -0
- package/src/memory/index.ts +22 -0
- package/src/memory/local.ts +259 -0
- package/src/memory/summarizer.ts +283 -0
- package/src/memory/types.ts +153 -0
- package/src/memory/uwuchat.ts +157 -0
- package/src/migrate/cli.ts +115 -0
- package/src/orchestrator/analyze-cli.ts +104 -0
- package/src/orchestrator/auto-split.ts +206 -0
- package/src/orchestrator/cleanup.ts +1003 -0
- package/src/orchestrator/cli.ts +3571 -0
- package/src/orchestrator/cost-estimate.ts +564 -0
- package/src/orchestrator/cost-history-cli.ts +242 -0
- package/src/orchestrator/cost-history.ts +397 -0
- package/src/orchestrator/git-sync.ts +250 -0
- package/src/orchestrator/index.ts +153 -0
- package/src/orchestrator/log-analysis.ts +0 -0
- package/src/orchestrator/merge-check.ts +108 -0
- package/src/orchestrator/pipeline.ts +411 -0
- package/src/orchestrator/resume.ts +1940 -0
- package/src/orchestrator/reviewer.ts +503 -0
- package/src/orchestrator/split.ts +296 -0
- package/src/orchestrator/state.ts +542 -0
- package/src/orchestrator/status.ts +697 -0
- package/src/orchestrator/verification-gate.ts +134 -0
- package/src/orchestrator/watch.ts +898 -0
- package/src/permissions/commands.ts +1083 -0
- package/src/permissions/config.ts +241 -0
- package/src/permissions/index.ts +12 -0
- package/src/permissions/protected-files.ts +96 -0
- package/src/permissions/store-protection.ts +272 -0
- package/src/project-store.ts +648 -0
- package/src/projects/cli.ts +382 -0
- package/src/qa/qa.ts +487 -0
- package/src/tools/browser/handler.ts +346 -0
- package/src/tools/browser/service.ts +406 -0
- package/src/tools/browser/smoke.ts +78 -0
- package/src/tools/browser/tool.ts +99 -0
- package/src/tools/executor.ts +2575 -0
- package/src/tools/language-detect.ts +183 -0
- package/src/tools/output-summarizer.ts +369 -0
- package/src/tools/run-tests.ts +302 -0
- package/src/tools/set-indentation-tool.ts +49 -0
- package/src/tools/test-selection.ts +160 -0
- package/src/vendor/tests/smoke.ts +103 -0
- package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
- package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
- package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
- package/src/vendor/zoo-code/shim/os-name.ts +18 -0
- package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
- package/src/vendor/zoo-code/shim/vscode.ts +76 -0
- package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
- package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
- package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
- package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
- package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
- package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
- package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
- package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
- package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
- package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
- package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
- package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
- package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
- package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
- package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
- package/src/vendor/zoo-code/src/shared/language.ts +43 -0
- package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
- package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
- package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
- package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
- package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
- package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
- package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
- package/src/vendor/zoo-code/src/utils/object.ts +18 -0
- package/src/vendor/zoo-code/src/utils/path.ts +94 -0
- package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
- package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
- package/src/vendor/zoo-code/types/global-settings.ts +19 -0
- package/src/vendor/zoo-code/types/index.ts +22 -0
- package/src/vendor/zoo-code/types/message.ts +375 -0
- package/src/vendor/zoo-code/types/mode.ts +241 -0
- package/src/vendor/zoo-code/types/todo.ts +19 -0
- package/src/vendor/zoo-code/types/tool-params.ts +116 -0
- package/src/vendor/zoo-code/types/tool.ts +67 -0
- package/src/vendor/zoo-code/types/vscode.ts +84 -0
- package/src/vision/describe.ts +242 -0
- package/src/vision/tool.ts +91 -0
- package/src/watcher/cli.ts +369 -0
- package/src/watcher/github.ts +304 -0
- package/src/watcher/index.ts +59 -0
- package/src/watcher/state.ts +254 -0
- package/src/watcher/watch.ts +562 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage-isolated pipeline sequencing (issue #148).
|
|
3
|
+
*
|
|
4
|
+
* Generalizes the reviewer pattern (src/orchestrator/reviewer.ts) from ONE
|
|
5
|
+
* stage — review — to the pipeline stages that need it: research (produce a
|
|
6
|
+
* finding doc) and filing (turn that doc into a real GitHub issue). Each
|
|
7
|
+
* stage runs as a genuinely FRESH `HeadlessSession` (in-process, own
|
|
8
|
+
* context, own mode, own executor) that takes the PRIOR STAGE'S ARTIFACT as
|
|
9
|
+
* input — never the prior stage's raw conversation — exactly as
|
|
10
|
+
* `runReview`/`parseReviewResult` already do for review and `--plan-first`
|
|
11
|
+
* does for planning.
|
|
12
|
+
*
|
|
13
|
+
* This is deliberately NOT the per-worker subprocess spawn path
|
|
14
|
+
* (`run-worker.sh`): those already give process isolation between worktrees.
|
|
15
|
+
* This layer is for the stages that run IN-PROCESS today as one continuous
|
|
16
|
+
* session (the top-level explore→file→implement→review pipeline), so each
|
|
17
|
+
* one gets the same fresh-context treatment review already has.
|
|
18
|
+
*
|
|
19
|
+
* Design notes:
|
|
20
|
+
* - The research stage binds `requireArtifactPathPattern` /
|
|
21
|
+
* `requireArtifactMinCitations` / `requireArtifactSections` BY DEFAULT —
|
|
22
|
+
* the CLI flags exist (src/cli.ts) but nothing binds them per-mode; a
|
|
23
|
+
* researcher session must not complete without a real, well-cited doc on
|
|
24
|
+
* disk at the known path.
|
|
25
|
+
* - The filing stage's task input is built from the research doc's CONTENT
|
|
26
|
+
* read off disk (not the research session's conversation), and its result
|
|
27
|
+
* is parsed deterministically (mirroring `parseReviewResult`) into the
|
|
28
|
+
* real issue number(s) `gh issue create` returned.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as fsSync from "node:fs"
|
|
32
|
+
import * as fsp from "node:fs/promises"
|
|
33
|
+
import * as path from "node:path"
|
|
34
|
+
|
|
35
|
+
import { HeadlessSession } from "../engine/loop.js"
|
|
36
|
+
import { Logger } from "../engine/logger.js"
|
|
37
|
+
import { OpenRouterClient } from "../llm/openrouter.js"
|
|
38
|
+
import { OllamaClient } from "../llm/ollama.js"
|
|
39
|
+
import { loadCustomModes, selectToolsForMode } from "../engine/prompt.js"
|
|
40
|
+
import { createHeadlessExecutor } from "../tools/executor.js"
|
|
41
|
+
import { resolvePerModeEnv } from "../cli.js"
|
|
42
|
+
import type { LlmClient, SessionResult } from "../engine/types.js"
|
|
43
|
+
|
|
44
|
+
/** Default path pattern for a research stage's artifact (relative to workspace). */
|
|
45
|
+
export const DEFAULT_RESEARCH_ARTIFACT_PATTERN = "research/*.md"
|
|
46
|
+
|
|
47
|
+
/** Default minimum real file:line citations the research doc must contain. */
|
|
48
|
+
export const DEFAULT_RESEARCH_MIN_CITATIONS = 3
|
|
49
|
+
|
|
50
|
+
/** Default required section headings in the research doc. */
|
|
51
|
+
export const DEFAULT_RESEARCH_SECTIONS = ["Finding", "What to build", "What NOT to do", "How to verify"]
|
|
52
|
+
|
|
53
|
+
/** Mode slug the research stage runs (defined in .roomodes). */
|
|
54
|
+
export const RESEARCH_MODE = "researcher"
|
|
55
|
+
|
|
56
|
+
/** Mode slug the filing stage runs (defined in .roomodes). */
|
|
57
|
+
export const FILER_MODE = "issue-filer"
|
|
58
|
+
|
|
59
|
+
/** Max iterations for a stage session (research/filing are bounded, short). */
|
|
60
|
+
export const STAGE_MAX_ITERATIONS = 60
|
|
61
|
+
|
|
62
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export interface PipelineStageOptions {
|
|
65
|
+
/** The worktree/repo the stage runs against. */
|
|
66
|
+
workspaceRoot: string
|
|
67
|
+
/** Mode slug for the stage session (default: stage's own). */
|
|
68
|
+
mode?: string
|
|
69
|
+
/** Model id (default: env OPENROUTER_MODEL / client default). */
|
|
70
|
+
model?: string
|
|
71
|
+
/** LLM client; inject a fake in tests (default: OpenRouterClient). */
|
|
72
|
+
llmClient?: LlmClient
|
|
73
|
+
/** Per-stage iteration cap (default: STAGE_MAX_ITERATIONS). */
|
|
74
|
+
maxIterations?: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ResearchStageOptions extends PipelineStageOptions {
|
|
78
|
+
/** Task text for the research session (default: built from the workspace). */
|
|
79
|
+
taskText?: string
|
|
80
|
+
/**
|
|
81
|
+
* Glob (relative to workspaceRoot) that the research doc must match on
|
|
82
|
+
* disk at completion. Bound by default — the research stage is the one
|
|
83
|
+
* that needs the artifact gate as a structural guarantee, not a
|
|
84
|
+
* per-invocation flag.
|
|
85
|
+
*/
|
|
86
|
+
artifactPathPattern?: string
|
|
87
|
+
/** Minimum real file:line citations the doc must contain. */
|
|
88
|
+
minCitations?: number
|
|
89
|
+
/** Required section headings (case-insensitive substring match). */
|
|
90
|
+
sections?: string[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ResearchStageResult {
|
|
94
|
+
/** Path (absolute) of the research doc on disk, when produced. */
|
|
95
|
+
artifactPath?: string
|
|
96
|
+
/** Status: ok = doc produced + artifact gate passed; error = session/artifact failure. */
|
|
97
|
+
status: "ok" | "error"
|
|
98
|
+
/** The session's final summary. */
|
|
99
|
+
summary: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface FilingStageOptions extends PipelineStageOptions {
|
|
103
|
+
/** Task text for the filing session (default: built from the research doc). */
|
|
104
|
+
taskText?: string
|
|
105
|
+
/**
|
|
106
|
+
* Path of the research doc whose CONTENT becomes the filing task input
|
|
107
|
+
* (read off disk — never the research session's conversation).
|
|
108
|
+
*/
|
|
109
|
+
researchArtifactPath?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface FilingStageResult {
|
|
113
|
+
/** Real GitHub issue number(s) parsed from the session's completion. */
|
|
114
|
+
issueNumbers: number[]
|
|
115
|
+
/** Status: ok = at least one issue number parsed; error = none found. */
|
|
116
|
+
status: "ok" | "error"
|
|
117
|
+
/** The session's final summary. */
|
|
118
|
+
summary: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── LLM client resolution (mirrors reviewer.ts) ─────────────────────────────
|
|
122
|
+
|
|
123
|
+
function resolveStageClient(mode: string, model?: string, llmClient?: LlmClient): LlmClient {
|
|
124
|
+
if (llmClient) {
|
|
125
|
+
return llmClient
|
|
126
|
+
}
|
|
127
|
+
const useLocalBackend =
|
|
128
|
+
process.env.HEADLESSCODE_CODE_MODE_BACKEND === "ollama" &&
|
|
129
|
+
(process.env.HEADLESSCODE_LOCAL_BACKEND_MODES ?? "code")
|
|
130
|
+
.split(",")
|
|
131
|
+
.map((s) => s.trim())
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.includes(mode)
|
|
134
|
+
if (useLocalBackend) {
|
|
135
|
+
return new OllamaClient({
|
|
136
|
+
baseUrl: resolvePerModeEnv("HEADLESSCODE_OLLAMA_URL", mode),
|
|
137
|
+
defaultModel: resolvePerModeEnv("HEADLESSCODE_CODE_MODE_MODEL", mode) ?? model,
|
|
138
|
+
// OllamaClient has its own independent abort timer (ollama.ts's
|
|
139
|
+
// DEFAULT_OLLAMA_TIMEOUT_MS, 300s) — see cli.ts's
|
|
140
|
+
// LOCAL_LLM_TIMEOUT_MS doc comment (verified live 2026-08-28)
|
|
141
|
+
// for why local sessions need this raised past the shim's own
|
|
142
|
+
// 600s upstream patience.
|
|
143
|
+
timeoutMs: 630_000,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
return new OpenRouterClient({ apiKey: process.env.HEADLESSCODE_OPENROUTER_API_KEY, defaultModel: model })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildLogger(workspaceRoot: string, label: string): Logger {
|
|
150
|
+
const logFilePath = path.join(workspaceRoot, `${label}.log`)
|
|
151
|
+
fsSync.appendFileSync(
|
|
152
|
+
logFilePath,
|
|
153
|
+
`\n===== headlesscode ${label} stage start: ${new Date().toISOString()} =====\n`,
|
|
154
|
+
"utf-8",
|
|
155
|
+
)
|
|
156
|
+
return new Logger({ level: "info", filePath: logFilePath })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Research stage ──────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
export function defaultResearchTaskText(workspaceRoot: string, artifactPathPattern: string): string {
|
|
162
|
+
return (
|
|
163
|
+
`Research the codebase in this workspace exactly as your operating procedure instructs, and ` +
|
|
164
|
+
`produce ONE written research document. Your task text names an exact output path — that path ` +
|
|
165
|
+
`is your deliverable, not a suggestion.\n\n` +
|
|
166
|
+
`Write the document to a path matching \`${artifactPathPattern}\` (relative to this workspace ` +
|
|
167
|
+
`root). The document MUST contain:\n` +
|
|
168
|
+
`- at least one clearly-labeled "Finding" section;\n` +
|
|
169
|
+
`- a "What to build" section proposing the concrete change;\n` +
|
|
170
|
+
`- a "What NOT to do" section bounding scope;\n` +
|
|
171
|
+
`- a "How to verify" section with real checkable steps.\n\n` +
|
|
172
|
+
`Ground every claim in real file:line citations from files you actually opened ` +
|
|
173
|
+
`(e.g. \`src/engine/loop.ts:123\`) — never invent a citation. Any scratch you need goes in ` +
|
|
174
|
+
`\`.headlesscode/scratch/\` inside this workspace — NEVER write to \`/tmp\` or any other path ` +
|
|
175
|
+
`outside the workspace.\n\n` +
|
|
176
|
+
`You do NOT implement anything, and you do NOT decide there is a different, better task to ` +
|
|
177
|
+
`work on. When the document exists on disk with real content, call attempt_completion naming ` +
|
|
178
|
+
`the path you wrote.`
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Run the research stage: a fresh, bounded `researcher`-mode session that
|
|
184
|
+
* must produce a real, well-cited `.md` doc on disk (artifact gate bound by
|
|
185
|
+
* default). Returns the artifact path when the gate passed.
|
|
186
|
+
*/
|
|
187
|
+
export async function runResearchStage(options: ResearchStageOptions): Promise<ResearchStageResult> {
|
|
188
|
+
const {
|
|
189
|
+
workspaceRoot,
|
|
190
|
+
mode = RESEARCH_MODE,
|
|
191
|
+
model,
|
|
192
|
+
llmClient,
|
|
193
|
+
maxIterations = STAGE_MAX_ITERATIONS,
|
|
194
|
+
artifactPathPattern = DEFAULT_RESEARCH_ARTIFACT_PATTERN,
|
|
195
|
+
minCitations = DEFAULT_RESEARCH_MIN_CITATIONS,
|
|
196
|
+
sections = DEFAULT_RESEARCH_SECTIONS,
|
|
197
|
+
} = options
|
|
198
|
+
|
|
199
|
+
const customModes = await loadCustomModes(workspaceRoot)
|
|
200
|
+
const tools = selectToolsForMode(mode, customModes)
|
|
201
|
+
// The researcher writes a markdown doc — it needs the write tools, but
|
|
202
|
+
// the mode's own `edit` group already restricts to `*.md`; the executor
|
|
203
|
+
// is the full headless one (write_to_file registered) so the artifact
|
|
204
|
+
// can actually be created.
|
|
205
|
+
const executor = createHeadlessExecutor(workspaceRoot)
|
|
206
|
+
|
|
207
|
+
const session = new HeadlessSession({
|
|
208
|
+
workspaceRoot,
|
|
209
|
+
mode,
|
|
210
|
+
model,
|
|
211
|
+
taskText: options.taskText ?? defaultResearchTaskText(workspaceRoot, artifactPathPattern),
|
|
212
|
+
maxIterations,
|
|
213
|
+
tools,
|
|
214
|
+
executor,
|
|
215
|
+
customModes,
|
|
216
|
+
llmClient: resolveStageClient(mode, model, llmClient),
|
|
217
|
+
logger: buildLogger(workspaceRoot, "research"),
|
|
218
|
+
// Structural guarantee: the research stage REQUIRES a real artifact
|
|
219
|
+
// on disk before attempt_completion is accepted.
|
|
220
|
+
requireArtifactPathPattern: artifactPathPattern,
|
|
221
|
+
requireArtifactMinCitations: minCitations,
|
|
222
|
+
requireArtifactSections: sections,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const result: SessionResult = await session.run()
|
|
226
|
+
if (result.status !== "success" || result.result === undefined) {
|
|
227
|
+
return {
|
|
228
|
+
status: "error",
|
|
229
|
+
summary: `Research session failed: ${result.error ?? "unknown error"}`,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// The artifact gate already confirmed a matching file exists on disk at
|
|
234
|
+
// completion; resolve its real path for the caller (first match).
|
|
235
|
+
const artifactPath = await firstMatchingArtifact(workspaceRoot, artifactPathPattern)
|
|
236
|
+
if (!artifactPath) {
|
|
237
|
+
return {
|
|
238
|
+
status: "error",
|
|
239
|
+
summary: "Research session completed but no artifact matching the required pattern was found on disk",
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { status: "ok", artifactPath, summary: result.result }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Filing stage ────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
export function defaultFilingTaskText(researchArtifactPath: string, researchContent: string): string {
|
|
248
|
+
return (
|
|
249
|
+
`File REAL GitHub issue(s) from the research finding below, exactly as your operating ` +
|
|
250
|
+
`procedure instructs. The finding was produced by an earlier research stage and is ` +
|
|
251
|
+
`already well-scoped — you do NOT investigate from scratch.\n\n` +
|
|
252
|
+
`Use \`gh issue create\` to file the issue(s) in the repo that owns this workspace. ` +
|
|
253
|
+
`Confirm the repo with \`gh repo view --json nameWithOwner -q .nameWithOwner\` first, ` +
|
|
254
|
+
`check for duplicates, and report EVERY issue URL \`gh issue create\` returned — never ` +
|
|
255
|
+
`claim an issue was filed without the real URL.\n\n` +
|
|
256
|
+
`Your attempt_completion result MUST end with a line of the exact form:\n` +
|
|
257
|
+
`ISSUES: <number1>, <number2>, ...\n` +
|
|
258
|
+
`listing every issue number you actually filed. Nothing else on that line — this is the ` +
|
|
259
|
+
`only line the orchestrator parses.\n\n` +
|
|
260
|
+
`===== Research finding =====\n${researchContent}`
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Run the filing stage: a fresh `issue-filer`-mode session whose task input
|
|
266
|
+
* is built from the RESEARCH DOC'S CONTENT read off disk (never the research
|
|
267
|
+
* session's conversation). Parses the real issue number(s) from the
|
|
268
|
+
* completion deterministically.
|
|
269
|
+
*/
|
|
270
|
+
export async function runFilingStage(options: FilingStageOptions): Promise<FilingStageResult> {
|
|
271
|
+
const {
|
|
272
|
+
workspaceRoot,
|
|
273
|
+
mode = FILER_MODE,
|
|
274
|
+
model,
|
|
275
|
+
llmClient,
|
|
276
|
+
maxIterations = STAGE_MAX_ITERATIONS,
|
|
277
|
+
} = options
|
|
278
|
+
|
|
279
|
+
let taskText = options.taskText
|
|
280
|
+
if (taskText === undefined) {
|
|
281
|
+
if (!options.researchArtifactPath) {
|
|
282
|
+
return {
|
|
283
|
+
status: "error",
|
|
284
|
+
summary: "Filing stage requires either taskText or researchArtifactPath",
|
|
285
|
+
issueNumbers: [],
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
let content = ""
|
|
289
|
+
try {
|
|
290
|
+
content = await fsp.readFile(options.researchArtifactPath, "utf-8")
|
|
291
|
+
} catch (err) {
|
|
292
|
+
return {
|
|
293
|
+
status: "error",
|
|
294
|
+
summary: `Filing stage could not read research artifact: ${err instanceof Error ? err.message : String(err)}`,
|
|
295
|
+
issueNumbers: [],
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
taskText = defaultFilingTaskText(options.researchArtifactPath, content)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const customModes = await loadCustomModes(workspaceRoot)
|
|
302
|
+
const tools = selectToolsForMode(mode, customModes)
|
|
303
|
+
const executor = createHeadlessExecutor(workspaceRoot)
|
|
304
|
+
|
|
305
|
+
const session = new HeadlessSession({
|
|
306
|
+
workspaceRoot,
|
|
307
|
+
mode,
|
|
308
|
+
model,
|
|
309
|
+
taskText,
|
|
310
|
+
maxIterations,
|
|
311
|
+
tools,
|
|
312
|
+
executor,
|
|
313
|
+
customModes,
|
|
314
|
+
llmClient: resolveStageClient(mode, model, llmClient),
|
|
315
|
+
logger: buildLogger(workspaceRoot, "filer"),
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const result: SessionResult = await session.run()
|
|
319
|
+
if (result.status !== "success" || result.result === undefined) {
|
|
320
|
+
return {
|
|
321
|
+
status: "error",
|
|
322
|
+
summary: `Filing session failed: ${result.error ?? "unknown error"}`,
|
|
323
|
+
issueNumbers: [],
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return { ...parseFilingResult(result.result), summary: result.result }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Parse a filing session's completion into real issue number(s).
|
|
332
|
+
*
|
|
333
|
+
* Deterministic: looks for the required `ISSUES: <n1>, <n2>` line (exact
|
|
334
|
+
* match), then falls back to any `#\d+` references that look like the URLs
|
|
335
|
+
* `gh issue create` returns. Returns `status: "error"` (not a partial-ok)
|
|
336
|
+
* when NO issue number could be parsed — an unverifiable filing claim must
|
|
337
|
+
* not be trusted, mirroring the reviewer's fail-closed verdict.
|
|
338
|
+
*/
|
|
339
|
+
export function parseFilingResult(text: string): { issueNumbers: number[]; status: "ok" | "error" } {
|
|
340
|
+
const summary = text.trim()
|
|
341
|
+
|
|
342
|
+
// Primary path: the required exact `ISSUES: n1, n2` line.
|
|
343
|
+
const structured = summary.match(/^ISSUES\s*:\s*([\d,\s]+)\s*$/im)
|
|
344
|
+
if (structured?.[1]) {
|
|
345
|
+
const numbers = structured[1]
|
|
346
|
+
.split(/[\s,]+/)
|
|
347
|
+
.map((s) => Number(s))
|
|
348
|
+
.filter((n) => Number.isInteger(n) && n > 0)
|
|
349
|
+
if (numbers.length > 0) {
|
|
350
|
+
return { issueNumbers: [...new Set(numbers)], status: "ok" }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Fallback: any issue URL `gh issue create` returns looks like
|
|
355
|
+
// `https://github.com/<owner>/<repo>/issues/<n>` — pull the numbers from
|
|
356
|
+
// those, but ONLY from real URL-shaped references (a bare "#42" in prose
|
|
357
|
+
// is not proof of a filed issue).
|
|
358
|
+
const urlNumbers = [...summary.matchAll(/github\.com\/[^/\s]+\/[^/\s]+\/issues\/(\d+)/gi)].map((m) =>
|
|
359
|
+
Number(m[1]),
|
|
360
|
+
)
|
|
361
|
+
const unique = [...new Set(urlNumbers)].filter((n) => Number.isInteger(n) && n > 0)
|
|
362
|
+
if (unique.length > 0) {
|
|
363
|
+
return { issueNumbers: unique, status: "ok" }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { issueNumbers: [], status: "error" }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Return the absolute path of the first file matching a single-directory
|
|
373
|
+
* glob pattern (relative to workspaceRoot) that is real and non-empty.
|
|
374
|
+
* Mirrors the artifact gate's own `matchingArtifactFileStatus` semantics for
|
|
375
|
+
* the caller-facing result.
|
|
376
|
+
*/
|
|
377
|
+
async function firstMatchingArtifact(workspaceRoot: string, pattern: string): Promise<string | undefined> {
|
|
378
|
+
const base = path.resolve(workspaceRoot)
|
|
379
|
+
const dir = path.posix.dirname(pattern)
|
|
380
|
+
const fileGlob = path.posix.basename(pattern)
|
|
381
|
+
const dirAbs = path.resolve(base, dir)
|
|
382
|
+
if (!dirAbs.startsWith(base + path.sep) && dirAbs !== base) {
|
|
383
|
+
return undefined
|
|
384
|
+
}
|
|
385
|
+
let entries: fsSync.Dirent[]
|
|
386
|
+
try {
|
|
387
|
+
entries = fsSync.readdirSync(dirAbs, { withFileTypes: true })
|
|
388
|
+
} catch {
|
|
389
|
+
return undefined
|
|
390
|
+
}
|
|
391
|
+
// Convert the glob's `*` into a regex (only `*` is used in practice).
|
|
392
|
+
const re = new RegExp(`^${fileGlob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`)
|
|
393
|
+
for (const entry of entries) {
|
|
394
|
+
if (!entry.isFile()) {
|
|
395
|
+
continue
|
|
396
|
+
}
|
|
397
|
+
if (!re.test(entry.name)) {
|
|
398
|
+
continue
|
|
399
|
+
}
|
|
400
|
+
const full = path.join(dirAbs, entry.name)
|
|
401
|
+
try {
|
|
402
|
+
const stat = fsSync.statSync(full)
|
|
403
|
+
if (stat.size > 0) {
|
|
404
|
+
return full
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
// ignore unreadable
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return undefined
|
|
411
|
+
}
|