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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UwUChatMemoryStore — the harness-side client for the future AIRunner/UwUChat
|
|
3
|
+
* memory API (Phase 3). See docs/memory-uwuchat-contract.md for the full
|
|
4
|
+
* endpoint contract + the data-isolation guarantee.
|
|
5
|
+
*
|
|
6
|
+
* STATUS: awaiting the AIRunner endpoints. Until a base URL + token are
|
|
7
|
+
* provided, every method throws a typed `UwUChatMemoryError` with the message
|
|
8
|
+
* "not implemented — awaiting AIRunner endpoint". Once `UWUCHAT_BASE_URL` (and
|
|
9
|
+
* optionally `UWUCHAT_TOKEN`) are set, the store fetches the documented
|
|
10
|
+
* endpoints with native `fetch` and Bearer-token auth.
|
|
11
|
+
*
|
|
12
|
+
* It implements the SAME `MemoryStore` interface as `LocalMemoryStore`, so the
|
|
13
|
+
* harness can swap backends without changing any caller.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Embedder, FactInput, MemoryFact, MemoryStore, RecallResult, SessionSummary } from "./types.js"
|
|
17
|
+
|
|
18
|
+
export interface UwUChatMemoryOptions {
|
|
19
|
+
/** AIRunner/UwUChat base URL. Default: $UWUCHAT_BASE_URL. */
|
|
20
|
+
baseUrl?: string
|
|
21
|
+
/** Scoped service token. Default: $UWUCHAT_TOKEN. */
|
|
22
|
+
token?: string
|
|
23
|
+
/** Injectable fetch (tests use a fake; default: global fetch). */
|
|
24
|
+
fetchImpl?: typeof fetch
|
|
25
|
+
/** Unused here (embeddings are computed harness-side per the spec). */
|
|
26
|
+
embedder?: Embedder
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Typed error for UwUChat API failures. */
|
|
30
|
+
export class UwUChatMemoryError extends Error {
|
|
31
|
+
constructor(
|
|
32
|
+
message: string,
|
|
33
|
+
readonly status?: number,
|
|
34
|
+
readonly body?: unknown,
|
|
35
|
+
) {
|
|
36
|
+
super(message)
|
|
37
|
+
this.name = "UwUChatMemoryError"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Base path prefix for all harness memory endpoints on the AIRunner side. */
|
|
42
|
+
export const API_PREFIX = "/api/v1/harness/memory"
|
|
43
|
+
|
|
44
|
+
export class UwUChatMemoryStore implements MemoryStore {
|
|
45
|
+
readonly baseUrl: string
|
|
46
|
+
private readonly token: string | undefined
|
|
47
|
+
private readonly fetchImpl: typeof fetch
|
|
48
|
+
|
|
49
|
+
constructor(options: UwUChatMemoryOptions = {}) {
|
|
50
|
+
this.baseUrl = (options.baseUrl ?? process.env.UWUCHAT_BASE_URL ?? "").replace(/\/+$/, "")
|
|
51
|
+
this.token = options.token ?? process.env.UWUCHAT_TOKEN
|
|
52
|
+
this.fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Build the absolute URL for an API path under the memory prefix. */
|
|
56
|
+
endpoint(apiPath: string): string {
|
|
57
|
+
return `${this.baseUrl}${apiPath}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Auth + content headers (Bearer token when configured). */
|
|
61
|
+
authHeaders(): Record<string, string> {
|
|
62
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" }
|
|
63
|
+
if (this.token) {
|
|
64
|
+
headers["Authorization"] = `Bearer ${this.token}`
|
|
65
|
+
}
|
|
66
|
+
return headers
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private ensureConfigured(): void {
|
|
70
|
+
if (!this.baseUrl) {
|
|
71
|
+
throw new UwUChatMemoryError(
|
|
72
|
+
"UwUChatMemoryStore: not implemented — awaiting AIRunner endpoint (set UWUCHAT_BASE_URL and, if required, UWUCHAT_TOKEN)",
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async request<T>(method: string, apiPath: string, body?: unknown): Promise<T> {
|
|
78
|
+
this.ensureConfigured()
|
|
79
|
+
const init: RequestInit = { method, headers: this.authHeaders() }
|
|
80
|
+
if (body !== undefined) {
|
|
81
|
+
init.body = JSON.stringify(body)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let response: Response
|
|
85
|
+
try {
|
|
86
|
+
response = await this.fetchImpl(this.endpoint(apiPath), init)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new UwUChatMemoryError(
|
|
89
|
+
`UwUChat ${method} ${apiPath} request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
let bodyText = ""
|
|
95
|
+
try {
|
|
96
|
+
bodyText = await response.text()
|
|
97
|
+
} catch {
|
|
98
|
+
// ignore body read errors
|
|
99
|
+
}
|
|
100
|
+
throw new UwUChatMemoryError(
|
|
101
|
+
`UwUChat ${method} ${apiPath} failed with status ${response.status}`,
|
|
102
|
+
response.status,
|
|
103
|
+
bodyText,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
return (await response.json()) as T
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw new UwUChatMemoryError(`UwUChat ${method} ${apiPath} returned invalid JSON`, response.status)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── MemoryStore implementation (project-scoped) ─────────────────────────
|
|
115
|
+
|
|
116
|
+
async listFacts(project: string): Promise<MemoryFact[]> {
|
|
117
|
+
const data = await this.request<{ facts?: MemoryFact[] }>(
|
|
118
|
+
"GET",
|
|
119
|
+
`${API_PREFIX}/facts?project=${encodeURIComponent(project)}`,
|
|
120
|
+
)
|
|
121
|
+
return data.facts ?? []
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async addFact(project: string, factInput: FactInput): Promise<MemoryFact> {
|
|
125
|
+
const data = await this.request<{ fact: MemoryFact }>("POST", `${API_PREFIX}/record-fact`, {
|
|
126
|
+
project,
|
|
127
|
+
fact: factInput,
|
|
128
|
+
})
|
|
129
|
+
return data.fact
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async queryRecall(project: string, query: string, limit = 5): Promise<RecallResult> {
|
|
133
|
+
const data = await this.request<RecallResult>("POST", `${API_PREFIX}/query-recall`, { project, query, limit })
|
|
134
|
+
return { facts: data.facts ?? [], summaries: data.summaries ?? [] }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async recordSession(project: string, summary: SessionSummary): Promise<void> {
|
|
138
|
+
await this.request<{ ok?: boolean }>("POST", `${API_PREFIX}/record-session`, { project, session: summary })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async listSessions(project: string): Promise<SessionSummary[]> {
|
|
142
|
+
const data = await this.request<{ sessions?: SessionSummary[] }>(
|
|
143
|
+
"GET",
|
|
144
|
+
`${API_PREFIX}/sessions?project=${encodeURIComponent(project)}`,
|
|
145
|
+
)
|
|
146
|
+
return data.sessions ?? []
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async summarize(project: string, options: { maxEntries?: number } = {}): Promise<string> {
|
|
150
|
+
const query = new URLSearchParams({ project })
|
|
151
|
+
if (options.maxEntries !== undefined) {
|
|
152
|
+
query.set("maxEntries", String(options.maxEntries))
|
|
153
|
+
}
|
|
154
|
+
const data = await this.request<{ summary: string }>("GET", `${API_PREFIX}/summaries?${query.toString()}`)
|
|
155
|
+
return data.summary
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `headlesscode migrate` subcommand — the explicit, human-triggerable version
|
|
3
|
+
* of the one-time migrations the central store performs automatically on first
|
|
4
|
+
* use (see src/project-store.ts). Running it is always safe (idempotent): each
|
|
5
|
+
* migration is skipped when the source is absent or the target already exists,
|
|
6
|
+
* and every move verifies the content landed before the source is removed.
|
|
7
|
+
*
|
|
8
|
+
* headlesscode migrate [--workspace <path>]
|
|
9
|
+
*
|
|
10
|
+
* Migrates:
|
|
11
|
+
* 1. Global shared instructions out of `~/.roo/` (Zoo-Code-branded) into
|
|
12
|
+
* `~/.local/share/headlesscode/shared/` (modes.yaml + rules(-<mode>)/).
|
|
13
|
+
* 2. The checkpoint store from `~/.headlesscode/checkpoints` into
|
|
14
|
+
* `~/.local/share/headlesscode/checkpoints` (a MOVE — real, potentially
|
|
15
|
+
* large data is never copied-and-orphaned).
|
|
16
|
+
* 3. When `--workspace <path>` is given (default: the current directory),
|
|
17
|
+
* that workspace's legacy `<root>/.headlesscode/{codesearch,
|
|
18
|
+
* mode-models.json, permissions.json}` into the central project store —
|
|
19
|
+
* keyed by the repo's git-common-dir, so worktrees share it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as path from "node:path"
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
isStoreOverridden,
|
|
26
|
+
migrateCheckpointStore,
|
|
27
|
+
migrateLegacyProjectData,
|
|
28
|
+
migrateSharedInstructions,
|
|
29
|
+
resolveProjectDataDir,
|
|
30
|
+
resolveProjectIdentity,
|
|
31
|
+
} from "../project-store.js"
|
|
32
|
+
|
|
33
|
+
const MIGRATE_USAGE = `headlesscode migrate — move legacy headlesscode data into the central store
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
headlesscode migrate [--workspace <path>]
|
|
37
|
+
|
|
38
|
+
The central store is ~/.local/share/headlesscode/ (see src/project-store.ts).
|
|
39
|
+
Migrations are one-time and idempotent; each move is verified before the
|
|
40
|
+
source is removed, so re-running is always safe.
|
|
41
|
+
|
|
42
|
+
Steps:
|
|
43
|
+
1. ~/.roo/ -> ~/.local/share/headlesscode/shared/ (global modes + rules)
|
|
44
|
+
2. ~/.headlesscode/checkpoints -> ~/.local/share/headlesscode/checkpoints
|
|
45
|
+
3. <workspace>/.headlesscode/{codesearch,mode-models.json,permissions.json}
|
|
46
|
+
-> ~/.local/share/headlesscode/projects/<key>/ (when --workspace given)
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--workspace <path> Workspace whose legacy .headlesscode/ content to migrate
|
|
50
|
+
(default: the current directory)
|
|
51
|
+
--help Show this help and exit
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
export function parseMigrateArgs(argv: string[]): { workspace?: string; help: boolean; error?: string } {
|
|
55
|
+
const options: { workspace?: string; help: boolean } = { help: false }
|
|
56
|
+
for (let i = 0; i < argv.length; i++) {
|
|
57
|
+
const arg = argv[i]
|
|
58
|
+
switch (arg) {
|
|
59
|
+
case "--help":
|
|
60
|
+
case "-h":
|
|
61
|
+
options.help = true
|
|
62
|
+
break
|
|
63
|
+
case "--workspace": {
|
|
64
|
+
const v = argv[i + 1]
|
|
65
|
+
if (v === undefined || v.startsWith("--")) {
|
|
66
|
+
return { ...options, error: "Missing value for --workspace" }
|
|
67
|
+
}
|
|
68
|
+
options.workspace = v
|
|
69
|
+
i++
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
return { ...options, error: `Unknown argument: ${arg}` }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return options
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function migrateMain(argv: string[]): Promise<number> {
|
|
80
|
+
const { workspace, help, error } = parseMigrateArgs(argv)
|
|
81
|
+
if (error) {
|
|
82
|
+
process.stderr.write(`headlesscode migrate: ${error}\n\n${MIGRATE_USAGE}`)
|
|
83
|
+
return 2
|
|
84
|
+
}
|
|
85
|
+
if (help) {
|
|
86
|
+
process.stdout.write(MIGRATE_USAGE)
|
|
87
|
+
return 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Step 1 + 2: global shared instructions + checkpoint store. Log to stdout
|
|
91
|
+
// (this is the explicit command's report channel). Under a
|
|
92
|
+
// $HEADLESSCODE_DATA_DIR override (tests/scratch) these two REAL-home-data
|
|
93
|
+
// migrations are skipped — the override means "don't touch real home data";
|
|
94
|
+
// the workspace migration below still runs against the override store.
|
|
95
|
+
const log = (msg: string): void => {
|
|
96
|
+
process.stdout.write(`[migrate] ${msg}\n`)
|
|
97
|
+
}
|
|
98
|
+
if (isStoreOverridden()) {
|
|
99
|
+
log("skipping global migrations (shared instructions + checkpoints) under a HEADLESSCODE_DATA_DIR override")
|
|
100
|
+
} else {
|
|
101
|
+
migrateSharedInstructions({ log })
|
|
102
|
+
migrateCheckpointStore({ log })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 3: the workspace's legacy .headlesscode/ content.
|
|
106
|
+
const ws = path.resolve(workspace ?? process.cwd())
|
|
107
|
+
const { keySource } = resolveProjectIdentity(ws)
|
|
108
|
+
const centralDir = resolveProjectDataDir(ws)
|
|
109
|
+
migrateLegacyProjectData(ws, keySource, centralDir, { log })
|
|
110
|
+
|
|
111
|
+
process.stdout.write(
|
|
112
|
+
"[migrate] done. New central store root: ~/.local/share/headlesscode/ (per-project data under projects/, checkpoints under checkpoints/, shared instructions under shared/).\n",
|
|
113
|
+
)
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `headlesscode analyze-worktree` subcommand — ad-hoc, standalone entry
|
|
3
|
+
* point for src/orchestrator/log-analysis.ts. The orchestrator already
|
|
4
|
+
* runs this automatically once each group reaches "done" (see cli.ts's
|
|
5
|
+
* onGroupUpdate); this subcommand exists for pointing it at ANY worktree
|
|
6
|
+
* by hand — a stalled group mid-round, a worktree from a round that
|
|
7
|
+
* predates this feature, or just re-reading a report without waiting for
|
|
8
|
+
* the next orchestrate poll.
|
|
9
|
+
*
|
|
10
|
+
* headlesscode analyze-worktree --worktree <path> [--json]
|
|
11
|
+
* [--stall-minutes <n>] [--repeat-threshold <n>]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { analyzeWorktreeSessions, formatAnalysisReport } from "./log-analysis.js"
|
|
15
|
+
|
|
16
|
+
const ANALYZE_USAGE = `headlesscode analyze-worktree — deterministic session log analysis
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
headlesscode analyze-worktree --worktree <path> [options]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--worktree <path> Worktree root to analyze (required) — the dir
|
|
23
|
+
containing .headlesscode/events/*.jsonl
|
|
24
|
+
--json Print the raw SessionLogAnalysis as JSON instead
|
|
25
|
+
of the human-readable report
|
|
26
|
+
--stall-minutes <n> Gap between events considered a stall (default 15)
|
|
27
|
+
--repeat-threshold <n> Min verbatim repeats to flag a command (default 3)
|
|
28
|
+
--help Show this help and exit
|
|
29
|
+
`
|
|
30
|
+
|
|
31
|
+
interface AnalyzeCliOptions {
|
|
32
|
+
worktree: string
|
|
33
|
+
json: boolean
|
|
34
|
+
stallMinutes?: number
|
|
35
|
+
repeatThreshold?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseAnalyzeArgs(argv: string[]): AnalyzeCliOptions | { help: true } | { error: string } {
|
|
39
|
+
let worktree: string | undefined
|
|
40
|
+
let json = false
|
|
41
|
+
let stallMinutes: number | undefined
|
|
42
|
+
let repeatThreshold: number | undefined
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const arg = argv[i]
|
|
46
|
+
if (arg === "--help" || arg === "-h") {
|
|
47
|
+
return { help: true }
|
|
48
|
+
} else if (arg === "--worktree") {
|
|
49
|
+
worktree = argv[++i]
|
|
50
|
+
} else if (arg === "--json") {
|
|
51
|
+
json = true
|
|
52
|
+
} else if (arg === "--stall-minutes") {
|
|
53
|
+
const raw = argv[++i]
|
|
54
|
+
const n = Number(raw)
|
|
55
|
+
if (!raw || Number.isNaN(n) || n <= 0) {
|
|
56
|
+
return { error: `--stall-minutes must be a positive number, got: ${raw}` }
|
|
57
|
+
}
|
|
58
|
+
stallMinutes = n
|
|
59
|
+
} else if (arg === "--repeat-threshold") {
|
|
60
|
+
const raw = argv[++i]
|
|
61
|
+
const n = Number(raw)
|
|
62
|
+
if (!raw || Number.isNaN(n) || n <= 0) {
|
|
63
|
+
return { error: `--repeat-threshold must be a positive number, got: ${raw}` }
|
|
64
|
+
}
|
|
65
|
+
repeatThreshold = n
|
|
66
|
+
} else {
|
|
67
|
+
return { error: `unknown argument: ${arg}` }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!worktree) {
|
|
72
|
+
return { error: "--worktree is required" }
|
|
73
|
+
}
|
|
74
|
+
return { worktree, json, stallMinutes, repeatThreshold }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function analyzeCliMain(argv: string[]): Promise<number> {
|
|
78
|
+
const parsed = parseAnalyzeArgs(argv)
|
|
79
|
+
if ("help" in parsed) {
|
|
80
|
+
process.stdout.write(ANALYZE_USAGE)
|
|
81
|
+
return 0
|
|
82
|
+
}
|
|
83
|
+
if ("error" in parsed) {
|
|
84
|
+
process.stderr.write(`analyze-worktree: ${parsed.error}\n\n${ANALYZE_USAGE}`)
|
|
85
|
+
return 2
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const analysis = await analyzeWorktreeSessions(parsed.worktree, {
|
|
89
|
+
stallThresholdMs: parsed.stallMinutes !== undefined ? parsed.stallMinutes * 60 * 1000 : undefined,
|
|
90
|
+
repeatThreshold: parsed.repeatThreshold,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (!analysis) {
|
|
94
|
+
process.stderr.write(`analyze-worktree: no .headlesscode/events feed found under ${parsed.worktree}\n`)
|
|
95
|
+
return 1
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (parsed.json) {
|
|
99
|
+
process.stdout.write(JSON.stringify(analysis, null, 2) + "\n")
|
|
100
|
+
} else {
|
|
101
|
+
process.stdout.write(formatAnalysisReport(analysis) + "\n")
|
|
102
|
+
}
|
|
103
|
+
return 0
|
|
104
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-split for oversized issues (issue #53's follow-up): the pre-flight
|
|
3
|
+
* heuristic in split.ts (topLevelSectionCount) was warn-only by design
|
|
4
|
+
* because a crude "count top-level bullets" signal false-positives on issues
|
|
5
|
+
* that are genuinely ONE coherent piece of work with many sequential steps —
|
|
6
|
+
* exploding every bullet into its own issue would be actively worse than
|
|
7
|
+
* dispatching the oversized issue as-is (it fragments one fix's sequential
|
|
8
|
+
* steps across unrelated issues that make no sense in isolation).
|
|
9
|
+
*
|
|
10
|
+
* This module replaces "warn and let a human decide" with a SEMANTIC split:
|
|
11
|
+
* an LLM reads the flagged issue and proposes the smallest number of
|
|
12
|
+
* independently-shippable, coherent sub-issues that partition the work (for
|
|
13
|
+
* a two-part bug report like "server query is unbounded AND client loading
|
|
14
|
+
* state is misleading", that's 2 sub-issues, not one per bullet). Fails
|
|
15
|
+
* OPEN: any failure (LLM error, unparseable response, model proposes fewer
|
|
16
|
+
* than 2 pieces meaning "this doesn't actually decompose") leaves the
|
|
17
|
+
* original issue untouched and dispatches it as-is — the same behavior as
|
|
18
|
+
* before this module existed. A bad split must never be worse than no split.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { LlmClient } from "../engine/types.js"
|
|
22
|
+
import type { IssueSizeWarning, SplitIssue } from "./split.js"
|
|
23
|
+
|
|
24
|
+
/** One sub-issue the model proposed as an independently-shippable slice of the original. */
|
|
25
|
+
export interface ProposedSubIssue {
|
|
26
|
+
title: string
|
|
27
|
+
body: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Never propose fewer than this many pieces — 1 means "don't split", handled as a no-op by the caller. */
|
|
31
|
+
const MIN_SUBISSUES = 2
|
|
32
|
+
|
|
33
|
+
/** Never propose more than this many — a runaway response must not file dozens of issues from one call. */
|
|
34
|
+
const MAX_SUBISSUES = 8
|
|
35
|
+
|
|
36
|
+
const SPLIT_SYSTEM_PROMPT =
|
|
37
|
+
"You split an oversized GitHub issue into a SMALL number of independently-shippable sub-issues. " +
|
|
38
|
+
"HARD RULES:\n" +
|
|
39
|
+
"1. Group by INDEPENDENT DELIVERABLE, not by bullet point. If the issue describes N sequential steps of " +
|
|
40
|
+
"ONE fix (e.g. 'step 1: add an index, step 2: change the query, step 3: verify'), those steps belong in " +
|
|
41
|
+
"ONE sub-issue together — splitting sequential steps of a single fix across issues is WRONG.\n" +
|
|
42
|
+
"2. Only split along boundaries where the pieces could genuinely ship, be reviewed, and be tested " +
|
|
43
|
+
"independently (e.g. a server-side fix vs. a client-side fix for the same bug are usually independent; " +
|
|
44
|
+
"the individual code changes inside ONE of those fixes usually are not).\n" +
|
|
45
|
+
"3. If the issue is really just one coherent piece of work with a long list of steps/details — NOT " +
|
|
46
|
+
`multiple independent deliverables — return a JSON array with FEWER THAN ${MIN_SUBISSUES} elements ` +
|
|
47
|
+
"(e.g. an empty array) to say so. Do not force a split that doesn't exist.\n" +
|
|
48
|
+
"4. Each sub-issue's body must be self-contained: include everything from the original body relevant to " +
|
|
49
|
+
"that piece (file paths, line numbers, root-cause analysis, verification steps) so implementing it doesn't " +
|
|
50
|
+
"require re-reading the original issue. Do not invent details the original didn't contain.\n" +
|
|
51
|
+
"5. Output ONLY a JSON array of objects with exactly two string fields, \"title\" and \"body\" — no " +
|
|
52
|
+
"markdown fences, no preamble, no commentary."
|
|
53
|
+
|
|
54
|
+
function buildSplitUserPrompt(issue: SplitIssue): string {
|
|
55
|
+
return (
|
|
56
|
+
`Issue #${issue.number}: ${issue.title}\n\n` +
|
|
57
|
+
`=== BODY BEGIN ===\n${issue.body ?? "(no body)"}\n=== BODY END ===`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Extract a JSON array from a model response that may still wrap it in prose or a code fence, despite rule 5. */
|
|
62
|
+
function extractJsonArray(text: string): unknown {
|
|
63
|
+
const trimmed = text.trim()
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(trimmed)
|
|
66
|
+
} catch {
|
|
67
|
+
// Fall back to the first [...] span — tolerates an accidental ```json fence or a stray leading sentence.
|
|
68
|
+
const match = trimmed.match(/\[[\s\S]*\]/)
|
|
69
|
+
if (!match) {
|
|
70
|
+
throw new Error("no JSON array found in split proposal response")
|
|
71
|
+
}
|
|
72
|
+
return JSON.parse(match[0])
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Ask the model to propose a semantic split for one oversized issue. Returns
|
|
78
|
+
* an empty array (not an error) when the model determines the issue is
|
|
79
|
+
* genuinely one coherent piece of work — that is a valid, expected outcome,
|
|
80
|
+
* not a failure. Throws only on a real failure (LLM error, garbage response)
|
|
81
|
+
* so the caller can distinguish "model said don't split" from "couldn't ask."
|
|
82
|
+
*/
|
|
83
|
+
export async function proposeSemanticSplit(
|
|
84
|
+
issue: SplitIssue,
|
|
85
|
+
llmClient: LlmClient,
|
|
86
|
+
model: string,
|
|
87
|
+
): Promise<ProposedSubIssue[]> {
|
|
88
|
+
const response = await llmClient.createChatCompletion({
|
|
89
|
+
model,
|
|
90
|
+
messages: [
|
|
91
|
+
{ role: "system", content: SPLIT_SYSTEM_PROMPT },
|
|
92
|
+
{ role: "user", content: buildSplitUserPrompt(issue) },
|
|
93
|
+
],
|
|
94
|
+
})
|
|
95
|
+
const text = response.message.content
|
|
96
|
+
if (typeof text !== "string" || text.trim() === "") {
|
|
97
|
+
throw new Error("split proposal returned an empty message")
|
|
98
|
+
}
|
|
99
|
+
const parsed = extractJsonArray(text)
|
|
100
|
+
if (!Array.isArray(parsed)) {
|
|
101
|
+
throw new Error("split proposal response was not a JSON array")
|
|
102
|
+
}
|
|
103
|
+
const proposals: ProposedSubIssue[] = []
|
|
104
|
+
for (const entry of parsed) {
|
|
105
|
+
if (
|
|
106
|
+
entry &&
|
|
107
|
+
typeof entry === "object" &&
|
|
108
|
+
typeof (entry as { title?: unknown }).title === "string" &&
|
|
109
|
+
typeof (entry as { body?: unknown }).body === "string"
|
|
110
|
+
) {
|
|
111
|
+
proposals.push({ title: (entry as { title: string }).title, body: (entry as { body: string }).body })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (proposals.length < MIN_SUBISSUES) {
|
|
115
|
+
return [] // model said (or effectively said, via malformed entries): don't split.
|
|
116
|
+
}
|
|
117
|
+
return proposals.slice(0, MAX_SUBISSUES)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One flagged issue's outcome, for a human-readable dispatch-time report. */
|
|
121
|
+
export interface AutoSplitOutcome {
|
|
122
|
+
number: number
|
|
123
|
+
title: string
|
|
124
|
+
outcome: "split" | "kept-as-is" | "failed"
|
|
125
|
+
/** Present when outcome is "split": the newly filed sub-issues. */
|
|
126
|
+
created?: Array<{ number: number; title: string; url: string }>
|
|
127
|
+
/** Present when outcome is "failed": why the split attempt didn't happen. */
|
|
128
|
+
reason?: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* For every issue flagged by the pre-flight size check, attempt a semantic
|
|
133
|
+
* split and file the result as real GitHub issues, replacing the oversized
|
|
134
|
+
* issue in the returned list. Fails open per-issue: any single issue's
|
|
135
|
+
* failure (LLM error, filing error) falls back to keeping that issue
|
|
136
|
+
* unchanged and dispatching it as-is — one bad split must never abort the
|
|
137
|
+
* whole round or leave OTHER issues un-split.
|
|
138
|
+
*
|
|
139
|
+
* `proposeSplit`/`createIssue`/`closeParent` are injected (not hardcoded to
|
|
140
|
+
* OpenRouter/gh) so this is unit-testable without network access — the real
|
|
141
|
+
* orchestrate command wires them to proposeSemanticSplit/createGhIssue/a
|
|
142
|
+
* `gh issue close --comment` call.
|
|
143
|
+
*/
|
|
144
|
+
export async function autoSplitOversizedIssues(
|
|
145
|
+
issues: SplitIssue[],
|
|
146
|
+
warnings: IssueSizeWarning[],
|
|
147
|
+
deps: {
|
|
148
|
+
proposeSplit: (issue: SplitIssue) => Promise<ProposedSubIssue[]>
|
|
149
|
+
createIssue: (issue: SplitIssue) => { number: number; url: string }
|
|
150
|
+
closeParent: (issueNumber: number, comment: string) => void
|
|
151
|
+
},
|
|
152
|
+
): Promise<{ issues: SplitIssue[]; outcomes: AutoSplitOutcome[] }> {
|
|
153
|
+
const flagged = new Set(warnings.map((w) => w.number))
|
|
154
|
+
const outcomes: AutoSplitOutcome[] = []
|
|
155
|
+
const next: SplitIssue[] = []
|
|
156
|
+
|
|
157
|
+
for (const issue of issues) {
|
|
158
|
+
if (!flagged.has(issue.number)) {
|
|
159
|
+
next.push(issue)
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
let proposals: ProposedSubIssue[]
|
|
163
|
+
try {
|
|
164
|
+
proposals = await deps.proposeSplit(issue)
|
|
165
|
+
} catch (err) {
|
|
166
|
+
outcomes.push({
|
|
167
|
+
number: issue.number,
|
|
168
|
+
title: issue.title,
|
|
169
|
+
outcome: "failed",
|
|
170
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
171
|
+
})
|
|
172
|
+
next.push(issue)
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
if (proposals.length < MIN_SUBISSUES) {
|
|
176
|
+
outcomes.push({ number: issue.number, title: issue.title, outcome: "kept-as-is" })
|
|
177
|
+
next.push(issue)
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const created: Array<{ number: number; title: string; url: string }> = []
|
|
182
|
+
for (const p of proposals) {
|
|
183
|
+
const real = deps.createIssue({ number: -1, title: p.title, body: `${p.body}\n\n---\nSplit from #${issue.number}.` })
|
|
184
|
+
created.push({ number: real.number, title: p.title, url: real.url })
|
|
185
|
+
next.push({ number: real.number, title: p.title, body: p.body })
|
|
186
|
+
}
|
|
187
|
+
deps.closeParent(
|
|
188
|
+
issue.number,
|
|
189
|
+
`Auto-split into ${created.length} sub-issue(s) by headlesscode's pre-flight size check (issue #53) — ` +
|
|
190
|
+
`the body read like independent pieces of work, so each is dispatched separately instead of risking an ` +
|
|
191
|
+
`iteration-cap/budget burn on one worker:\n\n` +
|
|
192
|
+
created.map((c) => `- #${c.number}: ${c.title}`).join("\n"),
|
|
193
|
+
)
|
|
194
|
+
outcomes.push({ number: issue.number, title: issue.title, outcome: "split", created })
|
|
195
|
+
} catch (err) {
|
|
196
|
+
outcomes.push({
|
|
197
|
+
number: issue.number,
|
|
198
|
+
title: issue.title,
|
|
199
|
+
outcome: "failed",
|
|
200
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
201
|
+
})
|
|
202
|
+
next.push(issue)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { issues: next, outcomes }
|
|
206
|
+
}
|