@gh-symphony/cli 0.2.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{chunk-3SKN5L3I.js → chunk-6OPRRC2J.js} +36 -7
- package/dist/{chunk-NRABQNAX.js → chunk-B6G3KGBB.js} +212 -17
- package/dist/{chunk-DLZ2XHWY.js → chunk-QOX5UGUE.js} +1 -1
- package/dist/{chunk-5U36B7FC.js → chunk-TTVGBHZI.js} +8 -4
- package/dist/{chunk-FAU72YC2.js → chunk-Z7CDL3T2.js} +1 -1
- package/dist/{chunk-DTPIJO6S.js → chunk-ZPS4CQZJ.js} +524 -63
- package/dist/{doctor-TQR54KNZ.js → doctor-CCUTNEYN.js} +5 -5
- package/dist/index.js +6 -6
- package/dist/{repo-Y6EF2DZP.js → repo-C2APQR2P.js} +21 -4
- package/dist/{setup-T2QENR26.js → setup-JINI7HBM.js} +18 -6
- package/dist/{upgrade-7452LZXX.js → upgrade-EBD4LX5W.js} +2 -2
- package/dist/{version-D3FB3PXO.js → version-6Z354HHH.js} +1 -1
- package/dist/worker-entry.js +5 -5
- package/dist/{workflow-AV676KAP.js → workflow-BOZ25AJ2.js} +5 -5
- package/package.json +3 -3
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
|
|
|
@@ -5,7 +5,8 @@ var DEFAULT_WORKFLOW_LIFECYCLE = {
|
|
|
5
5
|
stateFieldName: "Status",
|
|
6
6
|
activeStates: ["Todo", "In Progress"],
|
|
7
7
|
terminalStates: ["Done"],
|
|
8
|
-
blockerCheckStates: [
|
|
8
|
+
blockerCheckStates: [],
|
|
9
|
+
planningStates: []
|
|
9
10
|
};
|
|
10
11
|
function isStateActive(state, lifecycle) {
|
|
11
12
|
return matchesWorkflowState(state, lifecycle.activeStates);
|
|
@@ -15,7 +16,9 @@ function isStateTerminal(state, lifecycle) {
|
|
|
15
16
|
}
|
|
16
17
|
function matchesWorkflowState(state, candidates) {
|
|
17
18
|
const normalizedState = normalizeWorkflowState(state);
|
|
18
|
-
return candidates.some(
|
|
19
|
+
return candidates.some(
|
|
20
|
+
(candidate) => normalizeWorkflowState(candidate) === normalizedState
|
|
21
|
+
);
|
|
19
22
|
}
|
|
20
23
|
function normalizeWorkflowState(state) {
|
|
21
24
|
return state.trim().toLowerCase();
|
|
@@ -55,7 +58,8 @@ var DEFAULT_WORKFLOW_TRACKER = {
|
|
|
55
58
|
stateFieldName: DEFAULT_WORKFLOW_LIFECYCLE.stateFieldName,
|
|
56
59
|
priority: null,
|
|
57
60
|
priorityFieldName: null,
|
|
58
|
-
blockerCheckStates: DEFAULT_WORKFLOW_LIFECYCLE.blockerCheckStates
|
|
61
|
+
blockerCheckStates: DEFAULT_WORKFLOW_LIFECYCLE.blockerCheckStates,
|
|
62
|
+
planningStates: DEFAULT_WORKFLOW_LIFECYCLE.planningStates
|
|
59
63
|
};
|
|
60
64
|
var DEFAULT_WORKFLOW_WORKSPACE = {
|
|
61
65
|
root: null
|
|
@@ -135,6 +139,7 @@ function parseWorkflowMarkdown(markdown, env = process.env, options = {}) {
|
|
|
135
139
|
const activeStates = readStringList(tracker, "active_states") ?? DEFAULT_WORKFLOW_TRACKER.activeStates;
|
|
136
140
|
const terminalStates = readStringList(tracker, "terminal_states") ?? DEFAULT_WORKFLOW_TRACKER.terminalStates;
|
|
137
141
|
const blockerCheckStates = readStringList(tracker, "blocker_check_states") ?? DEFAULT_WORKFLOW_TRACKER.blockerCheckStates;
|
|
142
|
+
const planningStates = readStringList(tracker, "planning_states") ?? blockerCheckStates;
|
|
138
143
|
const maxConcurrentAgentsByState = readNumberMap(
|
|
139
144
|
agent,
|
|
140
145
|
"max_concurrent_agents_by_state"
|
|
@@ -172,7 +177,8 @@ function parseWorkflowMarkdown(markdown, env = process.env, options = {}) {
|
|
|
172
177
|
stateFieldName: readOptionalString(tracker, "state_field", env) ?? DEFAULT_WORKFLOW_TRACKER.stateFieldName,
|
|
173
178
|
priority: readPriorityConfig(tracker, env),
|
|
174
179
|
priorityFieldName: readOptionalString(tracker, "priority_field", env),
|
|
175
|
-
blockerCheckStates
|
|
180
|
+
blockerCheckStates,
|
|
181
|
+
planningStates
|
|
176
182
|
},
|
|
177
183
|
polling: {
|
|
178
184
|
intervalMs: readOptionalIntegerLike(polling, "interval_ms") ?? DEFAULT_POLL_INTERVAL_MS
|
|
@@ -201,7 +207,8 @@ function parseWorkflowMarkdown(markdown, env = process.env, options = {}) {
|
|
|
201
207
|
stateFieldName: readOptionalString(tracker, "state_field", env) ?? DEFAULT_WORKFLOW_TRACKER.stateFieldName,
|
|
202
208
|
activeStates,
|
|
203
209
|
terminalStates,
|
|
204
|
-
blockerCheckStates
|
|
210
|
+
blockerCheckStates,
|
|
211
|
+
planningStates
|
|
205
212
|
},
|
|
206
213
|
format: "front-matter",
|
|
207
214
|
githubProjectId: readOptionalString(tracker, "project_id", env),
|
|
@@ -451,7 +458,9 @@ function parseScalar(value) {
|
|
|
451
458
|
return parsed;
|
|
452
459
|
}
|
|
453
460
|
} catch {
|
|
454
|
-
throw new Error(
|
|
461
|
+
throw new Error(
|
|
462
|
+
`Invalid quoted workflow front matter scalar "${value}".`
|
|
463
|
+
);
|
|
455
464
|
}
|
|
456
465
|
}
|
|
457
466
|
if (value.startsWith("'") && value.endsWith("'")) {
|
|
@@ -880,7 +889,8 @@ var SESSION_EXIT_CLASSIFICATIONS = [
|
|
|
880
889
|
"max-turns-reached",
|
|
881
890
|
"user-input-required",
|
|
882
891
|
"timeout",
|
|
883
|
-
"error"
|
|
892
|
+
"error",
|
|
893
|
+
"incomplete-turn-dirty-workspace"
|
|
884
894
|
];
|
|
885
895
|
function isSessionExitClassification(value) {
|
|
886
896
|
return typeof value === "string" && SESSION_EXIT_CLASSIFICATIONS.includes(value);
|
|
@@ -1462,11 +1472,30 @@ function buildProjectSnapshot(input) {
|
|
|
1462
1472
|
retryKind: run.retryKind ?? "failure",
|
|
1463
1473
|
nextRetryAt: run.nextRetryAt
|
|
1464
1474
|
})),
|
|
1475
|
+
recovery: findLatestRecovery([...allRuns ?? [], ...activeRuns]),
|
|
1465
1476
|
lastError,
|
|
1466
1477
|
codexTotals: aggregateTokenUsage(allRuns ?? activeRuns, lastTickAt),
|
|
1467
1478
|
rateLimits: rateLimits ?? null
|
|
1468
1479
|
};
|
|
1469
1480
|
}
|
|
1481
|
+
function findLatestRecovery(runs) {
|
|
1482
|
+
return [...runs].filter((run) => isUnresolvedRecoveryRun(run, runs)).sort((left, right) => {
|
|
1483
|
+
const leftTime = new Date(left.updatedAt).getTime();
|
|
1484
|
+
const rightTime = new Date(right.updatedAt).getTime();
|
|
1485
|
+
return rightTime - leftTime;
|
|
1486
|
+
}).find((run) => run.recovery)?.recovery ?? null;
|
|
1487
|
+
}
|
|
1488
|
+
function isUnresolvedRecoveryRun(run, runs) {
|
|
1489
|
+
if (!run.recovery) {
|
|
1490
|
+
return false;
|
|
1491
|
+
}
|
|
1492
|
+
if (run.status === "suppressed" && runs.some(
|
|
1493
|
+
(candidate) => candidate.runId !== run.runId && candidate.retryKind === "recovery" && candidate.recovery?.runId === run.recovery?.runId && new Date(candidate.updatedAt).getTime() > new Date(run.updatedAt).getTime() && candidate.status !== "running" && candidate.status !== "retrying"
|
|
1494
|
+
)) {
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
return run.status === "suppressed" || run.retryKind === "recovery" && (run.status === "running" || run.status === "retrying");
|
|
1498
|
+
}
|
|
1470
1499
|
function aggregateTokenUsageByIssue(runs) {
|
|
1471
1500
|
const totals = /* @__PURE__ */ new Map();
|
|
1472
1501
|
for (const run of runs) {
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
resolveWorkflowRuntimeTimeouts,
|
|
31
31
|
safeReadDir,
|
|
32
32
|
scheduleRetryAt
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-6OPRRC2J.js";
|
|
34
34
|
import {
|
|
35
35
|
loadGlobalConfig,
|
|
36
36
|
loadProjectConfig
|
|
@@ -2313,14 +2313,21 @@ async function syncRepositoryForRun(input) {
|
|
|
2313
2313
|
});
|
|
2314
2314
|
}
|
|
2315
2315
|
async function ensureIssueWorkspaceRepository(input) {
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2316
|
+
let dirtyExistingWorkspaceAllowed = false;
|
|
2317
|
+
const repositoryDirectory = input.existingWorkspace ? await syncExistingIssueWorkspaceRepository(
|
|
2318
|
+
{
|
|
2319
|
+
...input,
|
|
2320
|
+
skipPull: Boolean(input.pullRequestBranch),
|
|
2321
|
+
allowDirty: input.allowDirtyExistingWorkspace
|
|
2322
|
+
},
|
|
2323
|
+
(dirtyAllowed) => {
|
|
2324
|
+
dirtyExistingWorkspaceAllowed = dirtyAllowed;
|
|
2325
|
+
}
|
|
2326
|
+
) : await cloneRepositoryForRun({
|
|
2320
2327
|
repository: input.repository,
|
|
2321
2328
|
targetDirectory: input.issueWorkspacePath
|
|
2322
2329
|
});
|
|
2323
|
-
if (input.pullRequestBranch) {
|
|
2330
|
+
if (input.pullRequestBranch && !dirtyExistingWorkspaceAllowed) {
|
|
2324
2331
|
await checkoutPullRequestBranch(
|
|
2325
2332
|
repositoryDirectory,
|
|
2326
2333
|
input.pullRequestBranch
|
|
@@ -2328,6 +2335,20 @@ async function ensureIssueWorkspaceRepository(input) {
|
|
|
2328
2335
|
}
|
|
2329
2336
|
return repositoryDirectory;
|
|
2330
2337
|
}
|
|
2338
|
+
async function inspectIssueWorkspaceDirtyStatus(input) {
|
|
2339
|
+
const repositoryDirectory = join(input.issueWorkspacePath, "repository");
|
|
2340
|
+
const hasGit = await pathExists(join(repositoryDirectory, ".git"));
|
|
2341
|
+
if (!hasGit) {
|
|
2342
|
+
return null;
|
|
2343
|
+
}
|
|
2344
|
+
const status = await readGitStatusPorcelain(repositoryDirectory);
|
|
2345
|
+
return {
|
|
2346
|
+
repositoryDirectory,
|
|
2347
|
+
dirty: status.trim().length > 0,
|
|
2348
|
+
dirtyFiles: parseGitStatusFiles(status),
|
|
2349
|
+
summary: status.trim() ? summarizeGitStatus(status) : null
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2331
2352
|
async function loadRepositoryWorkflow(repositoryDirectory, _repository) {
|
|
2332
2353
|
const workflowPath = join(repositoryDirectory, "WORKFLOW.md");
|
|
2333
2354
|
try {
|
|
@@ -2377,7 +2398,7 @@ async function readGitHead(repositoryDirectory) {
|
|
|
2377
2398
|
return null;
|
|
2378
2399
|
}
|
|
2379
2400
|
}
|
|
2380
|
-
async function syncExistingIssueWorkspaceRepository(input) {
|
|
2401
|
+
async function syncExistingIssueWorkspaceRepository(input, onDirtyAllowed) {
|
|
2381
2402
|
await mkdir(input.issueWorkspacePath, { recursive: true });
|
|
2382
2403
|
const repositoryDirectory = join(input.issueWorkspacePath, "repository");
|
|
2383
2404
|
const lockDirectory = join(input.issueWorkspacePath, "repository.lock");
|
|
@@ -2394,6 +2415,10 @@ async function syncExistingIssueWorkspaceRepository(input) {
|
|
|
2394
2415
|
`could not be inspected: ${formatCommandError(error, "git status --porcelain failed")}`
|
|
2395
2416
|
);
|
|
2396
2417
|
}
|
|
2418
|
+
if (dirtyStatus.trim() && input.allowDirty) {
|
|
2419
|
+
onDirtyAllowed?.(true);
|
|
2420
|
+
return repositoryDirectory;
|
|
2421
|
+
}
|
|
2397
2422
|
if (dirtyStatus.trim()) {
|
|
2398
2423
|
throw createIssueWorkspacePreservedError(
|
|
2399
2424
|
repositoryDirectory,
|
|
@@ -2537,6 +2562,9 @@ function summarizeGitStatus(status) {
|
|
|
2537
2562
|
const summary = lines.slice(0, 5).join("; ");
|
|
2538
2563
|
return lines.length > 5 ? `${summary}; ...` : summary;
|
|
2539
2564
|
}
|
|
2565
|
+
function parseGitStatusFiles(status) {
|
|
2566
|
+
return status.trim().split(/\r?\n/).map((line) => line.slice(3).trim()).filter(Boolean);
|
|
2567
|
+
}
|
|
2540
2568
|
function normalizeWhitespace(value) {
|
|
2541
2569
|
return value.replace(/\s+/g, " ").trim();
|
|
2542
2570
|
}
|
|
@@ -3207,6 +3235,25 @@ function explainRuntimeOwnership(issue, issueRecords, runs) {
|
|
|
3207
3235
|
};
|
|
3208
3236
|
}
|
|
3209
3237
|
}
|
|
3238
|
+
if (latestRun?.status === "suppressed" && latestRun.recovery?.kind === "incomplete-turn-dirty-workspace") {
|
|
3239
|
+
return {
|
|
3240
|
+
id: "runtime_ownership",
|
|
3241
|
+
status: "warn",
|
|
3242
|
+
message: "Latest run has a recoverable incomplete-turn dirty workspace; dispatch will start a recovery turn.",
|
|
3243
|
+
details: {
|
|
3244
|
+
runId: latestRun.recovery.runId,
|
|
3245
|
+
issueId: latestRun.recovery.issueId,
|
|
3246
|
+
workspacePath: latestRun.recovery.workspacePath,
|
|
3247
|
+
dirtyFiles: latestRun.recovery.dirtyFiles,
|
|
3248
|
+
lastEvent: latestRun.recovery.lastEvent,
|
|
3249
|
+
lastEventAt: latestRun.recovery.lastEventAt,
|
|
3250
|
+
sessionId: latestRun.recovery.sessionId,
|
|
3251
|
+
threadId: latestRun.recovery.threadId,
|
|
3252
|
+
suggestedCommand: latestRun.recovery.suggestedCommand
|
|
3253
|
+
},
|
|
3254
|
+
hint: latestRun.recovery.suggestedCommand
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3210
3257
|
return {
|
|
3211
3258
|
id: "runtime_ownership",
|
|
3212
3259
|
status: "pass",
|
|
@@ -3311,6 +3358,7 @@ var CONTINUATION_RETRY_DELAY_MS = 1e3;
|
|
|
3311
3358
|
var DEFAULT_WORKER_COMMAND = "node packages/worker/dist/index.js";
|
|
3312
3359
|
var DEFAULT_MAX_NONPRODUCTIVE_TURNS = 3;
|
|
3313
3360
|
var LOW_RATE_LIMIT_WARNING_THRESHOLD = 0.05;
|
|
3361
|
+
var MAX_RECOVERY_DIRTY_FILES_IN_CONTEXT = 50;
|
|
3314
3362
|
var MAX_FAILURE_RETRIES_EXCEEDED_REASON2 = "max_failure_retries_exceeded";
|
|
3315
3363
|
var LINKED_PR_ACTIVE_ISSUE_INACTIVE_MARKER_PREFIX = "gh-symphony:linked-pr-active-while-issue-inactive";
|
|
3316
3364
|
var STUCK_WORKER_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
@@ -3556,6 +3604,7 @@ var OrchestratorService = class {
|
|
|
3556
3604
|
kind: currentRun?.retryKind ?? null,
|
|
3557
3605
|
error: currentRun?.lastError ?? issueRecord.retryEntry?.error ?? null
|
|
3558
3606
|
} : null,
|
|
3607
|
+
recovery: currentRun?.recovery ?? null,
|
|
3559
3608
|
logs: {
|
|
3560
3609
|
codex_session_logs: currentRun === null ? [] : [
|
|
3561
3610
|
{
|
|
@@ -3786,6 +3835,12 @@ var OrchestratorService = class {
|
|
|
3786
3835
|
},
|
|
3787
3836
|
issue.identifier
|
|
3788
3837
|
);
|
|
3838
|
+
const recoveryContext = await this.resolveIncompleteTurnRecoveryContext(
|
|
3839
|
+
tenant,
|
|
3840
|
+
issue,
|
|
3841
|
+
latestRunsByIssueId.get(issue.id) ?? null,
|
|
3842
|
+
preferredWorkspaceKey
|
|
3843
|
+
);
|
|
3789
3844
|
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
3790
3845
|
issueId: issue.id,
|
|
3791
3846
|
identifier: issue.identifier,
|
|
@@ -3798,7 +3853,9 @@ var OrchestratorService = class {
|
|
|
3798
3853
|
});
|
|
3799
3854
|
let run;
|
|
3800
3855
|
try {
|
|
3801
|
-
run = await this.startRun(tenant, issue
|
|
3856
|
+
run = await this.startRun(tenant, issue, {
|
|
3857
|
+
recovery: recoveryContext
|
|
3858
|
+
});
|
|
3802
3859
|
} catch (error) {
|
|
3803
3860
|
issueRecords = releaseIssueOrchestration(issueRecords, issue.id, now);
|
|
3804
3861
|
throw error;
|
|
@@ -3855,6 +3912,11 @@ var OrchestratorService = class {
|
|
|
3855
3912
|
const activeWorkerPid = activeRun.processId;
|
|
3856
3913
|
this.sendSignal(activeWorkerPid, "SIGTERM");
|
|
3857
3914
|
this.retireWorkerPid(activeWorkerPid);
|
|
3915
|
+
const recovery = await this.classifyIncompleteTurnDirtyWorkspace(
|
|
3916
|
+
tenant,
|
|
3917
|
+
activeRun,
|
|
3918
|
+
now
|
|
3919
|
+
);
|
|
3858
3920
|
const suppressedRun = {
|
|
3859
3921
|
...activeRun,
|
|
3860
3922
|
status: "suppressed",
|
|
@@ -3862,7 +3924,17 @@ var OrchestratorService = class {
|
|
|
3862
3924
|
completedAt: now.toISOString(),
|
|
3863
3925
|
updatedAt: now.toISOString(),
|
|
3864
3926
|
runPhase: "canceled_by_reconciliation",
|
|
3865
|
-
|
|
3927
|
+
runtimeSession: recovery ? buildRuntimeSession(
|
|
3928
|
+
activeRun.runtimeSession,
|
|
3929
|
+
recovery.sessionId,
|
|
3930
|
+
recovery.threadId,
|
|
3931
|
+
"completed",
|
|
3932
|
+
activeRun.runtimeSession?.startedAt ?? activeRun.startedAt ?? now.toISOString(),
|
|
3933
|
+
now.toISOString(),
|
|
3934
|
+
"incomplete-turn-dirty-workspace"
|
|
3935
|
+
) : activeRun.runtimeSession,
|
|
3936
|
+
recovery,
|
|
3937
|
+
lastError: recovery ? "Run suppressed with recoverable incomplete-turn dirty workspace." : "Run suppressed because the tracker issue is no longer tracked."
|
|
3866
3938
|
};
|
|
3867
3939
|
await this.store.saveRun(suppressedRun);
|
|
3868
3940
|
this.logVerbose(
|
|
@@ -3888,6 +3960,11 @@ var OrchestratorService = class {
|
|
|
3888
3960
|
}
|
|
3889
3961
|
if (activeRun) {
|
|
3890
3962
|
const terminalState = isStateTerminal(issue.state, lifecycle);
|
|
3963
|
+
const recovery = terminalState ? null : await this.classifyIncompleteTurnDirtyWorkspace(
|
|
3964
|
+
tenant,
|
|
3965
|
+
activeRun,
|
|
3966
|
+
now
|
|
3967
|
+
);
|
|
3891
3968
|
const suppressedRun = {
|
|
3892
3969
|
...activeRun,
|
|
3893
3970
|
status: "suppressed",
|
|
@@ -3895,7 +3972,17 @@ var OrchestratorService = class {
|
|
|
3895
3972
|
completedAt: now.toISOString(),
|
|
3896
3973
|
updatedAt: now.toISOString(),
|
|
3897
3974
|
runPhase: "canceled_by_reconciliation",
|
|
3898
|
-
|
|
3975
|
+
runtimeSession: recovery ? buildRuntimeSession(
|
|
3976
|
+
activeRun.runtimeSession,
|
|
3977
|
+
recovery.sessionId,
|
|
3978
|
+
recovery.threadId,
|
|
3979
|
+
"completed",
|
|
3980
|
+
activeRun.runtimeSession?.startedAt ?? activeRun.startedAt ?? now.toISOString(),
|
|
3981
|
+
now.toISOString(),
|
|
3982
|
+
"incomplete-turn-dirty-workspace"
|
|
3983
|
+
) : activeRun.runtimeSession,
|
|
3984
|
+
recovery,
|
|
3985
|
+
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
3986
|
};
|
|
3900
3987
|
await this.store.saveRun(suppressedRun);
|
|
3901
3988
|
this.logVerbose(
|
|
@@ -4156,7 +4243,8 @@ var OrchestratorService = class {
|
|
|
4156
4243
|
stateFieldName: "Status",
|
|
4157
4244
|
activeStates: ["Todo", "In Progress"],
|
|
4158
4245
|
terminalStates: ["Done"],
|
|
4159
|
-
blockerCheckStates: ["Todo"]
|
|
4246
|
+
blockerCheckStates: ["Todo"],
|
|
4247
|
+
planningStates: ["Todo"]
|
|
4160
4248
|
}
|
|
4161
4249
|
};
|
|
4162
4250
|
}
|
|
@@ -4246,7 +4334,7 @@ var OrchestratorService = class {
|
|
|
4246
4334
|
true
|
|
4247
4335
|
);
|
|
4248
4336
|
}
|
|
4249
|
-
async startRun(tenant, issue) {
|
|
4337
|
+
async startRun(tenant, issue, options = {}) {
|
|
4250
4338
|
if (this.shuttingDown || !this.running) {
|
|
4251
4339
|
throw new Error(
|
|
4252
4340
|
"Orchestrator is shutting down and cannot start new runs."
|
|
@@ -4288,7 +4376,8 @@ var OrchestratorService = class {
|
|
|
4288
4376
|
repository: issue.repository,
|
|
4289
4377
|
issueWorkspacePath,
|
|
4290
4378
|
existingWorkspace: Boolean(existingWorkspaceRecord),
|
|
4291
|
-
pullRequestBranch
|
|
4379
|
+
pullRequestBranch,
|
|
4380
|
+
allowDirtyExistingWorkspace: options.recovery?.kind === "incomplete-turn-dirty-workspace"
|
|
4292
4381
|
});
|
|
4293
4382
|
if (!existingWorkspaceRecord) {
|
|
4294
4383
|
const workspaceRecord = {
|
|
@@ -4339,9 +4428,10 @@ var OrchestratorService = class {
|
|
|
4339
4428
|
attempt: null
|
|
4340
4429
|
// first execution
|
|
4341
4430
|
});
|
|
4342
|
-
const renderedPrompt =
|
|
4431
|
+
const renderedPrompt = appendIncompleteTurnRecoveryPrompt(
|
|
4343
4432
|
workflow.promptTemplate,
|
|
4344
|
-
promptVariables
|
|
4433
|
+
promptVariables,
|
|
4434
|
+
options.recovery ?? null
|
|
4345
4435
|
);
|
|
4346
4436
|
await this.runHook(
|
|
4347
4437
|
"before_run",
|
|
@@ -4433,6 +4523,9 @@ var OrchestratorService = class {
|
|
|
4433
4523
|
SYMPHONY_CUMULATIVE_OUTPUT_TOKENS: "0",
|
|
4434
4524
|
SYMPHONY_CUMULATIVE_TOTAL_TOKENS: "0",
|
|
4435
4525
|
SYMPHONY_LAST_TURN_SUMMARY: "",
|
|
4526
|
+
SYMPHONY_RECOVERY_KIND: options.recovery?.kind ?? "",
|
|
4527
|
+
SYMPHONY_RECOVERY_DIRTY_FILES: options.recovery ? formatRecoveryDirtyFilesForContext(options.recovery.dirtyFiles) : "",
|
|
4528
|
+
SYMPHONY_RECOVERY_SUGGESTED_COMMAND: options.recovery?.suggestedCommand ?? "",
|
|
4436
4529
|
SYMPHONY_SESSION_STARTED_AT: "",
|
|
4437
4530
|
SYMPHONY_READ_TIMEOUT_MS: String(runtimeTimeouts.readTimeoutMs),
|
|
4438
4531
|
SYMPHONY_TURN_TIMEOUT_MS: String(runtimeTimeouts.turnTimeoutMs)
|
|
@@ -4546,7 +4639,7 @@ var OrchestratorService = class {
|
|
|
4546
4639
|
issueWorkspaceKey: workspaceKey,
|
|
4547
4640
|
workspaceRuntimeDir,
|
|
4548
4641
|
workflowPath: workflow.workflowPath,
|
|
4549
|
-
retryKind: null,
|
|
4642
|
+
retryKind: options.recovery ? "recovery" : null,
|
|
4550
4643
|
threadId: null,
|
|
4551
4644
|
cumulativeTurnCount: 0,
|
|
4552
4645
|
lastTurnSummary: null,
|
|
@@ -4557,7 +4650,8 @@ var OrchestratorService = class {
|
|
|
4557
4650
|
lastError: null,
|
|
4558
4651
|
nextRetryAt: null,
|
|
4559
4652
|
runPhase: "preparing_workspace",
|
|
4560
|
-
rateLimits: issue.rateLimits ?? null
|
|
4653
|
+
rateLimits: issue.rateLimits ?? null,
|
|
4654
|
+
recovery: options.recovery ?? null
|
|
4561
4655
|
};
|
|
4562
4656
|
}
|
|
4563
4657
|
async syncActiveRunIssueStates(tenant, trackerAdapter, activeRuns, now) {
|
|
@@ -5181,6 +5275,66 @@ var OrchestratorService = class {
|
|
|
5181
5275
|
);
|
|
5182
5276
|
return issue ? { issue, issues } : null;
|
|
5183
5277
|
}
|
|
5278
|
+
async classifyIncompleteTurnDirtyWorkspace(tenant, run, now) {
|
|
5279
|
+
if (run.runtimeSession?.status !== "active" || run.runtimeSession.exitClassification !== null || run.lastEvent === "turn_completed") {
|
|
5280
|
+
return null;
|
|
5281
|
+
}
|
|
5282
|
+
const workspaceKey = run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
5283
|
+
{
|
|
5284
|
+
adapter: tenant.tracker.adapter,
|
|
5285
|
+
issueSubjectId: run.issueSubjectId
|
|
5286
|
+
},
|
|
5287
|
+
run.issueIdentifier
|
|
5288
|
+
);
|
|
5289
|
+
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
5290
|
+
this.store.projectDir(tenant.projectId),
|
|
5291
|
+
workspaceKey
|
|
5292
|
+
);
|
|
5293
|
+
const dirtyStatus = await inspectIssueWorkspaceDirtyStatus({
|
|
5294
|
+
issueWorkspacePath
|
|
5295
|
+
});
|
|
5296
|
+
if (!dirtyStatus?.dirty) {
|
|
5297
|
+
return null;
|
|
5298
|
+
}
|
|
5299
|
+
return {
|
|
5300
|
+
kind: "incomplete-turn-dirty-workspace",
|
|
5301
|
+
runId: run.runId,
|
|
5302
|
+
issueId: run.issueId,
|
|
5303
|
+
issueIdentifier: run.issueIdentifier,
|
|
5304
|
+
workspacePath: dirtyStatus.repositoryDirectory,
|
|
5305
|
+
dirtyFiles: dirtyStatus.dirtyFiles,
|
|
5306
|
+
lastEvent: run.lastEvent ?? null,
|
|
5307
|
+
lastEventAt: run.lastEventAt ?? null,
|
|
5308
|
+
sessionId: run.runtimeSession.sessionId ?? null,
|
|
5309
|
+
threadId: run.threadId ?? run.runtimeSession.threadId ?? null,
|
|
5310
|
+
suggestedCommand: `cd ${shellQuote(dirtyStatus.repositoryDirectory)} && git status --short && git diff`,
|
|
5311
|
+
detectedAt: now.toISOString()
|
|
5312
|
+
};
|
|
5313
|
+
}
|
|
5314
|
+
async resolveIncompleteTurnRecoveryContext(tenant, issue, latestRun, preferredWorkspaceKey) {
|
|
5315
|
+
const recovery = latestRun?.recovery;
|
|
5316
|
+
if (latestRun?.status !== "suppressed" || recovery?.kind !== "incomplete-turn-dirty-workspace" || latestRun.runtimeSession?.exitClassification !== "incomplete-turn-dirty-workspace") {
|
|
5317
|
+
return null;
|
|
5318
|
+
}
|
|
5319
|
+
const workspaceKey = latestRun.issueWorkspaceKey ?? preferredWorkspaceKey;
|
|
5320
|
+
const dirtyStatus = await inspectIssueWorkspaceDirtyStatus({
|
|
5321
|
+
issueWorkspacePath: resolveIssueWorkspaceDirectory(
|
|
5322
|
+
this.store.projectDir(tenant.projectId),
|
|
5323
|
+
workspaceKey
|
|
5324
|
+
)
|
|
5325
|
+
});
|
|
5326
|
+
if (!dirtyStatus?.dirty) {
|
|
5327
|
+
return null;
|
|
5328
|
+
}
|
|
5329
|
+
return {
|
|
5330
|
+
...recovery,
|
|
5331
|
+
issueId: issue.id,
|
|
5332
|
+
issueIdentifier: issue.identifier,
|
|
5333
|
+
workspacePath: dirtyStatus.repositoryDirectory,
|
|
5334
|
+
dirtyFiles: dirtyStatus.dirtyFiles,
|
|
5335
|
+
suggestedCommand: `cd ${shellQuote(dirtyStatus.repositoryDirectory)} && git status --short && git diff`
|
|
5336
|
+
};
|
|
5337
|
+
}
|
|
5184
5338
|
async fetchWorkerRunInfo(run) {
|
|
5185
5339
|
const latestRun = await this.store.loadRun(run.runId, run.projectId) ?? run;
|
|
5186
5340
|
const persistedTokenUsage = await this.readPersistedWorkerTokenUsage(latestRun);
|
|
@@ -5757,9 +5911,50 @@ function buildRuntimeSession(existing, sessionId, threadId, status, startedAt, u
|
|
|
5757
5911
|
exitClassification: exitClassification === void 0 ? existing?.exitClassification ?? null : exitClassification
|
|
5758
5912
|
};
|
|
5759
5913
|
}
|
|
5914
|
+
function appendIncompleteTurnRecoveryPrompt(promptTemplate, promptVariables, recovery) {
|
|
5915
|
+
const renderedPrompt = renderPrompt(promptTemplate, promptVariables);
|
|
5916
|
+
if (!recovery) {
|
|
5917
|
+
return renderedPrompt;
|
|
5918
|
+
}
|
|
5919
|
+
return [
|
|
5920
|
+
renderedPrompt,
|
|
5921
|
+
"",
|
|
5922
|
+
"## Recovery Context \u2014 Incomplete Turn Dirty Workspace",
|
|
5923
|
+
"",
|
|
5924
|
+
`Previous run: ${recovery.runId}`,
|
|
5925
|
+
`Workspace: ${recovery.workspacePath}`,
|
|
5926
|
+
`Last event: ${recovery.lastEvent ?? "unknown"}`,
|
|
5927
|
+
`Last event time: ${recovery.lastEventAt ?? "unknown"}`,
|
|
5928
|
+
`Session id: ${recovery.sessionId ?? "unknown"}`,
|
|
5929
|
+
`Thread id: ${recovery.threadId ?? "unknown"}`,
|
|
5930
|
+
"",
|
|
5931
|
+
"Dirty files:",
|
|
5932
|
+
...formatRecoveryDirtyFileLinesForPrompt(recovery.dirtyFiles),
|
|
5933
|
+
"",
|
|
5934
|
+
"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.",
|
|
5935
|
+
`Suggested operator command: ${recovery.suggestedCommand}`
|
|
5936
|
+
].join("\n");
|
|
5937
|
+
}
|
|
5938
|
+
function formatRecoveryDirtyFilesForContext(dirtyFiles) {
|
|
5939
|
+
return formatRecoveryDirtyFiles(dirtyFiles).join("\n");
|
|
5940
|
+
}
|
|
5941
|
+
function formatRecoveryDirtyFileLinesForPrompt(dirtyFiles) {
|
|
5942
|
+
return formatRecoveryDirtyFiles(dirtyFiles).map((file) => `- ${file}`);
|
|
5943
|
+
}
|
|
5944
|
+
function formatRecoveryDirtyFiles(dirtyFiles) {
|
|
5945
|
+
const visibleFiles = dirtyFiles.slice(0, MAX_RECOVERY_DIRTY_FILES_IN_CONTEXT);
|
|
5946
|
+
const remaining = dirtyFiles.length - visibleFiles.length;
|
|
5947
|
+
if (remaining <= 0) {
|
|
5948
|
+
return visibleFiles;
|
|
5949
|
+
}
|
|
5950
|
+
return [...visibleFiles, `... and ${remaining} more`];
|
|
5951
|
+
}
|
|
5760
5952
|
function resolvePersistedCumulativeTurnCount(run) {
|
|
5761
5953
|
return run.cumulativeTurnCount ?? run.turnCount ?? 0;
|
|
5762
5954
|
}
|
|
5955
|
+
function shellQuote(value) {
|
|
5956
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
5957
|
+
}
|
|
5763
5958
|
function resolveCumulativeTurnCount(run, turnCount) {
|
|
5764
5959
|
const carriedTotal = resolvePersistedCumulativeTurnCount(run);
|
|
5765
5960
|
if (turnCount === null) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
workflow_init_default
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ZPS4CQZJ.js";
|
|
5
5
|
import {
|
|
6
6
|
fetchGithubProjectIssueByRepositoryAndNumber,
|
|
7
7
|
inspectManagedProjectSelection,
|
|
8
8
|
resolveTrackerAdapter
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-B6G3KGBB.js";
|
|
10
10
|
import {
|
|
11
11
|
GitHubApiError,
|
|
12
12
|
createClient,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
buildPromptVariables,
|
|
20
20
|
parseWorkflowMarkdown,
|
|
21
21
|
renderPrompt
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-6OPRRC2J.js";
|
|
23
23
|
import {
|
|
24
24
|
loadActiveProjectConfig
|
|
25
25
|
} from "./chunk-4ICDSQCJ.js";
|
|
@@ -705,7 +705,9 @@ async function loadLinearIssue(issueIdentifier, workflow, options) {
|
|
|
705
705
|
repository: projectConfig.repository,
|
|
706
706
|
tracker: projectConfig.tracker
|
|
707
707
|
};
|
|
708
|
-
const trackerAdapter = workflowCommandDependencies.resolveTrackerAdapter(
|
|
708
|
+
const trackerAdapter = workflowCommandDependencies.resolveTrackerAdapter(
|
|
709
|
+
projectConfig.tracker
|
|
710
|
+
);
|
|
709
711
|
const [issue] = await trackerAdapter.fetchIssueStatesByIds(
|
|
710
712
|
orchestratorProject,
|
|
711
713
|
[issueIdentifier.trim().toUpperCase()],
|
|
@@ -752,6 +754,7 @@ function validateWorkflow(workflowPath, markdown) {
|
|
|
752
754
|
activeStates: workflow.lifecycle.activeStates,
|
|
753
755
|
terminalStates: workflow.lifecycle.terminalStates,
|
|
754
756
|
blockerCheckStates: workflow.lifecycle.blockerCheckStates,
|
|
757
|
+
planningStates: workflow.lifecycle.planningStates,
|
|
755
758
|
pollingIntervalMs: workflow.polling.intervalMs,
|
|
756
759
|
workspaceRoot: workflow.workspace.root,
|
|
757
760
|
agentCommand: workflow.agentCommand,
|
|
@@ -791,6 +794,7 @@ Lifecycle
|
|
|
791
794
|
active_states=${report.summary.activeStates.join(", ") || "(none)"}
|
|
792
795
|
terminal_states=${report.summary.terminalStates.join(", ") || "(none)"}
|
|
793
796
|
blocker_check_states=${report.summary.blockerCheckStates.join(", ") || "(none)"}
|
|
797
|
+
planning_states=${report.summary.planningStates.join(", ") || "(none)"}
|
|
794
798
|
|
|
795
799
|
Runtime
|
|
796
800
|
polling.interval_ms=${report.summary.pollingIntervalMs}
|