@gh-symphony/cli 0.3.0 → 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 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
 
@@ -889,7 +889,8 @@ var SESSION_EXIT_CLASSIFICATIONS = [
889
889
  "max-turns-reached",
890
890
  "user-input-required",
891
891
  "timeout",
892
- "error"
892
+ "error",
893
+ "incomplete-turn-dirty-workspace"
893
894
  ];
894
895
  function isSessionExitClassification(value) {
895
896
  return typeof value === "string" && SESSION_EXIT_CLASSIFICATIONS.includes(value);
@@ -1471,11 +1472,30 @@ function buildProjectSnapshot(input) {
1471
1472
  retryKind: run.retryKind ?? "failure",
1472
1473
  nextRetryAt: run.nextRetryAt
1473
1474
  })),
1475
+ recovery: findLatestRecovery([...allRuns ?? [], ...activeRuns]),
1474
1476
  lastError,
1475
1477
  codexTotals: aggregateTokenUsage(allRuns ?? activeRuns, lastTickAt),
1476
1478
  rateLimits: rateLimits ?? null
1477
1479
  };
1478
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
+ }
1479
1499
  function aggregateTokenUsageByIssue(runs) {
1480
1500
  const totals = /* @__PURE__ */ new Map();
1481
1501
  for (const run of runs) {
@@ -30,7 +30,7 @@ import {
30
30
  resolveWorkflowRuntimeTimeouts,
31
31
  safeReadDir,
32
32
  scheduleRetryAt
33
- } from "./chunk-5HQLPZR5.js";
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
- const repositoryDirectory = input.existingWorkspace ? await syncExistingIssueWorkspaceRepository({
2317
- ...input,
2318
- skipPull: Boolean(input.pullRequestBranch)
2319
- }) : await cloneRepositoryForRun({
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
- lastError: "Run suppressed because the tracker issue is no longer tracked."
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
- lastError: terminalState ? "Run suppressed because the tracker issue moved to a terminal state." : "Run suppressed because the tracker state is no longer actionable."
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(
@@ -4247,7 +4334,7 @@ var OrchestratorService = class {
4247
4334
  true
4248
4335
  );
4249
4336
  }
4250
- async startRun(tenant, issue) {
4337
+ async startRun(tenant, issue, options = {}) {
4251
4338
  if (this.shuttingDown || !this.running) {
4252
4339
  throw new Error(
4253
4340
  "Orchestrator is shutting down and cannot start new runs."
@@ -4289,7 +4376,8 @@ var OrchestratorService = class {
4289
4376
  repository: issue.repository,
4290
4377
  issueWorkspacePath,
4291
4378
  existingWorkspace: Boolean(existingWorkspaceRecord),
4292
- pullRequestBranch
4379
+ pullRequestBranch,
4380
+ allowDirtyExistingWorkspace: options.recovery?.kind === "incomplete-turn-dirty-workspace"
4293
4381
  });
4294
4382
  if (!existingWorkspaceRecord) {
4295
4383
  const workspaceRecord = {
@@ -4340,9 +4428,10 @@ var OrchestratorService = class {
4340
4428
  attempt: null
4341
4429
  // first execution
4342
4430
  });
4343
- const renderedPrompt = renderPrompt(
4431
+ const renderedPrompt = appendIncompleteTurnRecoveryPrompt(
4344
4432
  workflow.promptTemplate,
4345
- promptVariables
4433
+ promptVariables,
4434
+ options.recovery ?? null
4346
4435
  );
4347
4436
  await this.runHook(
4348
4437
  "before_run",
@@ -4434,6 +4523,9 @@ var OrchestratorService = class {
4434
4523
  SYMPHONY_CUMULATIVE_OUTPUT_TOKENS: "0",
4435
4524
  SYMPHONY_CUMULATIVE_TOTAL_TOKENS: "0",
4436
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 ?? "",
4437
4529
  SYMPHONY_SESSION_STARTED_AT: "",
4438
4530
  SYMPHONY_READ_TIMEOUT_MS: String(runtimeTimeouts.readTimeoutMs),
4439
4531
  SYMPHONY_TURN_TIMEOUT_MS: String(runtimeTimeouts.turnTimeoutMs)
@@ -4547,7 +4639,7 @@ var OrchestratorService = class {
4547
4639
  issueWorkspaceKey: workspaceKey,
4548
4640
  workspaceRuntimeDir,
4549
4641
  workflowPath: workflow.workflowPath,
4550
- retryKind: null,
4642
+ retryKind: options.recovery ? "recovery" : null,
4551
4643
  threadId: null,
4552
4644
  cumulativeTurnCount: 0,
4553
4645
  lastTurnSummary: null,
@@ -4558,7 +4650,8 @@ var OrchestratorService = class {
4558
4650
  lastError: null,
4559
4651
  nextRetryAt: null,
4560
4652
  runPhase: "preparing_workspace",
4561
- rateLimits: issue.rateLimits ?? null
4653
+ rateLimits: issue.rateLimits ?? null,
4654
+ recovery: options.recovery ?? null
4562
4655
  };
4563
4656
  }
4564
4657
  async syncActiveRunIssueStates(tenant, trackerAdapter, activeRuns, now) {
@@ -5182,6 +5275,66 @@ var OrchestratorService = class {
5182
5275
  );
5183
5276
  return issue ? { issue, issues } : null;
5184
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
+ }
5185
5338
  async fetchWorkerRunInfo(run) {
5186
5339
  const latestRun = await this.store.loadRun(run.runId, run.projectId) ?? run;
5187
5340
  const persistedTokenUsage = await this.readPersistedWorkerTokenUsage(latestRun);
@@ -5758,9 +5911,50 @@ function buildRuntimeSession(existing, sessionId, threadId, status, startedAt, u
5758
5911
  exitClassification: exitClassification === void 0 ? existing?.exitClassification ?? null : exitClassification
5759
5912
  };
5760
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
+ }
5761
5952
  function resolvePersistedCumulativeTurnCount(run) {
5762
5953
  return run.cumulativeTurnCount ?? run.turnCount ?? 0;
5763
5954
  }
5955
+ function shellQuote(value) {
5956
+ return `'${value.replace(/'/g, "'\\''")}'`;
5957
+ }
5764
5958
  function resolveCumulativeTurnCount(run, turnCount) {
5765
5959
  const carriedTotal = resolvePersistedCumulativeTurnCount(run);
5766
5960
  if (turnCount === null) {
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-RZ3WO7OV.js";
5
5
  import {
6
6
  parseWorkflowMarkdown
7
- } from "./chunk-5HQLPZR5.js";
7
+ } from "./chunk-6OPRRC2J.js";
8
8
  import {
9
9
  saveGlobalConfig,
10
10
  saveProjectConfig
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  workflow_init_default
4
- } from "./chunk-GL55AKBI.js";
4
+ } from "./chunk-ZPS4CQZJ.js";
5
5
  import {
6
6
  fetchGithubProjectIssueByRepositoryAndNumber,
7
7
  inspectManagedProjectSelection,
8
8
  resolveTrackerAdapter
9
- } from "./chunk-VHEGRYK7.js";
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-5HQLPZR5.js";
22
+ } from "./chunk-6OPRRC2J.js";
23
23
  import {
24
24
  loadActiveProjectConfig
25
25
  } from "./chunk-4ICDSQCJ.js";
@@ -10,7 +10,7 @@ import {
10
10
  resolveGitHubGraphQLToken,
11
11
  shouldReuseAgentCredentialCache,
12
12
  writeAgentCredentialCache
13
- } from "./chunk-5HQLPZR5.js";
13
+ } from "./chunk-6OPRRC2J.js";
14
14
 
15
15
  // ../runtime-codex/src/runtime.ts
16
16
  import { spawn } from "child_process";
@@ -16,7 +16,7 @@ import {
16
16
  formatClaudePreflightText,
17
17
  resolveClaudeCommandBinary,
18
18
  runClaudePreflight
19
- } from "./chunk-5HQLPZR5.js";
19
+ } from "./chunk-6OPRRC2J.js";
20
20
 
21
21
  // src/mapping/smart-defaults.ts
22
22
  var ROLE_PATTERNS = [
@@ -98,7 +98,7 @@ import * as p from "@clack/prompts";
98
98
  import { spawnSync } from "child_process";
99
99
  import { createHash } from "crypto";
100
100
  import { chmod, mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
101
- import { basename, dirname as dirname2, join as join3, relative, resolve } from "path";
101
+ import { dirname as dirname3, join as join3, relative, resolve } from "path";
102
102
 
103
103
  // src/prompts/runtime-claude-constraints.ts
104
104
  var CLAUDE_RUNTIME_CONSTRAINTS_SECTION = `## Runtime Constraints
@@ -1366,7 +1366,7 @@ function resolveRoleAction(role) {
1366
1366
 
1367
1367
  // src/skills/skill-writer.ts
1368
1368
  import { mkdir as mkdir2, readFile as readFile2, rename, writeFile as writeFile2 } from "fs/promises";
1369
- import { join as join2 } from "path";
1369
+ import { dirname as dirname2, join as join2 } from "path";
1370
1370
  function normalizeRuntimeForSkills(runtime) {
1371
1371
  if (isClaudeRuntime(runtime)) {
1372
1372
  return "claude-code";
@@ -1393,10 +1393,12 @@ function buildSkillFilePlans(repoRoot, runtime, templates, context) {
1393
1393
  }
1394
1394
  return {
1395
1395
  skillsDir,
1396
- files: templates.map((template) => ({
1397
- path: join2(skillsDir, template.name, template.fileName),
1398
- content: template.generate(context)
1399
- }))
1396
+ files: templates.flatMap(
1397
+ (template) => template.files.map((file) => ({
1398
+ path: join2(skillsDir, template.name, file.relativePath),
1399
+ content: file.generate(context)
1400
+ }))
1401
+ )
1400
1402
  };
1401
1403
  }
1402
1404
 
@@ -1436,8 +1438,9 @@ function generateGhSymphonySkill(ctx) {
1436
1438
  `- \`${ctx.contextYamlPath}\` must exist (contains GitHub Project metadata)`
1437
1439
  );
1438
1440
  lines.push(
1439
- `- \`${ctx.referenceWorkflowPath}\` must exist (annotated reference template)`
1441
+ `- \`${ctx.referenceWorkflowPath}\` may exist for compatibility with older generated ecosystems`
1440
1442
  );
1443
+ lines.push("- `references/README.md` must exist beside this skill");
1441
1444
  lines.push("- `gh` CLI must be authenticated");
1442
1445
  lines.push("");
1443
1446
  lines.push("## Repository Validation Guidance");
@@ -1462,49 +1465,70 @@ function generateGhSymphonySkill(ctx) {
1462
1465
  lines.push("## Design Mode");
1463
1466
  lines.push("");
1464
1467
  lines.push(
1465
- `1. Read \`${ctx.contextYamlPath}\` to understand the project structure`
1468
+ "1. Read `WORKFLOW.md` if it exists and extract repository conventions:"
1469
+ );
1470
+ lines.push(" - test / lint / build commands from the prompt body");
1471
+ lines.push(" - lifecycle states from front matter");
1472
+ lines.push(
1473
+ "2. Read `references/README.md` for the available reference files."
1466
1474
  );
1475
+ lines.push('3. Ask the user: "What should this orchestration accomplish?"');
1476
+ lines.push(" - implement (default) \u2014 write features / fix bugs");
1477
+ lines.push(" - review \u2014 review PRs and leave comments");
1478
+ lines.push(" - maintain \u2014 dependency bumps, chores, hygiene");
1479
+ lines.push(" - custom \u2014 describe in their own words");
1467
1480
  lines.push(
1468
- `2. Read \`${ctx.referenceWorkflowPath}\` as the annotated reference`
1481
+ "4. Pick the matching `references/workflow-posture-*.md` file(s); compose multiple files when the intent spans categories."
1469
1482
  );
1470
- lines.push("3. Ask the user these key questions:");
1483
+ lines.push("5. Ask these setup questions:");
1471
1484
  lines.push(" - Which status columns should be **active** (agent works)?");
1472
1485
  lines.push(" - Which should be **wait** (agent pauses for human)?");
1473
1486
  lines.push(" - Which should be **terminal** (agent stops)?");
1474
1487
  lines.push(" - What runtime is being used? (codex / claude-code / custom)");
1475
1488
  lines.push(" - Any custom hooks needed? (after_create, before_run, etc.)");
1489
+ lines.push("6. Generate WORKFLOW.md:");
1490
+ lines.push(" - front matter from `references/workflow-schema.md`");
1476
1491
  lines.push(
1477
- "4. Generate WORKFLOW.md using the reference as a structural guide"
1492
+ " - prompt body from the selected posture file(s), adapted to actual repository commands"
1478
1493
  );
1479
- lines.push("5. Validate the generated file (see Validate Mode)");
1494
+ lines.push("7. Show a diff or preview and confirm with the user.");
1495
+ lines.push("8. Validate via `gh-symphony workflow validate`.");
1480
1496
  lines.push("");
1481
1497
  lines.push("## Refine Mode");
1482
1498
  lines.push("");
1483
1499
  lines.push("1. Read the current `WORKFLOW.md`");
1484
- lines.push(`2. Read \`${ctx.referenceWorkflowPath}\` for comparison`);
1485
- lines.push("3. Identify missing or incomplete sections:");
1500
+ lines.push(
1501
+ "2. Read `references/README.md` and select the relevant posture file(s)"
1502
+ );
1503
+ lines.push(
1504
+ "3. Compare the current prompt body against the selected posture references"
1505
+ );
1506
+ lines.push("4. Identify missing or incomplete sections:");
1486
1507
  lines.push(" - Status Map with role annotations");
1487
1508
  lines.push(" - Default Posture / Agent Instructions");
1488
1509
  lines.push(" - Guardrails section");
1489
1510
  lines.push(" - Workpad Template");
1490
1511
  lines.push(" - Step 0 routing logic");
1491
- lines.push("4. Propose improvements and apply with user confirmation");
1492
- lines.push("5. Validate the refined file");
1512
+ lines.push("5. Propose improvements and apply with user confirmation");
1513
+ lines.push("6. Validate the refined file");
1493
1514
  lines.push("");
1494
1515
  lines.push("## Validate Mode");
1495
1516
  lines.push("");
1496
1517
  lines.push("Check the WORKFLOW.md for:");
1497
1518
  lines.push("- Front matter is valid YAML");
1498
1519
  lines.push(
1499
- "- Required fields are present (see Supported Front Matter Fields)"
1520
+ "- Required fields are present (see `references/workflow-schema.md`)"
1500
1521
  );
1501
1522
  lines.push(
1502
- "- Template variables use only supported names (see Supported Template Variables)"
1523
+ "- Template variables use only supported names (see `references/workflow-schema.md`)"
1503
1524
  );
1504
1525
  lines.push("- Status Map matches the lifecycle configuration");
1505
1526
  lines.push(
1506
1527
  "- No unsupported double-brace variable patterns (only the 8 listed below are valid)"
1507
1528
  );
1529
+ lines.push(
1530
+ "- Prompt body posture is consistent with the selected `references/workflow-posture-*.md` file(s)"
1531
+ );
1508
1532
  lines.push("");
1509
1533
  lines.push("## Supported Front Matter Fields");
1510
1534
  lines.push("");
@@ -1901,22 +1925,319 @@ function generateLandSkill(_ctx) {
1901
1925
  });
1902
1926
  }
1903
1927
 
1928
+ // src/skills/templates/gh-symphony-references/readme.ts
1929
+ function generateGhSymphonyReferencesReadme(_ctx) {
1930
+ return [
1931
+ "# /gh-symphony references",
1932
+ "",
1933
+ "The /gh-symphony skill consults these files when designing or refining",
1934
+ "WORKFLOW.md.",
1935
+ "",
1936
+ "## Schema",
1937
+ "",
1938
+ "| File | What it is |",
1939
+ "| ---- | ---------- |",
1940
+ "| `workflow-schema.md` | All supported front matter fields and their types. |",
1941
+ "",
1942
+ "## Workflow prompt body postures",
1943
+ "",
1944
+ "When the user describes what the orchestration should do, pick the matching",
1945
+ "posture file(s) and use its prompt-body sections as the seed. Postures can be",
1946
+ "combined when the user's intent spans multiple categories.",
1947
+ "",
1948
+ "| File | Use when the user wants... |",
1949
+ "| ---- | -------------------------- |",
1950
+ "| `workflow-posture-implement.md` | Coding agent writes features / bug fixes (default). |",
1951
+ "| `workflow-posture-review.md` | Agent reviews PRs and leaves comments. No code writes. |",
1952
+ "| `workflow-posture-maintain.md` | Minimal-change maintenance: deps, lint sweeps, hygiene. |",
1953
+ "",
1954
+ "## Adding your own reference",
1955
+ "",
1956
+ "Drop a markdown file here with a descriptive name. The skill discovers files",
1957
+ "on each invocation; no code changes needed."
1958
+ ].join("\n");
1959
+ }
1960
+
1961
+ // src/skills/templates/gh-symphony-references/workflow-schema.ts
1962
+ function generateWorkflowSchemaReference(ctx) {
1963
+ const reference = generateReferenceWorkflow({
1964
+ runtime: ctx.runtime,
1965
+ statusColumns: ctx.statusColumns.map((column) => ({
1966
+ name: column.name,
1967
+ role: column.role
1968
+ })),
1969
+ projectId: ctx.projectId,
1970
+ priority: null,
1971
+ detectedEnvironment: ctx.detectedEnvironment
1972
+ });
1973
+ return [
1974
+ reference,
1975
+ "",
1976
+ "## Supported Template Variables",
1977
+ "",
1978
+ "Use these in the WORKFLOW.md prompt body with double-brace syntax.",
1979
+ "",
1980
+ "| Variable | Description |",
1981
+ "| -------- | ----------- |",
1982
+ "| `issue.identifier` | Issue identifier, for example `acme/platform#42`. |",
1983
+ "| `issue.title` | Issue title. |",
1984
+ "| `issue.state` | Current tracker state. |",
1985
+ "| `issue.description` | Issue body. |",
1986
+ "| `issue.url` | Issue URL. |",
1987
+ "| `issue.repository` | Repository in `owner/name` form. |",
1988
+ "| `issue.number` | Issue number. |",
1989
+ "| `attempt` | Retry attempt number, or null on the first run. |",
1990
+ "",
1991
+ "Only these variables are supported by strict-mode prompt rendering."
1992
+ ].join("\n");
1993
+ }
1994
+
1995
+ // src/skills/templates/gh-symphony-references/workflow-posture-implement.ts
1996
+ function generateWorkflowPostureImplementReference(ctx) {
1997
+ const validationGuidance = buildRepositoryValidationGuidance(
1998
+ ctx.detectedEnvironment
1999
+ );
2000
+ return [
2001
+ "# Workflow posture: implement",
2002
+ "",
2003
+ "Use this prompt-body posture when the agent should write features or fix bugs.",
2004
+ "This is the default posture and preserves the current generated WORKFLOW.md",
2005
+ "prompt-body behavior.",
2006
+ "",
2007
+ "## Agent Instructions",
2008
+ "",
2009
+ 'You are an AI coding agent working on issue `{issue.identifier}`: "`{issue.title}`".',
2010
+ "",
2011
+ "**Repository:** `{issue.repository}`",
2012
+ "**Current state:** `{issue.state}`",
2013
+ "",
2014
+ "### Task",
2015
+ "",
2016
+ "`{issue.description}`",
2017
+ "",
2018
+ "### Default Posture",
2019
+ "",
2020
+ "1. This is an unattended orchestration session. Do not ask humans for follow-up actions.",
2021
+ "2. Only abort early if there is a genuine blocker (missing required credentials or secrets).",
2022
+ '3. In your final message, report only what was completed and any blockers. Do not include "next steps".',
2023
+ "",
2024
+ "### Repository Validation Guidance",
2025
+ "",
2026
+ ...validationGuidance.map((line, index) => `${index + 1}. ${line}`),
2027
+ "",
2028
+ "### Workflow",
2029
+ "",
2030
+ "1. Read the issue description and understand the requirements.",
2031
+ "2. Explore the codebase to understand the relevant code structure.",
2032
+ "3. Implement the changes following the project's coding conventions.",
2033
+ "4. Write or update tests to cover the changes.",
2034
+ "5. Verify that all existing tests pass.",
2035
+ "6. Create a PR with a clear description of the changes.",
2036
+ "",
2037
+ "### Guardrails",
2038
+ "",
2039
+ "- Do not edit the issue body for planning or progress tracking.",
2040
+ "- If the issue is in a terminal state, do nothing and exit.",
2041
+ "- If you find out-of-scope improvements, open a separate issue rather than expanding the current scope.",
2042
+ "",
2043
+ "### Workpad Template",
2044
+ "",
2045
+ "Create a workpad comment on the issue with the following structure to track progress:",
2046
+ "",
2047
+ "```md",
2048
+ "## Workpad",
2049
+ "",
2050
+ "### Plan",
2051
+ "",
2052
+ "- [ ] 1. Task item",
2053
+ "",
2054
+ "### Acceptance Criteria",
2055
+ "",
2056
+ "- [ ] Criterion 1",
2057
+ "",
2058
+ "### Validation",
2059
+ "",
2060
+ "- [ ] Test: `command`",
2061
+ "",
2062
+ "### Notes",
2063
+ "",
2064
+ "- Progress notes",
2065
+ "```"
2066
+ ].join("\n");
2067
+ }
2068
+
2069
+ // src/skills/templates/gh-symphony-references/workflow-posture-review.ts
2070
+ function generateWorkflowPostureReviewReference(ctx) {
2071
+ const validationGuidance = buildRepositoryValidationGuidance(
2072
+ ctx.detectedEnvironment
2073
+ );
2074
+ return [
2075
+ "# Workflow posture: review",
2076
+ "",
2077
+ "Use this prompt-body posture when the agent should review PRs and leave",
2078
+ "comments. This posture is read-only for repository code.",
2079
+ "",
2080
+ "## Agent Instructions",
2081
+ "",
2082
+ 'You are an AI code-review agent working on issue `{issue.identifier}`: "`{issue.title}`".',
2083
+ "",
2084
+ "**Repository:** `{issue.repository}`",
2085
+ "**Current state:** `{issue.state}`",
2086
+ "",
2087
+ "### Task",
2088
+ "",
2089
+ "`{issue.description}`",
2090
+ "",
2091
+ "### Default Posture",
2092
+ "",
2093
+ "1. Review linked pull requests. Do NOT write code, push commits, or open new PRs.",
2094
+ "2. Treat failing required tests as grounds to request changes unless the failure is clearly unrelated and documented.",
2095
+ "3. In your final message, report only the review outcome and any blockers. Do not include follow-up work for the human unless it is required to unblock review.",
2096
+ "",
2097
+ "### Repository Validation Guidance",
2098
+ "",
2099
+ ...validationGuidance.map((line, index) => `${index + 1}. ${line}`),
2100
+ "",
2101
+ "### Workflow",
2102
+ "",
2103
+ "1. Find the PR linked from the issue, project item, or issue timeline.",
2104
+ "2. Read the PR title, body, diff, linked issue, existing reviews, inline comments, and check status.",
2105
+ "3. Run the repository's relevant tests, lint, typecheck, or build commands when available and practical.",
2106
+ "4. Leave inline review comments for concrete, actionable findings.",
2107
+ "5. Submit a summary review: approve only when the change is correct and validation is acceptable; otherwise request changes.",
2108
+ "",
2109
+ "### Guardrails",
2110
+ "",
2111
+ "- Never push code from this posture.",
2112
+ "- Never approve PRs that introduce new dependencies without explicitly noting the dependency risk and why it is acceptable.",
2113
+ "- If relevant tests fail and the failure is not proven unrelated, request changes.",
2114
+ "- Keep comments specific to correctness, maintainability, tests, security, and issue fit.",
2115
+ "- Do not create a workpad; the review threads on the PR are the audit trail."
2116
+ ].join("\n");
2117
+ }
2118
+
2119
+ // src/skills/templates/gh-symphony-references/workflow-posture-maintain.ts
2120
+ function generateWorkflowPostureMaintainReference(ctx) {
2121
+ const validationGuidance = buildRepositoryValidationGuidance(
2122
+ ctx.detectedEnvironment
2123
+ );
2124
+ return [
2125
+ "# Workflow posture: maintain",
2126
+ "",
2127
+ "Use this prompt-body posture for low-risk maintenance such as dependency",
2128
+ "bumps, lint sweeps, small chores, and repository hygiene.",
2129
+ "",
2130
+ "## Agent Instructions",
2131
+ "",
2132
+ 'You are a maintenance coding agent working on issue `{issue.identifier}`: "`{issue.title}`".',
2133
+ "",
2134
+ "**Repository:** `{issue.repository}`",
2135
+ "**Current state:** `{issue.state}`",
2136
+ "",
2137
+ "### Task",
2138
+ "",
2139
+ "`{issue.description}`",
2140
+ "",
2141
+ "### Default Posture",
2142
+ "",
2143
+ "1. Make the smallest possible change that satisfies the maintenance request.",
2144
+ "2. Defer human-judgment calls instead of broadening scope.",
2145
+ "3. In your final message, report only what was completed and any blockers. Do not include optional next steps.",
2146
+ "",
2147
+ "### Repository Validation Guidance",
2148
+ "",
2149
+ ...validationGuidance.map((line, index) => `${index + 1}. ${line}`),
2150
+ "",
2151
+ "### Workflow",
2152
+ "",
2153
+ "1. Identify the exact maintenance task and affected files.",
2154
+ "2. Make the minimal change needed; avoid drive-by refactors.",
2155
+ "3. Run the relevant tests, lint, typecheck, or build commands for the affected area.",
2156
+ "4. Create a PR when the change is complete, or exit with a blocker note if approval is required.",
2157
+ "",
2158
+ "### Guardrails",
2159
+ "",
2160
+ "- Do not perform major dependency bumps without explicit approval.",
2161
+ "- Do not delete files without confirmation unless the issue explicitly requests it.",
2162
+ "- If the implementation exceeds 50 lines of non-generated code, stop and ask for human confirmation before continuing.",
2163
+ "- Do not mix unrelated cleanup into the maintenance change.",
2164
+ "",
2165
+ "### Workpad Template",
2166
+ "",
2167
+ "Create a compact workpad comment on the issue with the following structure:",
2168
+ "",
2169
+ "```md",
2170
+ "## Workpad",
2171
+ "",
2172
+ "### Plan",
2173
+ "",
2174
+ "- [ ] Minimal maintenance change",
2175
+ "- [ ] Validation and PR handoff",
2176
+ "",
2177
+ "### Validation",
2178
+ "",
2179
+ "- [ ] Test/lint/typecheck/build command",
2180
+ "",
2181
+ "### Blockers",
2182
+ "",
2183
+ "None",
2184
+ "```"
2185
+ ].join("\n");
2186
+ }
2187
+
2188
+ // src/skills/templates/gh-symphony-references/index.ts
2189
+ var GH_SYMPHONY_REFERENCE_FILES = [
2190
+ {
2191
+ relativePath: "references/README.md",
2192
+ generate: generateGhSymphonyReferencesReadme
2193
+ },
2194
+ {
2195
+ relativePath: "references/workflow-schema.md",
2196
+ generate: generateWorkflowSchemaReference
2197
+ },
2198
+ {
2199
+ relativePath: "references/workflow-posture-implement.md",
2200
+ generate: generateWorkflowPostureImplementReference
2201
+ },
2202
+ {
2203
+ relativePath: "references/workflow-posture-review.md",
2204
+ generate: generateWorkflowPostureReviewReference
2205
+ },
2206
+ {
2207
+ relativePath: "references/workflow-posture-maintain.md",
2208
+ generate: generateWorkflowPostureMaintainReference
2209
+ }
2210
+ ];
2211
+
1904
2212
  // src/skills/templates/index.ts
1905
2213
  var ALL_SKILL_TEMPLATES = [
1906
2214
  {
1907
2215
  name: "gh-symphony",
1908
- fileName: "SKILL.md",
1909
- generate: generateGhSymphonySkill
2216
+ files: [
2217
+ { relativePath: "SKILL.md", generate: generateGhSymphonySkill },
2218
+ ...GH_SYMPHONY_REFERENCE_FILES
2219
+ ]
1910
2220
  },
1911
2221
  {
1912
2222
  name: "gh-project",
1913
- fileName: "SKILL.md",
1914
- generate: generateGhProjectSkill
2223
+ files: [{ relativePath: "SKILL.md", generate: generateGhProjectSkill }]
1915
2224
  },
1916
- { name: "commit", fileName: "SKILL.md", generate: generateCommitSkill },
1917
- { name: "push", fileName: "SKILL.md", generate: generatePushSkill },
1918
- { name: "pull", fileName: "SKILL.md", generate: generatePullSkill },
1919
- { name: "land", fileName: "SKILL.md", generate: generateLandSkill }
2225
+ {
2226
+ name: "commit",
2227
+ files: [{ relativePath: "SKILL.md", generate: generateCommitSkill }]
2228
+ },
2229
+ {
2230
+ name: "push",
2231
+ files: [{ relativePath: "SKILL.md", generate: generatePushSkill }]
2232
+ },
2233
+ {
2234
+ name: "pull",
2235
+ files: [{ relativePath: "SKILL.md", generate: generatePullSkill }]
2236
+ },
2237
+ {
2238
+ name: "land",
2239
+ files: [{ relativePath: "SKILL.md", generate: generateLandSkill }]
2240
+ }
1920
2241
  ];
1921
2242
 
1922
2243
  // src/commands/workflow-init.ts
@@ -2112,7 +2433,7 @@ async function writePlannedFile(file) {
2112
2433
  if (file.status === "unchanged") {
2113
2434
  return false;
2114
2435
  }
2115
- await mkdir3(dirname2(file.path), { recursive: true });
2436
+ await mkdir3(dirname3(file.path), { recursive: true });
2116
2437
  const temporaryPath = `${file.path}.tmp`;
2117
2438
  await writeFile3(temporaryPath, file.content, "utf8");
2118
2439
  await rename2(temporaryPath, file.path);
@@ -2121,6 +2442,9 @@ async function writePlannedFile(file) {
2121
2442
  }
2122
2443
  return true;
2123
2444
  }
2445
+ function skillNameForPath(skillsDir, filePath) {
2446
+ return relative(skillsDir, filePath).split(/[\\/]/)[0] ?? "";
2447
+ }
2124
2448
  function resolveStatusField(projectDetail) {
2125
2449
  return projectDetail.statusFields.find((f) => f.name.toLowerCase() === "status") ?? projectDetail.statusFields[0] ?? null;
2126
2450
  }
@@ -2543,10 +2867,11 @@ async function planEcosystem(opts) {
2543
2867
  }
2544
2868
  );
2545
2869
  for (const plannedSkill of plannedSkills) {
2870
+ const skillName = skillNameForPath(skillsDir, plannedSkill.path);
2546
2871
  files.push(
2547
2872
  await planFileChange({
2548
2873
  path: plannedSkill.path,
2549
- label: `Skill ${basename(dirname2(plannedSkill.path))}`,
2874
+ label: `Skill ${skillName}`,
2550
2875
  content: plannedSkill.content,
2551
2876
  mode: "create-only"
2552
2877
  })
@@ -2596,7 +2921,7 @@ async function writeEcosystem(opts) {
2596
2921
  continue;
2597
2922
  }
2598
2923
  if (file.label.startsWith("Skill ")) {
2599
- const skillName = basename(dirname2(file.path));
2924
+ const skillName = file.label.slice("Skill ".length);
2600
2925
  if (written) {
2601
2926
  skillsWritten.push(skillName);
2602
2927
  } else {
@@ -2616,8 +2941,8 @@ async function writeEcosystem(opts) {
2616
2941
  afterCreateHookWritten,
2617
2942
  contextYamlWritten,
2618
2943
  referenceWorkflowWritten,
2619
- skillsWritten: skillsWritten.sort(),
2620
- skillsSkipped: skillsSkipped.sort()
2944
+ skillsWritten: [...new Set(skillsWritten)].sort(),
2945
+ skillsSkipped: [...new Set(skillsSkipped)].sort()
2621
2946
  };
2622
2947
  }
2623
2948
  function formatPrioritySummaryLines(priority) {
@@ -5,14 +5,14 @@ import {
5
5
  parseIssueReference,
6
6
  readGitHubProjectBinding,
7
7
  renderIssueWorkflowPreview
8
- } from "./chunk-GTNWFVFU.js";
9
- import "./chunk-GL55AKBI.js";
8
+ } from "./chunk-TTVGBHZI.js";
9
+ import "./chunk-ZPS4CQZJ.js";
10
10
  import {
11
11
  fetchGithubProjectIssueByRepositoryAndNumber,
12
12
  fetchGithubProjectIssues,
13
13
  inspectManagedProjectSelection
14
- } from "./chunk-VHEGRYK7.js";
15
- import "./chunk-A7EFL6EJ.js";
14
+ } from "./chunk-B6G3KGBB.js";
15
+ import "./chunk-Z7CDL3T2.js";
16
16
  import {
17
17
  resolveRuntimeRoot
18
18
  } from "./chunk-RZ3WO7OV.js";
@@ -40,7 +40,7 @@ import {
40
40
  resolveClaudeCommandBinary,
41
41
  resolveRuntimeCommandBinary,
42
42
  runClaudePreflight
43
- } from "./chunk-5HQLPZR5.js";
43
+ } from "./chunk-6OPRRC2J.js";
44
44
  import {
45
45
  configFilePath,
46
46
  orchestratorLogPath,
package/dist/index.js CHANGED
@@ -417,13 +417,13 @@ function createRemovedCommandHandler(message) {
417
417
 
418
418
  // src/index.ts
419
419
  var COMMANDS = {
420
- workflow: () => import("./workflow-4OFPSVZ3.js"),
421
- setup: () => import("./setup-G5VYN6FV.js"),
422
- doctor: () => import("./doctor-3IIM4UYS.js"),
423
- upgrade: () => import("./upgrade-BXIHAENU.js"),
424
- repo: () => import("./repo-SLK4DDXH.js"),
420
+ workflow: () => import("./workflow-BOZ25AJ2.js"),
421
+ setup: () => import("./setup-JINI7HBM.js"),
422
+ doctor: () => import("./doctor-CCUTNEYN.js"),
423
+ upgrade: () => import("./upgrade-EBD4LX5W.js"),
424
+ repo: () => import("./repo-C2APQR2P.js"),
425
425
  config: () => import("./config-cmd-AOZVS6GU.js"),
426
- version: () => import("./version-MC2KSPJB.js")
426
+ version: () => import("./version-6Z354HHH.js")
427
427
  };
428
428
  function addGlobalOptions(command) {
429
429
  return command.option("--config <dir>", "Config directory").addOption(new Option("--config-dir <dir>").hideHelp()).option("-v, --verbose", "Enable verbose output").option("--json", "Output in JSON format").option("--no-color", "Disable color output");
@@ -17,7 +17,7 @@ import {
17
17
  import {
18
18
  initRepoRuntime,
19
19
  parseRepoRuntimeFlags
20
- } from "./chunk-2ZIPQ7ZS.js";
20
+ } from "./chunk-QOX5UGUE.js";
21
21
  import {
22
22
  OrchestratorService,
23
23
  acquireProjectLock,
@@ -33,8 +33,8 @@ import {
33
33
  resolveOrchestratorLogLevel,
34
34
  resolveTrackerAdapter,
35
35
  runCli
36
- } from "./chunk-VHEGRYK7.js";
37
- import "./chunk-A7EFL6EJ.js";
36
+ } from "./chunk-B6G3KGBB.js";
37
+ import "./chunk-Z7CDL3T2.js";
38
38
  import {
39
39
  resolveRepoRuntimeRoot,
40
40
  resolveRuntimeRoot
@@ -58,7 +58,7 @@ import {
58
58
  parseRecentEvents,
59
59
  readJsonFile,
60
60
  safeReadDir
61
- } from "./chunk-5HQLPZR5.js";
61
+ } from "./chunk-6OPRRC2J.js";
62
62
  import {
63
63
  daemonPidPath,
64
64
  httpStatusPath,
@@ -990,6 +990,7 @@ async function statusForIssue(reader, issueIdentifier) {
990
990
  kind: resolvedRun?.retryKind ?? null,
991
991
  error: resolvedRun?.lastError ?? issueRecord.retryEntry?.error ?? null
992
992
  } : null,
993
+ recovery: resolvedRun?.recovery ?? null,
993
994
  logs: {
994
995
  codex_session_logs: resolvedRun === null ? [] : [
995
996
  {
@@ -2568,6 +2569,22 @@ function renderLegacyStatus(snapshot, noColor) {
2568
2569
  }
2569
2570
  lines.push("");
2570
2571
  }
2572
+ if (snapshot.recovery) {
2573
+ const recovery = snapshot.recovery;
2574
+ lines.push(apply(yellow(" Recoverable incomplete turn:")));
2575
+ lines.push(` Run ${recovery.runId}`);
2576
+ lines.push(` Issue ${recovery.issueId}`);
2577
+ lines.push(` Workspace ${recovery.workspacePath}`);
2578
+ lines.push(
2579
+ ` Dirty ${recovery.dirtyFiles.length > 0 ? recovery.dirtyFiles.join(", ") : "none"}`
2580
+ );
2581
+ lines.push(` Last ${recovery.lastEvent ?? "unknown"}`);
2582
+ lines.push(` At ${recovery.lastEventAt ?? "unknown"}`);
2583
+ lines.push(` Session ${recovery.sessionId ?? "unknown"}`);
2584
+ lines.push(` Thread ${recovery.threadId ?? "unknown"}`);
2585
+ lines.push(` Command ${recovery.suggestedCommand}`);
2586
+ lines.push("");
2587
+ }
2571
2588
  if (snapshot.lastError) {
2572
2589
  lines.push(apply(red(` \u2717 ${snapshot.lastError}`)));
2573
2590
  lines.push("");
@@ -14,10 +14,10 @@ import {
14
14
  validateStateMapping,
15
15
  writeEcosystem,
16
16
  writeWorkflowPlan
17
- } from "./chunk-GL55AKBI.js";
17
+ } from "./chunk-ZPS4CQZJ.js";
18
18
  import {
19
19
  initRepoRuntime
20
- } from "./chunk-2ZIPQ7ZS.js";
20
+ } from "./chunk-QOX5UGUE.js";
21
21
  import "./chunk-RZ3WO7OV.js";
22
22
  import {
23
23
  GhAuthError,
@@ -31,7 +31,7 @@ import {
31
31
  listUserProjects,
32
32
  validateToken
33
33
  } from "./chunk-SMNIGNS3.js";
34
- import "./chunk-5HQLPZR5.js";
34
+ import "./chunk-6OPRRC2J.js";
35
35
  import "./chunk-4ICDSQCJ.js";
36
36
 
37
37
  // src/commands/setup.ts
@@ -16,8 +16,8 @@ function execFileAsync(file, args, execFileImpl = execFileCallback) {
16
16
  });
17
17
  }
18
18
  function resolveCurrentCliVersion() {
19
- if ("0.3.0".length > 0) {
20
- return "0.3.0";
19
+ if ("0.4.0".length > 0) {
20
+ return "0.4.0";
21
21
  }
22
22
  const pkg = JSON.parse(
23
23
  readFileSync(new URL("../../package.json", import.meta.url), "utf8")
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/commands/version.ts
4
4
  var handler = async (_args, options) => {
5
- const version = "0.3.0";
5
+ const version = "0.4.0";
6
6
  if (options.json) {
7
7
  process.stdout.write(JSON.stringify({ version }) + "\n");
8
8
  } else {
@@ -6,7 +6,7 @@ import {
6
6
  normalizeCodexRuntimeEvents,
7
7
  prepareCodexRuntimePlan,
8
8
  resolveLocalRuntimeLaunchConfig
9
- } from "./chunk-A7EFL6EJ.js";
9
+ } from "./chunk-Z7CDL3T2.js";
10
10
  import {
11
11
  DEFAULT_AGENT_INPUT_REQUIRED_REASON,
12
12
  classifySessionExit,
@@ -17,7 +17,7 @@ import {
17
17
  resolveClaudeCommandBinary,
18
18
  resolveWorkflowRuntimeCommand,
19
19
  runClaudePreflight
20
- } from "./chunk-5HQLPZR5.js";
20
+ } from "./chunk-6OPRRC2J.js";
21
21
 
22
22
  // ../worker/src/index.ts
23
23
  import { spawn as spawn2 } from "child_process";
@@ -6,12 +6,12 @@ import {
6
6
  resetWorkflowCommandDependenciesForTest,
7
7
  setWorkflowCommandDependenciesForTest,
8
8
  workflow_default
9
- } from "./chunk-GTNWFVFU.js";
10
- import "./chunk-GL55AKBI.js";
11
- import "./chunk-VHEGRYK7.js";
12
- import "./chunk-A7EFL6EJ.js";
9
+ } from "./chunk-TTVGBHZI.js";
10
+ import "./chunk-ZPS4CQZJ.js";
11
+ import "./chunk-B6G3KGBB.js";
12
+ import "./chunk-Z7CDL3T2.js";
13
13
  import "./chunk-SMNIGNS3.js";
14
- import "./chunk-5HQLPZR5.js";
14
+ import "./chunk-6OPRRC2J.js";
15
15
  import "./chunk-4ICDSQCJ.js";
16
16
  export {
17
17
  workflow_default as default,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gh-symphony/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "author": "hojinzs",
6
6
  "description": "Interactive CLI for GitHub Symphony orchestration",
@@ -42,12 +42,12 @@
42
42
  "devDependencies": {
43
43
  "tsup": "^8.5.1",
44
44
  "@gh-symphony/core": "0.0.14",
45
- "@gh-symphony/control-plane": "0.0.15",
45
+ "@gh-symphony/orchestrator": "0.0.14",
46
46
  "@gh-symphony/dashboard": "0.0.14",
47
+ "@gh-symphony/control-plane": "0.0.15",
48
+ "@gh-symphony/tracker-github": "0.0.14",
47
49
  "@gh-symphony/runtime-claude": "0.0.14",
48
- "@gh-symphony/orchestrator": "0.0.14",
49
- "@gh-symphony/worker": "0.0.14",
50
- "@gh-symphony/tracker-github": "0.0.14"
50
+ "@gh-symphony/worker": "0.0.14"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsup",