@gh-symphony/cli 0.3.0 → 0.4.1
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/README.md +2 -2
- package/dist/{chunk-GTNWFVFU.js → chunk-3EFLX75C.js} +5 -5
- package/dist/{chunk-RZ3WO7OV.js → chunk-3IRPSPAF.js} +1 -1
- package/dist/{chunk-VHEGRYK7.js → chunk-BBUCDKSL.js} +285 -23
- package/dist/{chunk-A7EFL6EJ.js → chunk-GOR4JGJT.js} +1 -1
- package/dist/{chunk-2ZIPQ7ZS.js → chunk-MUN7QSFF.js} +6 -3
- package/dist/{chunk-GL55AKBI.js → chunk-RU5GQG6A.js} +1112 -934
- package/dist/{chunk-5HQLPZR5.js → chunk-YKC6CGD6.js} +42 -1
- package/dist/{chunk-4ICDSQCJ.js → chunk-YZP5N5XP.js} +3 -1
- package/dist/{config-cmd-AOZVS6GU.js → config-cmd-OIVIUKG7.js} +1 -1
- package/dist/{doctor-3IIM4UYS.js → doctor-3WDODAKW.js} +7 -7
- package/dist/index.js +10 -10
- package/dist/{repo-SLK4DDXH.js → repo-3JLOCGRJ.js} +23 -6
- package/dist/{setup-G5VYN6FV.js → setup-AADOLSW5.js} +12 -6
- package/dist/{upgrade-BXIHAENU.js → upgrade-KTTYQQFB.js} +2 -2
- package/dist/{version-MC2KSPJB.js → version-HXAMSXVG.js} +1 -1
- package/dist/worker-entry.js +2 -2
- package/dist/{workflow-4OFPSVZ3.js → workflow-URPIYFYQ.js} +6 -6
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ whether `WORKFLOW.md`, `.gh-symphony/context.yaml`,
|
|
|
88
88
|
`.gh-symphony/reference-workflow.md`, and runtime skill files would be created,
|
|
89
89
|
updated, or left unchanged, and then exits without modifying the repository.
|
|
90
90
|
|
|
91
|
-
The same detected environment data is applied to the generated artifacts, so `WORKFLOW.md`, `.gh-symphony/reference-workflow.md`, and the runtime skill templates already include repository-aware validation guidance for the detected package manager, monorepo layout, and explicit validation commands when they exist.
|
|
91
|
+
The same detected environment data is applied to the generated artifacts, so `WORKFLOW.md`, `.gh-symphony/reference-workflow.md`, and the runtime skill templates already include repository-aware validation guidance for the detected package manager, monorepo layout, and explicit validation commands when they exist. The `/gh-symphony` skill also ships a `references/` directory with workflow schema details and composable prompt-body postures for implementation, review, and maintenance workflows.
|
|
92
92
|
|
|
93
93
|
The detector is language-agnostic by default:
|
|
94
94
|
|
|
@@ -102,7 +102,7 @@ Examples of generated validation guidance include `make test`, `just build`, `uv
|
|
|
102
102
|
|
|
103
103
|
### Customizing Agent Behavior
|
|
104
104
|
|
|
105
|
-
`gh-symphony workflow init` generates skill files under `.codex/skills/` (or `.claude/skills/` for Claude Code). These skills define how the AI agent handles commits, pushes, pulls, and project status transitions.
|
|
105
|
+
`gh-symphony workflow init` generates skill files under `.codex/skills/` (or `.claude/skills/` for Claude Code). These skills define how the AI agent handles commits, pushes, pulls, and project status transitions. The generated `/gh-symphony` skill includes `references/` files that can be customized or extended without adding CLI flags.
|
|
106
106
|
|
|
107
107
|
You can further customize the agent's behavior by editing `WORKFLOW.md` — this is the policy layer that controls what the agent does at each workflow phase.
|
|
108
108
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
workflow_init_default
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-RU5GQG6A.js";
|
|
5
5
|
import {
|
|
6
6
|
fetchGithubProjectIssueByRepositoryAndNumber,
|
|
7
7
|
inspectManagedProjectSelection,
|
|
8
8
|
resolveTrackerAdapter
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-BBUCDKSL.js";
|
|
10
10
|
import {
|
|
11
11
|
GitHubApiError,
|
|
12
12
|
createClient,
|
|
@@ -19,10 +19,10 @@ import {
|
|
|
19
19
|
buildPromptVariables,
|
|
20
20
|
parseWorkflowMarkdown,
|
|
21
21
|
renderPrompt
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-YKC6CGD6.js";
|
|
23
23
|
import {
|
|
24
24
|
loadActiveProjectConfig
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-YZP5N5XP.js";
|
|
26
26
|
|
|
27
27
|
// src/commands/workflow.ts
|
|
28
28
|
import { readFile } from "fs/promises";
|
|
@@ -377,7 +377,7 @@ Commands:
|
|
|
377
377
|
preview Render the final worker prompt from a sample or live issue
|
|
378
378
|
|
|
379
379
|
Options:
|
|
380
|
-
workflow init [--non-interactive] [--project <id>] [--output <path>] [--skip-skills] [--skip-context] [--dry-run]
|
|
380
|
+
workflow init [--non-interactive] [--project <id>] [--output <path>] [--skip-skills] [--skip-context (deprecated no-op)] [--dry-run]
|
|
381
381
|
workflow validate [--file <path>]
|
|
382
382
|
workflow preview [issue] [--file <path>] [--issue <owner/repo#number|ENG-123>] [--project-id <projectId>] [--sample <json>] [--attempt <n>]
|
|
383
383
|
|
|
@@ -30,11 +30,11 @@ import {
|
|
|
30
30
|
resolveWorkflowRuntimeTimeouts,
|
|
31
31
|
safeReadDir,
|
|
32
32
|
scheduleRetryAt
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-YKC6CGD6.js";
|
|
34
34
|
import {
|
|
35
35
|
loadGlobalConfig,
|
|
36
36
|
loadProjectConfig
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-YZP5N5XP.js";
|
|
38
38
|
|
|
39
39
|
// ../tracker-github/src/adapter.ts
|
|
40
40
|
import { createHash } from "crypto";
|
|
@@ -1865,7 +1865,9 @@ var linearTrackerAdapter = {
|
|
|
1865
1865
|
return listLinearIssues(
|
|
1866
1866
|
project,
|
|
1867
1867
|
project.tracker.settings?.activeStates,
|
|
1868
|
-
dependencies
|
|
1868
|
+
dependencies,
|
|
1869
|
+
void 0,
|
|
1870
|
+
{ applyPickupLabels: true }
|
|
1869
1871
|
);
|
|
1870
1872
|
},
|
|
1871
1873
|
async listIssuesByStates(project, states, dependencies = {}) {
|
|
@@ -1914,7 +1916,7 @@ var linearTrackerAdapter = {
|
|
|
1914
1916
|
};
|
|
1915
1917
|
}
|
|
1916
1918
|
};
|
|
1917
|
-
async function listLinearIssues(project, stateNamesInput, dependencies, issueIds) {
|
|
1919
|
+
async function listLinearIssues(project, stateNamesInput, dependencies, issueIds, options = {}) {
|
|
1918
1920
|
const config = resolveLinearTrackerConfig(project, dependencies);
|
|
1919
1921
|
const client = createLinearGraphqlClient(config, dependencies.fetchImpl);
|
|
1920
1922
|
const stateNames = readStringArray(stateNamesInput);
|
|
@@ -1931,10 +1933,15 @@ async function listLinearIssues(project, stateNamesInput, dependencies, issueIds
|
|
|
1931
1933
|
assignedOnly: config.assignedOnly,
|
|
1932
1934
|
pageSize: config.pageSize
|
|
1933
1935
|
});
|
|
1934
|
-
const
|
|
1936
|
+
const fetchedIssues = result.nodes.map(
|
|
1935
1937
|
(node) => normalizeLinearIssue(project, config.projectSlug, node, result.rateLimits)
|
|
1936
1938
|
);
|
|
1937
|
-
|
|
1939
|
+
const filteredIssues = options.applyPickupLabels ? filterIssuesByPickupLabels(
|
|
1940
|
+
fetchedIssues,
|
|
1941
|
+
config.pickupLabels,
|
|
1942
|
+
config.projectSlug
|
|
1943
|
+
) : fetchedIssues;
|
|
1944
|
+
Object.defineProperty(filteredIssues, "rateLimits", {
|
|
1938
1945
|
configurable: true,
|
|
1939
1946
|
enumerable: false,
|
|
1940
1947
|
value: result.rateLimits,
|
|
@@ -1943,10 +1950,10 @@ async function listLinearIssues(project, stateNamesInput, dependencies, issueIds
|
|
|
1943
1950
|
if (config.assignedOnly) {
|
|
1944
1951
|
emitAssignedOnlyFilterEvent2({
|
|
1945
1952
|
projectSlug: config.projectSlug,
|
|
1946
|
-
includedCount:
|
|
1953
|
+
includedCount: filteredIssues.length
|
|
1947
1954
|
});
|
|
1948
1955
|
}
|
|
1949
|
-
return
|
|
1956
|
+
return filteredIssues;
|
|
1950
1957
|
}
|
|
1951
1958
|
async function fetchPaginatedLinearIssues(client, input) {
|
|
1952
1959
|
const issues = [];
|
|
@@ -2100,6 +2107,7 @@ function resolveLinearTrackerConfig(project, dependencies) {
|
|
|
2100
2107
|
endpoint: resolveLinearEndpoint(project.tracker),
|
|
2101
2108
|
assignedOnly: resolveAssignedOnly2(project.tracker, dependencies),
|
|
2102
2109
|
pageSize: readPositiveIntegerSetting(project.tracker, "pageSize") ?? DEFAULT_PAGE_SIZE2,
|
|
2110
|
+
pickupLabels: resolvePickupLabels(project.tracker),
|
|
2103
2111
|
projectSlug,
|
|
2104
2112
|
token
|
|
2105
2113
|
};
|
|
@@ -2155,6 +2163,24 @@ function readBooleanSetting(tracker, key) {
|
|
|
2155
2163
|
const value = tracker.settings?.[key];
|
|
2156
2164
|
return value === true || value === "true";
|
|
2157
2165
|
}
|
|
2166
|
+
function resolvePickupLabels(tracker) {
|
|
2167
|
+
const pickupLabels = readObjectSetting(tracker, "pickupLabels") ?? readObjectSetting(tracker, "pickup_labels");
|
|
2168
|
+
return {
|
|
2169
|
+
include: normalizeConfiguredLabels(
|
|
2170
|
+
readStringArray(pickupLabels?.include) ?? []
|
|
2171
|
+
),
|
|
2172
|
+
exclude: normalizeConfiguredLabels(
|
|
2173
|
+
readStringArray(pickupLabels?.exclude) ?? []
|
|
2174
|
+
)
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
function readObjectSetting(tracker, key) {
|
|
2178
|
+
const value = tracker.settings?.[key];
|
|
2179
|
+
if (value === void 0 || value === null || Array.isArray(value) || typeof value !== "object") {
|
|
2180
|
+
return void 0;
|
|
2181
|
+
}
|
|
2182
|
+
return value;
|
|
2183
|
+
}
|
|
2158
2184
|
function readStringArray(value) {
|
|
2159
2185
|
if (value === void 0) {
|
|
2160
2186
|
return void 0;
|
|
@@ -2167,6 +2193,35 @@ function readStringArray(value) {
|
|
|
2167
2193
|
}
|
|
2168
2194
|
return value.filter((entry) => typeof entry === "string");
|
|
2169
2195
|
}
|
|
2196
|
+
function normalizeConfiguredLabels(labels) {
|
|
2197
|
+
return Array.from(
|
|
2198
|
+
new Set(
|
|
2199
|
+
labels.map((label) => label.trim()).filter((label) => label.length > 0)
|
|
2200
|
+
)
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
function filterIssuesByPickupLabels(issues, config, projectSlug) {
|
|
2204
|
+
if (config.include.length === 0 && config.exclude.length === 0) {
|
|
2205
|
+
return issues;
|
|
2206
|
+
}
|
|
2207
|
+
const includeLabels = new Set(config.include);
|
|
2208
|
+
const excludeLabels = new Set(config.exclude);
|
|
2209
|
+
const filtered = issues.filter((issue) => {
|
|
2210
|
+
const issueLabels = new Set(issue.labels);
|
|
2211
|
+
if (config.exclude.some((label) => issueLabels.has(label))) {
|
|
2212
|
+
return false;
|
|
2213
|
+
}
|
|
2214
|
+
return includeLabels.size === 0 || config.include.some((label) => issueLabels.has(label));
|
|
2215
|
+
});
|
|
2216
|
+
emitPickupLabelFilterEvent({
|
|
2217
|
+
projectSlug,
|
|
2218
|
+
include: [...includeLabels],
|
|
2219
|
+
exclude: [...excludeLabels],
|
|
2220
|
+
includedCount: filtered.length,
|
|
2221
|
+
excludedCount: issues.length - filtered.length
|
|
2222
|
+
});
|
|
2223
|
+
return filtered;
|
|
2224
|
+
}
|
|
2170
2225
|
function emitAssignedOnlyFilterEvent2(input) {
|
|
2171
2226
|
console.info(
|
|
2172
2227
|
JSON.stringify({
|
|
@@ -2179,6 +2234,19 @@ function emitAssignedOnlyFilterEvent2(input) {
|
|
|
2179
2234
|
})
|
|
2180
2235
|
);
|
|
2181
2236
|
}
|
|
2237
|
+
function emitPickupLabelFilterEvent(input) {
|
|
2238
|
+
console.info(
|
|
2239
|
+
JSON.stringify({
|
|
2240
|
+
event: "tracker-pickup-label-filtered",
|
|
2241
|
+
tracker: "linear",
|
|
2242
|
+
projectSlug: input.projectSlug,
|
|
2243
|
+
include: input.include,
|
|
2244
|
+
exclude: input.exclude,
|
|
2245
|
+
includedCount: input.includedCount,
|
|
2246
|
+
excludedCount: input.excludedCount
|
|
2247
|
+
})
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2182
2250
|
function requireString(value, label) {
|
|
2183
2251
|
if (typeof value !== "string" || value.length === 0) {
|
|
2184
2252
|
throw new Error(`${label} is required.`);
|
|
@@ -2313,14 +2381,21 @@ async function syncRepositoryForRun(input) {
|
|
|
2313
2381
|
});
|
|
2314
2382
|
}
|
|
2315
2383
|
async function ensureIssueWorkspaceRepository(input) {
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2384
|
+
let dirtyExistingWorkspaceAllowed = false;
|
|
2385
|
+
const repositoryDirectory = input.existingWorkspace ? await syncExistingIssueWorkspaceRepository(
|
|
2386
|
+
{
|
|
2387
|
+
...input,
|
|
2388
|
+
skipPull: Boolean(input.pullRequestBranch),
|
|
2389
|
+
allowDirty: input.allowDirtyExistingWorkspace
|
|
2390
|
+
},
|
|
2391
|
+
(dirtyAllowed) => {
|
|
2392
|
+
dirtyExistingWorkspaceAllowed = dirtyAllowed;
|
|
2393
|
+
}
|
|
2394
|
+
) : await cloneRepositoryForRun({
|
|
2320
2395
|
repository: input.repository,
|
|
2321
2396
|
targetDirectory: input.issueWorkspacePath
|
|
2322
2397
|
});
|
|
2323
|
-
if (input.pullRequestBranch) {
|
|
2398
|
+
if (input.pullRequestBranch && !dirtyExistingWorkspaceAllowed) {
|
|
2324
2399
|
await checkoutPullRequestBranch(
|
|
2325
2400
|
repositoryDirectory,
|
|
2326
2401
|
input.pullRequestBranch
|
|
@@ -2328,6 +2403,20 @@ async function ensureIssueWorkspaceRepository(input) {
|
|
|
2328
2403
|
}
|
|
2329
2404
|
return repositoryDirectory;
|
|
2330
2405
|
}
|
|
2406
|
+
async function inspectIssueWorkspaceDirtyStatus(input) {
|
|
2407
|
+
const repositoryDirectory = join(input.issueWorkspacePath, "repository");
|
|
2408
|
+
const hasGit = await pathExists(join(repositoryDirectory, ".git"));
|
|
2409
|
+
if (!hasGit) {
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
const status = await readGitStatusPorcelain(repositoryDirectory);
|
|
2413
|
+
return {
|
|
2414
|
+
repositoryDirectory,
|
|
2415
|
+
dirty: status.trim().length > 0,
|
|
2416
|
+
dirtyFiles: parseGitStatusFiles(status),
|
|
2417
|
+
summary: status.trim() ? summarizeGitStatus(status) : null
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2331
2420
|
async function loadRepositoryWorkflow(repositoryDirectory, _repository) {
|
|
2332
2421
|
const workflowPath = join(repositoryDirectory, "WORKFLOW.md");
|
|
2333
2422
|
try {
|
|
@@ -2377,7 +2466,7 @@ async function readGitHead(repositoryDirectory) {
|
|
|
2377
2466
|
return null;
|
|
2378
2467
|
}
|
|
2379
2468
|
}
|
|
2380
|
-
async function syncExistingIssueWorkspaceRepository(input) {
|
|
2469
|
+
async function syncExistingIssueWorkspaceRepository(input, onDirtyAllowed) {
|
|
2381
2470
|
await mkdir(input.issueWorkspacePath, { recursive: true });
|
|
2382
2471
|
const repositoryDirectory = join(input.issueWorkspacePath, "repository");
|
|
2383
2472
|
const lockDirectory = join(input.issueWorkspacePath, "repository.lock");
|
|
@@ -2394,6 +2483,10 @@ async function syncExistingIssueWorkspaceRepository(input) {
|
|
|
2394
2483
|
`could not be inspected: ${formatCommandError(error, "git status --porcelain failed")}`
|
|
2395
2484
|
);
|
|
2396
2485
|
}
|
|
2486
|
+
if (dirtyStatus.trim() && input.allowDirty) {
|
|
2487
|
+
onDirtyAllowed?.(true);
|
|
2488
|
+
return repositoryDirectory;
|
|
2489
|
+
}
|
|
2397
2490
|
if (dirtyStatus.trim()) {
|
|
2398
2491
|
throw createIssueWorkspacePreservedError(
|
|
2399
2492
|
repositoryDirectory,
|
|
@@ -2537,6 +2630,9 @@ function summarizeGitStatus(status) {
|
|
|
2537
2630
|
const summary = lines.slice(0, 5).join("; ");
|
|
2538
2631
|
return lines.length > 5 ? `${summary}; ...` : summary;
|
|
2539
2632
|
}
|
|
2633
|
+
function parseGitStatusFiles(status) {
|
|
2634
|
+
return status.trim().split(/\r?\n/).map((line) => line.slice(3).trim()).filter(Boolean);
|
|
2635
|
+
}
|
|
2540
2636
|
function normalizeWhitespace(value) {
|
|
2541
2637
|
return value.replace(/\s+/g, " ").trim();
|
|
2542
2638
|
}
|
|
@@ -3207,6 +3303,25 @@ function explainRuntimeOwnership(issue, issueRecords, runs) {
|
|
|
3207
3303
|
};
|
|
3208
3304
|
}
|
|
3209
3305
|
}
|
|
3306
|
+
if (latestRun?.status === "suppressed" && latestRun.recovery?.kind === "incomplete-turn-dirty-workspace") {
|
|
3307
|
+
return {
|
|
3308
|
+
id: "runtime_ownership",
|
|
3309
|
+
status: "warn",
|
|
3310
|
+
message: "Latest run has a recoverable incomplete-turn dirty workspace; dispatch will start a recovery turn.",
|
|
3311
|
+
details: {
|
|
3312
|
+
runId: latestRun.recovery.runId,
|
|
3313
|
+
issueId: latestRun.recovery.issueId,
|
|
3314
|
+
workspacePath: latestRun.recovery.workspacePath,
|
|
3315
|
+
dirtyFiles: latestRun.recovery.dirtyFiles,
|
|
3316
|
+
lastEvent: latestRun.recovery.lastEvent,
|
|
3317
|
+
lastEventAt: latestRun.recovery.lastEventAt,
|
|
3318
|
+
sessionId: latestRun.recovery.sessionId,
|
|
3319
|
+
threadId: latestRun.recovery.threadId,
|
|
3320
|
+
suggestedCommand: latestRun.recovery.suggestedCommand
|
|
3321
|
+
},
|
|
3322
|
+
hint: latestRun.recovery.suggestedCommand
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3210
3325
|
return {
|
|
3211
3326
|
id: "runtime_ownership",
|
|
3212
3327
|
status: "pass",
|
|
@@ -3311,6 +3426,7 @@ var CONTINUATION_RETRY_DELAY_MS = 1e3;
|
|
|
3311
3426
|
var DEFAULT_WORKER_COMMAND = "node packages/worker/dist/index.js";
|
|
3312
3427
|
var DEFAULT_MAX_NONPRODUCTIVE_TURNS = 3;
|
|
3313
3428
|
var LOW_RATE_LIMIT_WARNING_THRESHOLD = 0.05;
|
|
3429
|
+
var MAX_RECOVERY_DIRTY_FILES_IN_CONTEXT = 50;
|
|
3314
3430
|
var MAX_FAILURE_RETRIES_EXCEEDED_REASON2 = "max_failure_retries_exceeded";
|
|
3315
3431
|
var LINKED_PR_ACTIVE_ISSUE_INACTIVE_MARKER_PREFIX = "gh-symphony:linked-pr-active-while-issue-inactive";
|
|
3316
3432
|
var STUCK_WORKER_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
@@ -3556,6 +3672,7 @@ var OrchestratorService = class {
|
|
|
3556
3672
|
kind: currentRun?.retryKind ?? null,
|
|
3557
3673
|
error: currentRun?.lastError ?? issueRecord.retryEntry?.error ?? null
|
|
3558
3674
|
} : null,
|
|
3675
|
+
recovery: currentRun?.recovery ?? null,
|
|
3559
3676
|
logs: {
|
|
3560
3677
|
codex_session_logs: currentRun === null ? [] : [
|
|
3561
3678
|
{
|
|
@@ -3786,6 +3903,12 @@ var OrchestratorService = class {
|
|
|
3786
3903
|
},
|
|
3787
3904
|
issue.identifier
|
|
3788
3905
|
);
|
|
3906
|
+
const recoveryContext = await this.resolveIncompleteTurnRecoveryContext(
|
|
3907
|
+
tenant,
|
|
3908
|
+
issue,
|
|
3909
|
+
latestRunsByIssueId.get(issue.id) ?? null,
|
|
3910
|
+
preferredWorkspaceKey
|
|
3911
|
+
);
|
|
3789
3912
|
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
3790
3913
|
issueId: issue.id,
|
|
3791
3914
|
identifier: issue.identifier,
|
|
@@ -3798,7 +3921,9 @@ var OrchestratorService = class {
|
|
|
3798
3921
|
});
|
|
3799
3922
|
let run;
|
|
3800
3923
|
try {
|
|
3801
|
-
run = await this.startRun(tenant, issue
|
|
3924
|
+
run = await this.startRun(tenant, issue, {
|
|
3925
|
+
recovery: recoveryContext
|
|
3926
|
+
});
|
|
3802
3927
|
} catch (error) {
|
|
3803
3928
|
issueRecords = releaseIssueOrchestration(issueRecords, issue.id, now);
|
|
3804
3929
|
throw error;
|
|
@@ -3855,6 +3980,11 @@ var OrchestratorService = class {
|
|
|
3855
3980
|
const activeWorkerPid = activeRun.processId;
|
|
3856
3981
|
this.sendSignal(activeWorkerPid, "SIGTERM");
|
|
3857
3982
|
this.retireWorkerPid(activeWorkerPid);
|
|
3983
|
+
const recovery = await this.classifyIncompleteTurnDirtyWorkspace(
|
|
3984
|
+
tenant,
|
|
3985
|
+
activeRun,
|
|
3986
|
+
now
|
|
3987
|
+
);
|
|
3858
3988
|
const suppressedRun = {
|
|
3859
3989
|
...activeRun,
|
|
3860
3990
|
status: "suppressed",
|
|
@@ -3862,7 +3992,17 @@ var OrchestratorService = class {
|
|
|
3862
3992
|
completedAt: now.toISOString(),
|
|
3863
3993
|
updatedAt: now.toISOString(),
|
|
3864
3994
|
runPhase: "canceled_by_reconciliation",
|
|
3865
|
-
|
|
3995
|
+
runtimeSession: recovery ? buildRuntimeSession(
|
|
3996
|
+
activeRun.runtimeSession,
|
|
3997
|
+
recovery.sessionId,
|
|
3998
|
+
recovery.threadId,
|
|
3999
|
+
"completed",
|
|
4000
|
+
activeRun.runtimeSession?.startedAt ?? activeRun.startedAt ?? now.toISOString(),
|
|
4001
|
+
now.toISOString(),
|
|
4002
|
+
"incomplete-turn-dirty-workspace"
|
|
4003
|
+
) : activeRun.runtimeSession,
|
|
4004
|
+
recovery,
|
|
4005
|
+
lastError: recovery ? "Run suppressed with recoverable incomplete-turn dirty workspace." : "Run suppressed because the tracker issue is no longer tracked."
|
|
3866
4006
|
};
|
|
3867
4007
|
await this.store.saveRun(suppressedRun);
|
|
3868
4008
|
this.logVerbose(
|
|
@@ -3888,6 +4028,11 @@ var OrchestratorService = class {
|
|
|
3888
4028
|
}
|
|
3889
4029
|
if (activeRun) {
|
|
3890
4030
|
const terminalState = isStateTerminal(issue.state, lifecycle);
|
|
4031
|
+
const recovery = terminalState ? null : await this.classifyIncompleteTurnDirtyWorkspace(
|
|
4032
|
+
tenant,
|
|
4033
|
+
activeRun,
|
|
4034
|
+
now
|
|
4035
|
+
);
|
|
3891
4036
|
const suppressedRun = {
|
|
3892
4037
|
...activeRun,
|
|
3893
4038
|
status: "suppressed",
|
|
@@ -3895,7 +4040,17 @@ var OrchestratorService = class {
|
|
|
3895
4040
|
completedAt: now.toISOString(),
|
|
3896
4041
|
updatedAt: now.toISOString(),
|
|
3897
4042
|
runPhase: "canceled_by_reconciliation",
|
|
3898
|
-
|
|
4043
|
+
runtimeSession: recovery ? buildRuntimeSession(
|
|
4044
|
+
activeRun.runtimeSession,
|
|
4045
|
+
recovery.sessionId,
|
|
4046
|
+
recovery.threadId,
|
|
4047
|
+
"completed",
|
|
4048
|
+
activeRun.runtimeSession?.startedAt ?? activeRun.startedAt ?? now.toISOString(),
|
|
4049
|
+
now.toISOString(),
|
|
4050
|
+
"incomplete-turn-dirty-workspace"
|
|
4051
|
+
) : activeRun.runtimeSession,
|
|
4052
|
+
recovery,
|
|
4053
|
+
lastError: recovery ? "Run suppressed with recoverable incomplete-turn dirty workspace." : terminalState ? "Run suppressed because the tracker issue moved to a terminal state." : "Run suppressed because the tracker state is no longer actionable."
|
|
3899
4054
|
};
|
|
3900
4055
|
await this.store.saveRun(suppressedRun);
|
|
3901
4056
|
this.logVerbose(
|
|
@@ -4247,7 +4402,7 @@ var OrchestratorService = class {
|
|
|
4247
4402
|
true
|
|
4248
4403
|
);
|
|
4249
4404
|
}
|
|
4250
|
-
async startRun(tenant, issue) {
|
|
4405
|
+
async startRun(tenant, issue, options = {}) {
|
|
4251
4406
|
if (this.shuttingDown || !this.running) {
|
|
4252
4407
|
throw new Error(
|
|
4253
4408
|
"Orchestrator is shutting down and cannot start new runs."
|
|
@@ -4289,7 +4444,8 @@ var OrchestratorService = class {
|
|
|
4289
4444
|
repository: issue.repository,
|
|
4290
4445
|
issueWorkspacePath,
|
|
4291
4446
|
existingWorkspace: Boolean(existingWorkspaceRecord),
|
|
4292
|
-
pullRequestBranch
|
|
4447
|
+
pullRequestBranch,
|
|
4448
|
+
allowDirtyExistingWorkspace: options.recovery?.kind === "incomplete-turn-dirty-workspace"
|
|
4293
4449
|
});
|
|
4294
4450
|
if (!existingWorkspaceRecord) {
|
|
4295
4451
|
const workspaceRecord = {
|
|
@@ -4340,9 +4496,10 @@ var OrchestratorService = class {
|
|
|
4340
4496
|
attempt: null
|
|
4341
4497
|
// first execution
|
|
4342
4498
|
});
|
|
4343
|
-
const renderedPrompt =
|
|
4499
|
+
const renderedPrompt = appendIncompleteTurnRecoveryPrompt(
|
|
4344
4500
|
workflow.promptTemplate,
|
|
4345
|
-
promptVariables
|
|
4501
|
+
promptVariables,
|
|
4502
|
+
options.recovery ?? null
|
|
4346
4503
|
);
|
|
4347
4504
|
await this.runHook(
|
|
4348
4505
|
"before_run",
|
|
@@ -4434,6 +4591,9 @@ var OrchestratorService = class {
|
|
|
4434
4591
|
SYMPHONY_CUMULATIVE_OUTPUT_TOKENS: "0",
|
|
4435
4592
|
SYMPHONY_CUMULATIVE_TOTAL_TOKENS: "0",
|
|
4436
4593
|
SYMPHONY_LAST_TURN_SUMMARY: "",
|
|
4594
|
+
SYMPHONY_RECOVERY_KIND: options.recovery?.kind ?? "",
|
|
4595
|
+
SYMPHONY_RECOVERY_DIRTY_FILES: options.recovery ? formatRecoveryDirtyFilesForContext(options.recovery.dirtyFiles) : "",
|
|
4596
|
+
SYMPHONY_RECOVERY_SUGGESTED_COMMAND: options.recovery?.suggestedCommand ?? "",
|
|
4437
4597
|
SYMPHONY_SESSION_STARTED_AT: "",
|
|
4438
4598
|
SYMPHONY_READ_TIMEOUT_MS: String(runtimeTimeouts.readTimeoutMs),
|
|
4439
4599
|
SYMPHONY_TURN_TIMEOUT_MS: String(runtimeTimeouts.turnTimeoutMs)
|
|
@@ -4547,7 +4707,7 @@ var OrchestratorService = class {
|
|
|
4547
4707
|
issueWorkspaceKey: workspaceKey,
|
|
4548
4708
|
workspaceRuntimeDir,
|
|
4549
4709
|
workflowPath: workflow.workflowPath,
|
|
4550
|
-
retryKind: null,
|
|
4710
|
+
retryKind: options.recovery ? "recovery" : null,
|
|
4551
4711
|
threadId: null,
|
|
4552
4712
|
cumulativeTurnCount: 0,
|
|
4553
4713
|
lastTurnSummary: null,
|
|
@@ -4558,7 +4718,8 @@ var OrchestratorService = class {
|
|
|
4558
4718
|
lastError: null,
|
|
4559
4719
|
nextRetryAt: null,
|
|
4560
4720
|
runPhase: "preparing_workspace",
|
|
4561
|
-
rateLimits: issue.rateLimits ?? null
|
|
4721
|
+
rateLimits: issue.rateLimits ?? null,
|
|
4722
|
+
recovery: options.recovery ?? null
|
|
4562
4723
|
};
|
|
4563
4724
|
}
|
|
4564
4725
|
async syncActiveRunIssueStates(tenant, trackerAdapter, activeRuns, now) {
|
|
@@ -5182,6 +5343,66 @@ var OrchestratorService = class {
|
|
|
5182
5343
|
);
|
|
5183
5344
|
return issue ? { issue, issues } : null;
|
|
5184
5345
|
}
|
|
5346
|
+
async classifyIncompleteTurnDirtyWorkspace(tenant, run, now) {
|
|
5347
|
+
if (run.runtimeSession?.status !== "active" || run.runtimeSession.exitClassification !== null || run.lastEvent === "turn_completed") {
|
|
5348
|
+
return null;
|
|
5349
|
+
}
|
|
5350
|
+
const workspaceKey = run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
5351
|
+
{
|
|
5352
|
+
adapter: tenant.tracker.adapter,
|
|
5353
|
+
issueSubjectId: run.issueSubjectId
|
|
5354
|
+
},
|
|
5355
|
+
run.issueIdentifier
|
|
5356
|
+
);
|
|
5357
|
+
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
5358
|
+
this.store.projectDir(tenant.projectId),
|
|
5359
|
+
workspaceKey
|
|
5360
|
+
);
|
|
5361
|
+
const dirtyStatus = await inspectIssueWorkspaceDirtyStatus({
|
|
5362
|
+
issueWorkspacePath
|
|
5363
|
+
});
|
|
5364
|
+
if (!dirtyStatus?.dirty) {
|
|
5365
|
+
return null;
|
|
5366
|
+
}
|
|
5367
|
+
return {
|
|
5368
|
+
kind: "incomplete-turn-dirty-workspace",
|
|
5369
|
+
runId: run.runId,
|
|
5370
|
+
issueId: run.issueId,
|
|
5371
|
+
issueIdentifier: run.issueIdentifier,
|
|
5372
|
+
workspacePath: dirtyStatus.repositoryDirectory,
|
|
5373
|
+
dirtyFiles: dirtyStatus.dirtyFiles,
|
|
5374
|
+
lastEvent: run.lastEvent ?? null,
|
|
5375
|
+
lastEventAt: run.lastEventAt ?? null,
|
|
5376
|
+
sessionId: run.runtimeSession.sessionId ?? null,
|
|
5377
|
+
threadId: run.threadId ?? run.runtimeSession.threadId ?? null,
|
|
5378
|
+
suggestedCommand: `cd ${shellQuote(dirtyStatus.repositoryDirectory)} && git status --short && git diff`,
|
|
5379
|
+
detectedAt: now.toISOString()
|
|
5380
|
+
};
|
|
5381
|
+
}
|
|
5382
|
+
async resolveIncompleteTurnRecoveryContext(tenant, issue, latestRun, preferredWorkspaceKey) {
|
|
5383
|
+
const recovery = latestRun?.recovery;
|
|
5384
|
+
if (latestRun?.status !== "suppressed" || recovery?.kind !== "incomplete-turn-dirty-workspace" || latestRun.runtimeSession?.exitClassification !== "incomplete-turn-dirty-workspace") {
|
|
5385
|
+
return null;
|
|
5386
|
+
}
|
|
5387
|
+
const workspaceKey = latestRun.issueWorkspaceKey ?? preferredWorkspaceKey;
|
|
5388
|
+
const dirtyStatus = await inspectIssueWorkspaceDirtyStatus({
|
|
5389
|
+
issueWorkspacePath: resolveIssueWorkspaceDirectory(
|
|
5390
|
+
this.store.projectDir(tenant.projectId),
|
|
5391
|
+
workspaceKey
|
|
5392
|
+
)
|
|
5393
|
+
});
|
|
5394
|
+
if (!dirtyStatus?.dirty) {
|
|
5395
|
+
return null;
|
|
5396
|
+
}
|
|
5397
|
+
return {
|
|
5398
|
+
...recovery,
|
|
5399
|
+
issueId: issue.id,
|
|
5400
|
+
issueIdentifier: issue.identifier,
|
|
5401
|
+
workspacePath: dirtyStatus.repositoryDirectory,
|
|
5402
|
+
dirtyFiles: dirtyStatus.dirtyFiles,
|
|
5403
|
+
suggestedCommand: `cd ${shellQuote(dirtyStatus.repositoryDirectory)} && git status --short && git diff`
|
|
5404
|
+
};
|
|
5405
|
+
}
|
|
5185
5406
|
async fetchWorkerRunInfo(run) {
|
|
5186
5407
|
const latestRun = await this.store.loadRun(run.runId, run.projectId) ?? run;
|
|
5187
5408
|
const persistedTokenUsage = await this.readPersistedWorkerTokenUsage(latestRun);
|
|
@@ -5758,9 +5979,50 @@ function buildRuntimeSession(existing, sessionId, threadId, status, startedAt, u
|
|
|
5758
5979
|
exitClassification: exitClassification === void 0 ? existing?.exitClassification ?? null : exitClassification
|
|
5759
5980
|
};
|
|
5760
5981
|
}
|
|
5982
|
+
function appendIncompleteTurnRecoveryPrompt(promptTemplate, promptVariables, recovery) {
|
|
5983
|
+
const renderedPrompt = renderPrompt(promptTemplate, promptVariables);
|
|
5984
|
+
if (!recovery) {
|
|
5985
|
+
return renderedPrompt;
|
|
5986
|
+
}
|
|
5987
|
+
return [
|
|
5988
|
+
renderedPrompt,
|
|
5989
|
+
"",
|
|
5990
|
+
"## Recovery Context \u2014 Incomplete Turn Dirty Workspace",
|
|
5991
|
+
"",
|
|
5992
|
+
`Previous run: ${recovery.runId}`,
|
|
5993
|
+
`Workspace: ${recovery.workspacePath}`,
|
|
5994
|
+
`Last event: ${recovery.lastEvent ?? "unknown"}`,
|
|
5995
|
+
`Last event time: ${recovery.lastEventAt ?? "unknown"}`,
|
|
5996
|
+
`Session id: ${recovery.sessionId ?? "unknown"}`,
|
|
5997
|
+
`Thread id: ${recovery.threadId ?? "unknown"}`,
|
|
5998
|
+
"",
|
|
5999
|
+
"Dirty files:",
|
|
6000
|
+
...formatRecoveryDirtyFileLinesForPrompt(recovery.dirtyFiles),
|
|
6001
|
+
"",
|
|
6002
|
+
"Inspect the dirty diff before editing. If the partial work is correct, validate it, commit it, and push it. If it is invalid, revert it explicitly and record a blocker/comment with the reason. Do not discard uncommitted work without making an intentional recovery decision.",
|
|
6003
|
+
`Suggested operator command: ${recovery.suggestedCommand}`
|
|
6004
|
+
].join("\n");
|
|
6005
|
+
}
|
|
6006
|
+
function formatRecoveryDirtyFilesForContext(dirtyFiles) {
|
|
6007
|
+
return formatRecoveryDirtyFiles(dirtyFiles).join("\n");
|
|
6008
|
+
}
|
|
6009
|
+
function formatRecoveryDirtyFileLinesForPrompt(dirtyFiles) {
|
|
6010
|
+
return formatRecoveryDirtyFiles(dirtyFiles).map((file) => `- ${file}`);
|
|
6011
|
+
}
|
|
6012
|
+
function formatRecoveryDirtyFiles(dirtyFiles) {
|
|
6013
|
+
const visibleFiles = dirtyFiles.slice(0, MAX_RECOVERY_DIRTY_FILES_IN_CONTEXT);
|
|
6014
|
+
const remaining = dirtyFiles.length - visibleFiles.length;
|
|
6015
|
+
if (remaining <= 0) {
|
|
6016
|
+
return visibleFiles;
|
|
6017
|
+
}
|
|
6018
|
+
return [...visibleFiles, `... and ${remaining} more`];
|
|
6019
|
+
}
|
|
5761
6020
|
function resolvePersistedCumulativeTurnCount(run) {
|
|
5762
6021
|
return run.cumulativeTurnCount ?? run.turnCount ?? 0;
|
|
5763
6022
|
}
|
|
6023
|
+
function shellQuote(value) {
|
|
6024
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
6025
|
+
}
|
|
5764
6026
|
function resolveCumulativeTurnCount(run, turnCount) {
|
|
5765
6027
|
const carriedTotal = resolvePersistedCumulativeTurnCount(run);
|
|
5766
6028
|
if (turnCount === null) {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
resolveRepoRuntimeRoot
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-3IRPSPAF.js";
|
|
5
5
|
import {
|
|
6
6
|
parseWorkflowMarkdown
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-YKC6CGD6.js";
|
|
8
8
|
import {
|
|
9
9
|
saveGlobalConfig,
|
|
10
10
|
saveProjectConfig
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-YZP5N5XP.js";
|
|
12
12
|
|
|
13
13
|
// src/repo-runtime.ts
|
|
14
14
|
import { execFileSync } from "child_process";
|
|
@@ -68,6 +68,9 @@ async function initRepoRuntime(flags) {
|
|
|
68
68
|
...trackerAdapter === "linear" ? { activeStates: workflow.tracker.activeStates.join("\n") } : {},
|
|
69
69
|
repository: `${repository.owner}/${repository.name}`
|
|
70
70
|
};
|
|
71
|
+
if (trackerAdapter === "linear" && (workflow.tracker.pickupLabels.include.length > 0 || workflow.tracker.pickupLabels.exclude.length > 0)) {
|
|
72
|
+
trackerSettings.pickupLabels = workflow.tracker.pickupLabels;
|
|
73
|
+
}
|
|
71
74
|
if (workflow.tracker.priorityFieldName) {
|
|
72
75
|
trackerSettings.priorityFieldName = workflow.tracker.priorityFieldName;
|
|
73
76
|
}
|