@moretea-labs/forge 1.5.0-rc.1 → 1.5.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.
Files changed (259) hide show
  1. package/AGENTS.md +9 -11
  2. package/CHANGELOG.md +19 -0
  3. package/CLAUDE.md +8 -10
  4. package/DESIGN.md +278 -0
  5. package/README.md +70 -90
  6. package/README.zh-CN.md +50 -55
  7. package/SKILL.md +8 -11
  8. package/THIRD_PARTY_NOTICES.md +9 -3
  9. package/assets/branding/README.md +4 -2
  10. package/assets/hooks/lib/workflow-state.sh +19 -18
  11. package/assets/hooks/post-edit-guard.sh +6 -75
  12. package/assets/hooks/prompt-guard.sh +24 -12
  13. package/assets/hooks/session-start-context.sh +17 -17
  14. package/assets/hooks/stop-orchestrator.sh +2 -2
  15. package/assets/partials/04-project-structure.partial.md +6 -6
  16. package/assets/partials/05-workflow.partial.md +6 -6
  17. package/assets/partials/07-footer.partial.md +1 -1
  18. package/assets/partials/08-orchestration.partial.md +2 -2
  19. package/assets/partials-agents/02-operating-mode.partial.md +5 -5
  20. package/assets/partials-agents/03-orchestration.partial.md +3 -3
  21. package/assets/partials-agents/04-task-protocol.partial.md +3 -3
  22. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  23. package/assets/reference-configs/agentic-development-flow.md +49 -86
  24. package/assets/reference-configs/external-tooling.md +21 -61
  25. package/assets/reference-configs/handoff-protocol.md +25 -20
  26. package/assets/reference-configs/harness-overview.md +8 -8
  27. package/assets/templates/helpers/archive-workflow.sh +0 -1
  28. package/assets/templates/helpers/capture-plan.sh +3 -3
  29. package/assets/templates/helpers/check-task-workflow.sh +2 -2
  30. package/assets/templates/helpers/codex-handoff-resume.sh +5 -5
  31. package/assets/templates/helpers/ensure-task-workflow.sh +37 -35
  32. package/assets/templates/helpers/new-plan.sh +1 -1
  33. package/assets/templates/helpers/plan-to-todo.sh +2 -2
  34. package/assets/templates/helpers/prepare-codex-handoff.sh +2 -2
  35. package/assets/templates/helpers/prepare-handoff.sh +9 -9
  36. package/assets/templates/helpers/refresh-current-status.sh +2 -2
  37. package/assets/templates/helpers/ship-worktrees.sh +1 -1
  38. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  39. package/assets/templates/helpers/verify-sprint.sh +7 -7
  40. package/assets/templates/helpers/workstream-sync.sh +1 -1
  41. package/assets/templates/plan.template.md +1 -1
  42. package/assets/workflow-contract.v1.json +6 -3
  43. package/docs/architecture/current/README.md +10 -9
  44. package/docs/architecture/current/approved-target-architecture.zh-CN.md +2 -2
  45. package/docs/architecture/current/architecture-invariants.md +2 -2
  46. package/docs/architecture/current/automation-and-schedule-engine.md +15 -15
  47. package/docs/architecture/current/codegraph-context-provider.md +2 -2
  48. package/docs/architecture/current/control-plane-authority-inventory.md +12 -0
  49. package/docs/architecture/current/dispatch-and-agent-strategy.md +11 -11
  50. package/docs/architecture/current/entity-model.md +0 -27
  51. package/docs/architecture/current/failure-recovery.md +0 -24
  52. package/docs/architecture/current/governance.md +1 -1
  53. package/docs/architecture/current/implementation-status.md +1 -1
  54. package/docs/architecture/current/legacy-governance.md +58 -0
  55. package/docs/architecture/current/multi-repository-execution.md +1 -1
  56. package/docs/architecture/current/peer-supercontroller-runtime.md +1 -1
  57. package/docs/architecture/current/personal-assistant-plugin-baseline.md +4 -5
  58. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -10
  59. package/docs/architecture/current/target-requirements-traceability.md +2 -2
  60. package/docs/architecture/current/thin-harness-governance-policy.md +1 -1
  61. package/docs/architecture/current/thin-harness-v1.md +9 -11
  62. package/docs/architecture/domains/runtime-harness.md +1 -1
  63. package/docs/forge-autonomous-goal-loop.md +3 -1
  64. package/docs/forge-chatgpt-mcp-setup.md +51 -184
  65. package/docs/images/forge-banner-cn.svg +26 -1
  66. package/docs/images/forge-banner.svg +26 -1
  67. package/docs/operations/features.md +1 -1
  68. package/docs/operations/homebrew.md +3 -3
  69. package/docs/operations/mcp-tool-exposure.md +5 -5
  70. package/docs/operations/platform-support.md +45 -37
  71. package/docs/operations/platform-support.zh-CN.md +40 -38
  72. package/docs/operations/releasing.md +3 -3
  73. package/docs/operations/releasing.zh-CN.md +3 -3
  74. package/docs/operations/troubleshooting.md +11 -6
  75. package/docs/operations/troubleshooting.zh-CN.md +11 -6
  76. package/docs/public-usage-guide.md +28 -21
  77. package/docs/public-usage-guide.zh-CN.md +28 -21
  78. package/docs/tutorials/01-install-and-start.md +56 -35
  79. package/docs/tutorials/01-install-and-start.zh-CN.md +56 -35
  80. package/docs/tutorials/02-connect-chatgpt.md +112 -46
  81. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +113 -45
  82. package/docs/wiki/Architecture.md +1 -1
  83. package/docs/wiki/Installation.md +2 -2
  84. package/docs/wiki/Quick-Start.md +1 -1
  85. package/docs/wiki/Work-Lifecycle.md +1 -1
  86. package/install.ps1 +4 -5
  87. package/install.sh +4 -6
  88. package/package.json +15 -5
  89. package/scripts/archive-workflow.sh +0 -1
  90. package/scripts/benchmark-thin-harness-mcp-e2e.ts +27 -14
  91. package/scripts/build-controller-ui.ts +28 -0
  92. package/scripts/capture-plan.sh +4 -4
  93. package/scripts/check-agent-tooling.sh +12 -1
  94. package/scripts/check-platform-support.mjs +2 -2
  95. package/scripts/check-public-docs.sh +16 -5
  96. package/scripts/check-release-readiness.sh +5 -11
  97. package/scripts/check-runtime-architecture.mjs +14 -22
  98. package/scripts/check-tarball-install-smoke.sh +3 -2
  99. package/scripts/check-task-workflow.sh +2 -2
  100. package/scripts/codex-handoff-resume.sh +5 -5
  101. package/scripts/create-project-dirs.sh +3 -3
  102. package/scripts/ensure-task-workflow.sh +45 -35
  103. package/scripts/init-project.sh +4 -4
  104. package/scripts/install-desktop-operator-registration.ts +2 -2
  105. package/scripts/lib/project-init-lib.sh +46 -44
  106. package/scripts/migrate-project-template.sh +7 -4
  107. package/scripts/new-plan.sh +1 -1
  108. package/scripts/plan-to-todo.sh +2 -2
  109. package/scripts/prepare-codex-handoff.sh +2 -2
  110. package/scripts/prepare-handoff.sh +9 -9
  111. package/scripts/refresh-current-status.sh +2 -2
  112. package/scripts/setup-plugins.sh +3 -3
  113. package/scripts/ship-worktrees.sh +1 -1
  114. package/scripts/sprint-backlog.sh +5 -5
  115. package/scripts/sync-codex-installed-copies.sh +2 -3
  116. package/scripts/verify-sprint.sh +7 -7
  117. package/scripts/workstream-sync.sh +1 -1
  118. package/src/cli/chatgpt-browser/bridge-extension.ts +1 -1
  119. package/src/cli/chatgpt-browser/bridge-provider.ts +29 -0
  120. package/src/cli/chatgpt-browser/file-policy.ts +3 -1
  121. package/src/cli/commands/chatgpt.ts +51 -0
  122. package/src/cli/commands/doctor.ts +1 -1
  123. package/src/cli/commands/init-hook.ts +211 -70
  124. package/src/cli/commands/init.ts +5 -6
  125. package/src/cli/commands/mcp.ts +29 -5
  126. package/src/cli/commands/runtime.ts +60 -8
  127. package/src/cli/commands/setup-profile.ts +441 -0
  128. package/src/cli/controller/check-runner.ts +20 -3
  129. package/src/cli/controller/codex-continuation.ts +4 -7
  130. package/src/cli/controller/context-pack.ts +118 -14
  131. package/src/cli/controller/operational-plan.ts +4 -4
  132. package/src/cli/controller/task-ledger.ts +10 -10
  133. package/src/cli/controller/work-mode.ts +6 -13
  134. package/src/cli/index.ts +14 -35
  135. package/src/cli/local-bridge/connector-freshness.ts +1 -1
  136. package/src/cli/local-bridge/console-automations.ts +160 -0
  137. package/src/cli/local-bridge/dashboard.ts +8 -1303
  138. package/src/cli/local-bridge/facade-api.ts +16 -7
  139. package/src/cli/local-bridge/frontend/src/App.tsx +10 -0
  140. package/src/cli/local-bridge/frontend/src/components/Button.tsx +1 -0
  141. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +3 -0
  142. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +1 -0
  143. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +1 -0
  144. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +1 -0
  145. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +3 -0
  146. package/src/cli/local-bridge/frontend/src/design/icons.tsx +11 -0
  147. package/src/cli/local-bridge/frontend/src/design/tokens.css +8 -0
  148. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +3 -0
  149. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +4 -0
  150. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +3 -0
  151. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +2 -0
  152. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +2 -0
  153. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +2 -0
  154. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +3 -0
  155. package/src/cli/local-bridge/frontend/src/lib/api.ts +23 -0
  156. package/src/cli/local-bridge/frontend/src/lib/format.ts +4 -0
  157. package/src/cli/local-bridge/frontend/src/lib/routes.ts +7 -0
  158. package/src/cli/local-bridge/frontend/src/main.tsx +1 -0
  159. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +1 -0
  160. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +2 -0
  161. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +3 -0
  162. package/src/cli/local-bridge/frontend/src/styles.css +12 -0
  163. package/src/cli/local-bridge/frontend/src/types.ts +14 -0
  164. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +1 -0
  165. package/src/cli/local-bridge/frontend/vite.config.ts +27 -0
  166. package/src/cli/local-bridge/server.ts +44 -0
  167. package/src/cli/local-bridge/ui-dist/app.css +1 -0
  168. package/src/cli/local-bridge/ui-dist/app.js +11094 -0
  169. package/src/cli/mcp/instructions.ts +3 -3
  170. package/src/cli/mcp/legacy-tool-service.ts +27 -23
  171. package/src/cli/mcp/multi-repository.ts +2 -0
  172. package/src/cli/mcp/policy.ts +7 -5
  173. package/src/cli/mcp/repository-tools.ts +53 -14
  174. package/src/cli/mcp/server.ts +5 -10
  175. package/src/cli/mcp/setup.ts +211 -275
  176. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  177. package/src/cli/mcp/toolset-names.ts +1 -1
  178. package/src/cli/mcp/toolset.ts +0 -1
  179. package/src/cli/mcp/transports/http.ts +24 -4
  180. package/src/cli/repositories/branch-name-policy.ts +0 -7
  181. package/src/cli/repositories/controller-home.ts +110 -3
  182. package/src/cli/repositories/safe-patch.ts +1 -1
  183. package/src/cli/repositories/selected-path-actions.ts +26 -24
  184. package/src/core/adoption/gitignore-plan.ts +2 -3
  185. package/src/core/adoption/plan.ts +4 -1
  186. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +556 -0
  187. package/src/runtime/control-plane/execution/work-handle-store.ts +128 -1
  188. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +277 -0
  189. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +169 -0
  190. package/src/runtime/control-plane/facade/capability-registry.ts +0 -10
  191. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +44 -25
  192. package/src/runtime/control-plane/facade/suggested-actions.ts +7 -1
  193. package/src/runtime/control-plane/facade/types.ts +0 -1
  194. package/src/runtime/control-plane/global-scheduler/scheduler.ts +30 -8
  195. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +114 -0
  196. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +175 -0
  197. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +161 -0
  198. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +64 -0
  199. package/src/runtime/control-plane/launcher/thin-launcher.ts +117 -24
  200. package/src/runtime/control-plane/routing/route-policy.ts +14 -14
  201. package/src/runtime/control-plane/runtime-cleanup.ts +0 -1
  202. package/src/runtime/evidence/event-ledger.ts +1 -1
  203. package/src/runtime/{workflow/campaigns/workspace.ts → execution/managed-workspace.ts} +41 -43
  204. package/src/runtime/execution/process-runtime/check-facade.ts +6 -0
  205. package/src/runtime/execution/process-runtime/check-result.ts +45 -0
  206. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -16
  207. package/src/runtime/execution/process-runtime/command-facade.ts +4 -2
  208. package/src/runtime/execution/process-runtime/gc.ts +1 -1
  209. package/src/runtime/execution/process-runtime/index.ts +3 -0
  210. package/src/runtime/execution/process-runtime/interactive-admission.ts +14 -5
  211. package/src/runtime/execution/process-runtime/persisted-check.ts +285 -0
  212. package/src/runtime/execution/process-runtime/resource-claims.ts +30 -0
  213. package/src/runtime/execution/process-runtime/runtime.ts +18 -1
  214. package/src/runtime/execution/process-runtime/store.ts +1 -19
  215. package/src/runtime/execution/process-runtime/types.ts +4 -0
  216. package/src/runtime/execution/thin-harness/execution-router.ts +0 -1
  217. package/src/runtime/execution/thin-harness/fast-executor.ts +1 -1
  218. package/src/runtime/execution/thin-harness/lightweight-lanes.ts +1 -2
  219. package/src/runtime/execution/thin-harness/types.ts +0 -1
  220. package/src/runtime/execution/workers/executor.ts +1 -1
  221. package/src/runtime/gateway/mcp/execution-tools.ts +1 -1
  222. package/src/runtime/gateway/mcp/persisted-check-process.ts +3 -255
  223. package/src/runtime/gateway/mcp/process-tools.ts +16 -3
  224. package/src/runtime/gateway/mcp/resource-policy.ts +0 -9
  225. package/src/runtime/gateway/mcp/router.ts +117 -182
  226. package/src/runtime/gateway/mcp/runtime-tools.ts +331 -409
  227. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +1 -219
  228. package/src/runtime/plugins/browser-adapter.ts +5 -0
  229. package/src/runtime/plugins/desktop-operator-registration.ts +1 -1
  230. package/src/runtime/plugins/store.ts +14 -6
  231. package/src/runtime/plugins/types.ts +1 -1
  232. package/src/runtime/projections/materialized-view.ts +1 -23
  233. package/src/runtime/recovery/maintenance-executor.ts +9 -0
  234. package/src/runtime/resources/ownership.ts +0 -1
  235. package/src/runtime/root/gateway-adapter.ts +2 -0
  236. package/src/runtime/root/mcp-transport.ts +10 -2
  237. package/src/runtime/root/package-runtime-release.ts +161 -0
  238. package/src/runtime/root/package-runtime-service.ts +172 -0
  239. package/src/runtime/root/runtime.ts +2 -1
  240. package/src/runtime/root/scheduler.ts +5 -0
  241. package/src/runtime/root/service.ts +18 -5
  242. package/src/runtime/standalone-recovery/core.ts +65 -16
  243. package/src/runtime/workflow/findings/store.ts +145 -106
  244. package/src/runtime/workflow/findings/types.ts +22 -0
  245. package/src/runtime/workflow/schedules/engine.ts +132 -2
  246. package/src/runtime/workflow/schedules/pending-activation.ts +139 -0
  247. package/src/runtime/workflow/schedules/work-continuation.ts +218 -0
  248. package/docs/CHANGELOG.md +0 -978
  249. package/docs/README.zh-CN.md +0 -58
  250. package/scripts/smoke-chatgpt-supervised-automation.ts +0 -208
  251. package/src/cli/commands/controller.ts +0 -680
  252. package/src/runtime/workflow/campaigns/cleanup.ts +0 -263
  253. package/src/runtime/workflow/campaigns/engine.ts +0 -112
  254. package/src/runtime/workflow/campaigns/normalize.ts +0 -100
  255. package/src/runtime/workflow/campaigns/review.ts +0 -315
  256. package/src/runtime/workflow/campaigns/store.ts +0 -526
  257. package/src/runtime/workflow/campaigns/supervisor.ts +0 -112
  258. package/src/runtime/workflow/campaigns/types.ts +0 -290
  259. /package/src/runtime/{workflow/campaigns → execution}/workspace-agent.ts +0 -0
@@ -1,680 +0,0 @@
1
- import { Command } from 'commander';
2
- import { createHash } from 'crypto';
3
- import { readFileSync } from 'fs';
4
- import { resolve } from 'path';
5
- import { getAgentJob, getAgentJobEvents, getAgentJobLog, listAgentJobs } from '../agent-jobs/job-manager';
6
- import { archiveIssue, getIssue, inspectIssueReadiness, projectBoard, restoreIssue } from '../controller/issue-store';
7
- import { getControllerTimeline, getProjectProgress } from '../controller/progress';
8
- import { finishEditSession, finishTaskRun, type TaskReviewDecision } from '../controller/completion-orchestrator';
9
- import { applyCompletionDecision, completionDecisionQueues, finishCompletionBacklog, inspectCompletionBacklog, type CompletionBacklogReport, type CompletionDecisionAction } from '../controller/completion-backlog';
10
- import { exportControllerWorklog, parseWorklogCategory } from '../controller/worklog';
11
- import { inspectProjectGovernance, reconcileProjectGovernance } from '../controller/governance';
12
- import { prepareCodexContinuation } from '../controller/codex-continuation';
13
- import { applyStuckStateMigration, inspectStuckControllerStates } from '../controller/stuck-state-migration';
14
- import { assessWorkMode } from '../controller/work-mode';
15
- import { runControllerCheck, snapshotControllerCheck } from '../controller/check-runner';
16
- import { finalizeEditSession, getEditSession, getEditSessionDiff, listEditSessions, rollbackEditSession, verifyEditSession } from '../editing/edit-session';
17
- import { loadControllerProjectState, saveControllerProjectState } from '../controller/project-state';
18
- import { closeIssueWithGitHubPlugin, getGitHubPluginStatus, publishIssueWithGitHubPlugin, refreshIssueWithGitHubPlugin, saveGitHubPluginConfig } from '../github/plugin';
19
- import { getGitHubStatus } from '../github/github';
20
- import { repositoryChangeVerify } from '../controller/composite-operations';
21
- import { executeLocalBridgeJob } from '../local-bridge/job-store';
22
-
23
- const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
24
-
25
- function output(value: unknown, json = false): void {
26
- if (json) console.log(JSON.stringify(value, null, 2));
27
- else console.log(typeof value === 'string' ? value : JSON.stringify(value, null, 2));
28
- }
29
-
30
- function repoRoot(value?: string): string {
31
- return resolve(value ?? process.cwd());
32
- }
33
-
34
- function formatBoard(board: ReturnType<typeof projectBoard>): string {
35
- const lines = ['forge Controller board', ''];
36
- const counts = Object.entries(board.counts).sort(([a], [b]) => a.localeCompare(b));
37
- lines.push(`Tasks: ${counts.length ? counts.map(([status, count]) => `${status}=${count}`).join(' ') : 'none'}`);
38
- lines.push('');
39
- for (const issue of board.issues) {
40
- lines.push(`${issue.id} [${issue.status}] ${issue.title}${issue.github && typeof issue.github === 'object' && 'url' in issue.github ? `\n GitHub: ${String((issue.github as { url: unknown }).url)}` : ''}`);
41
- const tasks = Array.isArray(issue.tasks) ? issue.tasks as Array<Record<string, unknown>> : [];
42
- for (const task of tasks) {
43
- const runIds = Array.isArray(task.runIds) ? task.runIds : [];
44
- lines.push(` ${String(task.id)} [${String(task.status)}] ${String(task.title)} agent=${String(task.agent)}${runIds.length ? ` run=${String(runIds.at(-1))}` : ''}`);
45
- }
46
- }
47
- return lines.join('\n');
48
- }
49
-
50
- function formatRuns(runs: ReturnType<typeof listAgentJobs>): string {
51
- if (runs.length === 0) return 'No controller Runs.';
52
- return runs.map((run) => [
53
- `${run.runId} [${run.status}] ${run.agent}/${run.provider}`,
54
- ` ${run.issueId}/${run.taskId}`,
55
- ...(run.github?.url ? [` Session: ${run.github.url}`] : []),
56
- ...(run.github?.pullRequestUrl ? [` Pull request: ${run.github.pullRequestUrl}`] : []),
57
- ...(run.error ? [` Error: ${run.error}`] : []),
58
- ].join('\n')).join('\n\n');
59
- }
60
-
61
-
62
- function formatCompletionBacklog(report: CompletionBacklogReport): string {
63
- const lines = ['forge Completion backlog', ''];
64
- lines.push(`Scanned: ${report.scannedAt}`);
65
- lines.push(`Counts: ${Object.entries(report.counts).map(([key, count]) => `${key}=${count}`).join(' ')}`);
66
- if (report.recommendations.length) {
67
- lines.push('', 'Recommendations:');
68
- for (const recommendation of report.recommendations) lines.push(` - ${recommendation}`);
69
- }
70
- const actionable = report.items.filter((item) => item.action !== 'already_terminal');
71
- if (actionable.length) {
72
- lines.push('', 'Items:');
73
- for (const item of actionable) {
74
- lines.push(` ${item.issueId}/${item.taskId} [${item.action}] task=${item.taskStatus}${item.runStatus ? ` run=${item.runStatus}` : ''}${item.runId ? ` ${item.runId}` : ''}`);
75
- lines.push(` ${item.reason}`);
76
- }
77
- }
78
- return lines.join('\n');
79
- }
80
-
81
- async function watchRun(root: string, runId: string, intervalSeconds: number, includeLog: boolean, json: boolean): Promise<void> {
82
- let eventCount = -1;
83
- let lastStatus = '';
84
- let previousLog = '';
85
- let logHeaderPrinted = false;
86
- let lastLogError = '';
87
- while (true) {
88
- const run = getAgentJob(root, runId);
89
- const events = getAgentJobEvents(root, runId, 1000);
90
- if (json) {
91
- output({ run, events: events.slice(Math.max(0, eventCount)) }, true);
92
- } else if (run.status !== lastStatus || events.length !== eventCount) {
93
- console.log(`[${new Date().toISOString()}] ${run.runId} status=${run.status} provider=${run.provider}`);
94
- for (const event of events.slice(Math.max(0, eventCount))) {
95
- console.log(` ${event.at} ${event.type}${event.message ? ` ${event.message}` : ''}`);
96
- }
97
- if (run.github?.url) console.log(` Session: ${run.github.url}`);
98
- if (run.github?.pullRequestUrl) console.log(` Pull request: ${run.github.pullRequestUrl}`);
99
- if (run.error) console.log(` Error: ${run.error}`);
100
- }
101
-
102
- if (includeLog && !json) {
103
- try {
104
- const currentLog = getAgentJobLog(root, runId, false).log;
105
- if (currentLog !== previousLog) {
106
- const addition = currentLog.startsWith(previousLog) ? currentLog.slice(previousLog.length) : currentLog;
107
- if (addition) {
108
- if (!logHeaderPrinted) {
109
- console.log('\n--- Live Run log ---');
110
- logHeaderPrinted = true;
111
- } else if (!currentLog.startsWith(previousLog)) {
112
- console.log('\n[forge] log window reset; showing current retained output');
113
- }
114
- process.stdout.write(addition.endsWith('\n') ? addition : `${addition}\n`);
115
- }
116
- previousLog = currentLog;
117
- }
118
- lastLogError = '';
119
- } catch (error) {
120
- const message = error instanceof Error ? error.message : String(error);
121
- if (message !== lastLogError) {
122
- console.log(` Log not available yet: ${message}`);
123
- lastLogError = message;
124
- }
125
- }
126
- }
127
-
128
- eventCount = events.length;
129
- lastStatus = run.status;
130
- if (TERMINAL.has(run.status)) return;
131
- await new Promise((resolvePromise) => setTimeout(resolvePromise, Math.max(1, intervalSeconds) * 1000));
132
- }
133
- }
134
-
135
- export function buildControllerCommand(): Command {
136
- const command = new Command('controller').description('Inspect forge Issues, Tasks, local Runs, and GitHub cloud sessions');
137
-
138
- command.command('board')
139
- .description('Show the durable Issue and Task board')
140
- .option('--repo <path>', 'Repository root')
141
- .option('--json', 'Output JSON')
142
- .action((opts: { repo?: string; json?: boolean }) => {
143
- const board = projectBoard(repoRoot(opts.repo));
144
- output(opts.json ? board : formatBoard(board), opts.json === true);
145
- });
146
-
147
- command.command('runs')
148
- .description('List recent local and GitHub cloud Runs')
149
- .option('--repo <path>', 'Repository root')
150
- .option('--limit <count>', 'Maximum Runs', '25')
151
- .option('--json', 'Output JSON')
152
- .action((opts: { repo?: string; limit?: string; json?: boolean }) => {
153
- const runs = listAgentJobs(repoRoot(opts.repo), Math.max(1, Number(opts.limit ?? 25)));
154
- output(opts.json ? { runs } : formatRuns(runs), opts.json === true);
155
- });
156
-
157
- command.command('watch')
158
- .description('Follow one local Run or GitHub Copilot cloud session until it reaches a terminal state')
159
- .argument('<run-id>', 'Controller Run ID')
160
- .option('--repo <path>', 'Repository root')
161
- .option('--interval <seconds>', 'Polling interval', '2')
162
- .option('--log', 'Print the final local or GitHub session log')
163
- .option('--json', 'Output JSON snapshots')
164
- .action(async (runId: string, opts: { repo?: string; interval?: string; log?: boolean; json?: boolean }) => {
165
- await watchRun(repoRoot(opts.repo), runId, Number(opts.interval ?? 2), opts.log === true, opts.json === true);
166
- });
167
-
168
- command.command('assess')
169
- .description('Choose direct_edit, quick_agent, or issue_task for one work request')
170
- .argument('<description>', 'Work request description')
171
- .option('--repo <path>', 'Repository root')
172
- .option('--path <path...>', 'Known target paths')
173
- .option('--expected-files <count>', 'Estimated file count')
174
- .option('--expected-lines <count>', 'Estimated changed lines')
175
- .option('--investigation', 'Root-cause investigation is required')
176
- .option('--parallel', 'Parallel work is required')
177
- .option('--long-checks', 'Long-running verification is required')
178
- .option('--dependencies', 'A durable Task dependency graph is required')
179
- .option('--mode <mode>', 'Explicit mode: direct, plan, debug, review, campaign, release, or scale')
180
- .option('--risk <risk>', 'readonly, low, medium, high, or destructive', 'low')
181
- .option('--json', 'Output JSON')
182
- .action((description: string, opts: { path?: string[]; expectedFiles?: string; expectedLines?: string; investigation?: boolean; parallel?: boolean; longChecks?: boolean; dependencies?: boolean; mode?: 'direct' | 'plan' | 'debug' | 'review' | 'campaign' | 'release' | 'scale'; risk?: 'readonly' | 'low' | 'medium' | 'high' | 'destructive'; json?: boolean }) => {
183
- const result = assessWorkMode({
184
- description,
185
- knownPaths: opts.path,
186
- expectedFiles: opts.expectedFiles ? Number(opts.expectedFiles) : undefined,
187
- expectedChangedLines: opts.expectedLines ? Number(opts.expectedLines) : undefined,
188
- requiresInvestigation: opts.investigation,
189
- requiresParallelism: opts.parallel,
190
- requiresLongRunningChecks: opts.longChecks,
191
- needsDependencies: opts.dependencies,
192
- explicitMode: opts.mode,
193
- risk: opts.risk,
194
- });
195
- output(result, opts.json === true);
196
- });
197
-
198
- command.command('edits')
199
- .description('List direct-edit sessions and their file/check evidence')
200
- .option('--repo <path>', 'Repository root')
201
- .option('--limit <count>', 'Maximum sessions', '50')
202
- .option('--json', 'Output JSON')
203
- .action((opts: { repo?: string; limit?: string; json?: boolean }) => {
204
- const sessions = listEditSessions(repoRoot(opts.repo), Number(opts.limit ?? 50));
205
- if (opts.json) return output({ sessions }, true);
206
- output(sessions.length ? sessions.map((session) => `${session.sessionId} [${session.status}] ${session.changedFiles} files / ${session.changedLines} lines / ${session.checksPassed}/${session.checksTotal} checks\n ${session.purpose}`).join('\n\n') : 'No direct-edit sessions.');
207
- });
208
-
209
- command.command('edit')
210
- .description('Show one direct-edit session')
211
- .argument('<session-id>', 'Edit session ID')
212
- .option('--repo <path>', 'Repository root')
213
- .option('--json', 'Output JSON')
214
- .action((sessionId: string, opts: { repo?: string; json?: boolean }) => output(getEditSession(repoRoot(opts.repo), sessionId), opts.json === true));
215
-
216
- command.command('edit-diff')
217
- .description('Print the persisted patch for one direct-edit session')
218
- .argument('<session-id>', 'Edit session ID')
219
- .option('--repo <path>', 'Repository root')
220
- .option('--json', 'Output JSON')
221
- .action((sessionId: string, opts: { repo?: string; json?: boolean }) => {
222
- const result = getEditSessionDiff(repoRoot(opts.repo), sessionId);
223
- output(opts.json ? result : result.patch || '(no patch)', opts.json === true);
224
- });
225
-
226
- command.command('verify-edit')
227
- .description('Run named checks and record review evidence for a direct edit')
228
- .argument('<session-id>', 'Edit session ID')
229
- .option('--repo <path>', 'Repository root')
230
- .option('--check <id...>', 'Override named checks')
231
- .option('--reviewer <name>', 'Reviewer identity', 'controller-cli-human')
232
- .option('--note <text>', 'Review note')
233
- .option('--json', 'Output JSON')
234
- .action((sessionId: string, opts: { repo?: string; check?: string[]; reviewer?: string; note?: string; json?: boolean }) => output(verifyEditSession(repoRoot(opts.repo), sessionId, { checkIds: opts.check, reviewer: opts.reviewer, note: opts.note }), opts.json === true));
235
-
236
- command.command('finalize-edit')
237
- .description('Finalize a verified direct edit')
238
- .argument('<session-id>', 'Edit session ID')
239
- .option('--repo <path>', 'Repository root')
240
- .option('--reviewer <name>', 'Reviewer identity', 'controller-cli-human')
241
- .option('--note <text>', 'Final note')
242
- .option('--json', 'Output JSON')
243
- .action((sessionId: string, opts: { repo?: string; reviewer?: string; note?: string; json?: boolean }) => output(finalizeEditSession(repoRoot(opts.repo), sessionId, { reviewer: opts.reviewer, note: opts.note }), opts.json === true));
244
-
245
- command.command('finish-edit')
246
- .description('Verify, finalize, commit, receipt, and complete one direct-edit Task session')
247
- .argument('<session-id>', 'Edit session ID')
248
- .option('--repo <path>', 'Repository root')
249
- .option('--decision <decision>', 'auto, approve_and_finish, request_changes, or discard', 'auto')
250
- .option('--reviewer <name>', 'Reviewer identity')
251
- .option('--note <text>', 'Review note')
252
- .option('--check <id...>', 'Additional named checks')
253
- .option('--no-commit', 'Do not create the required selected-path Git commit after successful finish')
254
- .option('--json', 'Output JSON')
255
- .action((sessionId: string, opts: { repo?: string; decision?: string; reviewer?: string; note?: string; check?: string[]; commit?: boolean; json?: boolean }) => {
256
- const decision = String(opts.decision ?? 'auto').replace(/-/g, '_') as TaskReviewDecision;
257
- if (!['auto', 'approve_and_finish', 'request_changes', 'discard'].includes(decision)) {
258
- throw new Error('decision must be auto, approve_and_finish, request_changes, or discard');
259
- }
260
- output(finishEditSession(repoRoot(opts.repo), {
261
- sessionId,
262
- decision,
263
- reviewer: opts.reviewer,
264
- note: opts.note,
265
- checkIds: opts.check,
266
- commit: opts.commit !== false,
267
- }), opts.json === true);
268
- });
269
-
270
- command.command('rollback-edit')
271
- .description('Rollback a non-finalized direct edit')
272
- .argument('<session-id>', 'Edit session ID')
273
- .option('--repo <path>', 'Repository root')
274
- .option('--json', 'Output JSON')
275
- .action((sessionId: string, opts: { repo?: string; json?: boolean }) => output(rollbackEditSession(repoRoot(opts.repo), sessionId), opts.json === true));
276
-
277
- command.command('finish-run')
278
- .description('Finish one reviewed Run: integrate if needed, verify, accept when policy allows, and clean the isolated worktree')
279
- .argument('<run-id>', 'Controller Run ID')
280
- .option('--repo <path>', 'Repository root')
281
- .option('--decision <decision>', 'auto, approve_and_finish, request_changes, or discard', 'auto')
282
- .option('--reviewer <name>', 'Reviewer identity')
283
- .option('--note <text>', 'Review note')
284
- .option('--keep-worktree', 'Do not remove the isolated worktree after integration')
285
- .option('--no-commit', 'Do not create the required selected-path Git commit after successful finish')
286
- .option('--json', 'Output JSON')
287
- .action((runId: string, opts: { repo?: string; decision?: string; reviewer?: string; note?: string; keepWorktree?: boolean; commit?: boolean; json?: boolean }) => {
288
- const decision = String(opts.decision ?? 'auto').replace(/-/g, '_') as TaskReviewDecision;
289
- if (!['auto', 'approve_and_finish', 'request_changes', 'discard'].includes(decision)) {
290
- throw new Error('decision must be auto, approve_and_finish, request_changes, or discard');
291
- }
292
- const finished = finishTaskRun(repoRoot(opts.repo), {
293
- runId,
294
- decision,
295
- reviewer: opts.reviewer,
296
- note: opts.note,
297
- cleanup: opts.keepWorktree !== true,
298
- commit: opts.commit !== false,
299
- });
300
- output(finished, opts.json === true);
301
- });
302
-
303
- command.command('completion-backlog')
304
- .description('Show finishable Runs, human-review items, retry-required Tasks, and stale review states')
305
- .option('--repo <path>', 'Repository root')
306
- .option('--include-terminal', 'Include terminal done/cancelled/superseded Tasks')
307
- .option('--limit <count>', 'Maximum items', '500')
308
- .option('--json', 'Output JSON')
309
- .action((opts: { repo?: string; includeTerminal?: boolean; limit?: string; json?: boolean }) => {
310
- const report = inspectCompletionBacklog(repoRoot(opts.repo), {
311
- includeTerminal: opts.includeTerminal === true,
312
- limit: opts.limit ? Number(opts.limit) : undefined,
313
- });
314
- output(opts.json ? report : formatCompletionBacklog(report), opts.json === true);
315
- });
316
-
317
- command.command('finish-ready-runs')
318
- .description('Batch-finish low/medium completed Runs; dry-run by default and never approves high-risk work')
319
- .option('--repo <path>', 'Repository root')
320
- .option('--apply', 'Apply the auto-finish actions instead of dry-running')
321
- .option('--limit <count>', 'Maximum auto-finish Runs', '25')
322
- .option('--keep-worktree', 'Do not remove isolated worktrees after integration')
323
- .option('--no-commit', 'Do not create selected-path Git commits after successful finishes')
324
- .option('--reviewer <name>', 'Reviewer identity', 'forge-completion-backlog')
325
- .option('--json', 'Output JSON')
326
- .action((opts: { repo?: string; apply?: boolean; limit?: string; keepWorktree?: boolean; commit?: boolean; reviewer?: string; json?: boolean }) => {
327
- const finished = finishCompletionBacklog(repoRoot(opts.repo), {
328
- dryRun: opts.apply !== true,
329
- limit: opts.limit ? Number(opts.limit) : undefined,
330
- cleanup: opts.keepWorktree !== true,
331
- commit: opts.commit !== false,
332
- reviewer: opts.reviewer,
333
- });
334
- output(finished, opts.json === true);
335
- if (!opts.json && finished.dryRun && finished.attempted > 0) {
336
- console.log('Dry run only. Re-run with --apply to finish these low/medium Runs.');
337
- }
338
- });
339
-
340
- command.command('completion-queues')
341
- .description('Show Local-Bridge-friendly completion decision queues')
342
- .option('--repo <path>', 'Repository root')
343
- .option('--include-terminal', 'Include terminal done/cancelled/superseded Tasks')
344
- .option('--limit <count>', 'Maximum items', '500')
345
- .option('--json', 'Output JSON')
346
- .action((opts: { repo?: string; includeTerminal?: boolean; limit?: string; json?: boolean }) => {
347
- const queues = completionDecisionQueues(repoRoot(opts.repo), {
348
- includeTerminal: opts.includeTerminal === true,
349
- limit: opts.limit ? Number(opts.limit) : undefined,
350
- });
351
- output(queues, opts.json === true);
352
- });
353
-
354
- command.command('completion-decision')
355
- .description('Apply one explicit completion decision from the queue')
356
- .option('--repo <path>', 'Repository root')
357
- .requiredOption('--action <action>', 'finish, approve_and_finish, request_changes, discard, mark_inspected, or retry_later')
358
- .option('--run-id <runId>', 'Run ID for run decisions')
359
- .option('--issue-id <issueId>', 'Issue ID for task decisions')
360
- .option('--task-id <taskId>', 'Task ID for task decisions')
361
- .option('--reviewer <name>', 'Reviewer identity', 'forge-completion-decision')
362
- .option('--note <text>', 'Decision note')
363
- .option('--keep-worktree', 'Do not remove isolated worktrees after integration')
364
- .option('--no-commit', 'Do not create the required selected-path Git commit after finish')
365
- .option('--json', 'Output JSON')
366
- .action((opts: { repo?: string; action: string; runId?: string; issueId?: string; taskId?: string; reviewer?: string; note?: string; keepWorktree?: boolean; commit?: boolean; json?: boolean }) => {
367
- const action = String(opts.action ?? '').replace(/-/g, '_') as CompletionDecisionAction;
368
- if (!['finish', 'approve_and_finish', 'request_changes', 'discard', 'mark_inspected', 'retry_later'].includes(action)) {
369
- throw new Error('action must be finish, approve_and_finish, request_changes, discard, mark_inspected, or retry_later');
370
- }
371
- output(applyCompletionDecision(repoRoot(opts.repo), {
372
- action,
373
- runId: opts.runId,
374
- issueId: opts.issueId,
375
- taskId: opts.taskId,
376
- reviewer: opts.reviewer,
377
- note: opts.note,
378
- cleanup: opts.keepWorktree !== true,
379
- commit: opts.commit !== false,
380
- }), opts.json === true);
381
- });
382
-
383
- command.command('stuck-states')
384
- .description('Inspect stale review, retry-required, missing-run, and blocked completion states')
385
- .option('--repo <path>', 'Repository root')
386
- .option('--limit <count>', 'Maximum findings', '500')
387
- .option('--json', 'Output JSON')
388
- .action((opts: { repo?: string; limit?: string; json?: boolean }) => {
389
- output(inspectStuckControllerStates(repoRoot(opts.repo), { limit: opts.limit ? Number(opts.limit) : undefined }), opts.json === true);
390
- });
391
-
392
- command.command('migrate-stuck-states')
393
- .description('Safely annotate stale completion states; dry-run by default')
394
- .option('--repo <path>', 'Repository root')
395
- .option('--apply', 'Apply note-only migration actions')
396
- .option('--limit <count>', 'Maximum findings', '100')
397
- .option('--mark-retry-required', 'Move retry-required findings to changes_requested')
398
- .option('--mark-no-run-evidence', 'Move stale review/running without run evidence to changes_requested')
399
- .option('--reviewer <name>', 'Reviewer identity', 'forge-stuck-state-migration')
400
- .option('--json', 'Output JSON')
401
- .action((opts: { repo?: string; apply?: boolean; limit?: string; markRetryRequired?: boolean; markNoRunEvidence?: boolean; reviewer?: string; json?: boolean }) => {
402
- output(applyStuckStateMigration(repoRoot(opts.repo), {
403
- dryRun: opts.apply !== true,
404
- limit: opts.limit ? Number(opts.limit) : undefined,
405
- reviewer: opts.reviewer,
406
- markRetryRequired: opts.markRetryRequired === true,
407
- markNoRunEvidence: opts.markNoRunEvidence === true,
408
- }), opts.json === true);
409
- });
410
-
411
- command.command('codex-continuation')
412
- .description('Prepare a Codex continuation packet; provider startup belongs to Thin Launcher')
413
- .option('--repo <path>', 'Repository root')
414
- .option('--objective <text>', 'Continuation objective')
415
- .option('--max-items <count>', 'Maximum backlog/stuck items', '100')
416
- .option('--launch', 'Deprecated: returns a Thin Launcher migration instruction')
417
- .option('--json', 'Output JSON')
418
- .action((opts: { repo?: string; objective?: string; maxItems?: string; launch?: boolean; json?: boolean }) => {
419
- output(prepareCodexContinuation(repoRoot(opts.repo), {
420
- objective: opts.objective,
421
- maxItems: opts.maxItems ? Number(opts.maxItems) : undefined,
422
- mode: opts.launch === true ? 'launch' : 'prepare',
423
- }), opts.json === true);
424
- });
425
-
426
- command.command('progress')
427
- .description('Show project, Issue, and Task progress derived from durable controller state')
428
- .option('--repo <path>', 'Repository root')
429
- .option('--json', 'Output JSON')
430
- .action((opts: { repo?: string; json?: boolean }) => {
431
- const progress = getProjectProgress(repoRoot(opts.repo));
432
- if (opts.json) return output(progress, true);
433
- const lines = [
434
- `Evidence gates: ${progress.completedGates}/${progress.totalGates} complete`,
435
- `Current Issue: ${progress.currentIssueId ?? 'not selected'}`,
436
- `Issues: ${progress.activeIssueCount} active / ${progress.archivedIssueCount} archived / ${progress.issueCount} total`,
437
- `Tasks: ${progress.completedTaskCount} accepted / ${progress.taskCount} total`,
438
- `Active Runs: ${progress.activeRunCount}`,
439
- '',
440
- ...progress.issues.map((issue) => `${issue.id} ${issue.completedGates}/${issue.totalGates} gates [${issue.status}] ${issue.title}${issue.isCurrent ? ' CURRENT' : ''}${issue.attentionCount ? ` attention=${issue.attentionCount}` : ''}`),
441
- ];
442
- output(lines.join('\n'));
443
- });
444
-
445
- command.command('governance')
446
- .description('Inspect execution focus, dead dependencies, retryable failures, review/acceptance backlog, duplicates, and closeout anomalies')
447
- .option('--repo <path>', 'Repository root')
448
- .option('--json', 'Output JSON')
449
- .action((opts: { repo?: string; json?: boolean }) => {
450
- const result = inspectProjectGovernance(repoRoot(opts.repo));
451
- if (opts.json) return output(result, true);
452
- const lines = [
453
- `Governance: ${result.health}`,
454
- `Current Issue: ${result.currentIssueId ?? 'not selected'}`,
455
- `Execution queue: ${result.executionQueue.length}`,
456
- `Findings: critical=${result.counts.critical ?? 0} warning=${result.counts.warning ?? 0} info=${result.counts.info ?? 0}`,
457
- '',
458
- ...result.findings.map((entry) => `${entry.severity.toUpperCase()} ${entry.code} ${[entry.issueId, entry.taskId].filter(Boolean).join('/')}\n ${entry.message}`),
459
- ];
460
- output(lines.join('\n'));
461
- });
462
-
463
- command.command('reconcile')
464
- .description('Apply safe project-governance repairs and refresh the execution queue')
465
- .option('--repo <path>', 'Repository root')
466
- .option('--json', 'Output JSON')
467
- .action((opts: { repo?: string; json?: boolean }) => output(reconcileProjectGovernance(repoRoot(opts.repo)), opts.json === true));
468
-
469
- command.command('focus')
470
- .description('Select an informational primary Issue without blocking other Tasks')
471
- .argument('<issue-id>', 'Controller Issue ID')
472
- .option('--repo <path>', 'Repository root')
473
- .option('--json', 'Output JSON')
474
- .action((issueId: string, opts: { repo?: string; json?: boolean }) => {
475
- const root = repoRoot(opts.repo);
476
- const issue = getIssue(root, issueId);
477
- if (issue.archivedAt || ['done', 'cancelled'].includes(issue.status)) throw new Error(`Issue is not active: ${issue.status}`);
478
- output(saveControllerProjectState(root, { currentIssueId: issue.id }, 'controller-cli'), opts.json === true);
479
- });
480
-
481
- command.command('launch')
482
- .description('Deprecated: prepare Task-local launch candidates for an external SuperController')
483
- .argument('<issue-id>', 'Controller Issue ID')
484
- .option('--repo <path>', 'Repository root')
485
- .option('--max-parallel <count>', 'Maximum Tasks to launch', '1')
486
- .option('--timeout-ms <milliseconds>', 'Agent timeout')
487
- .option('--json', 'Output JSON')
488
- .action((issueId: string, opts: { repo?: string; maxParallel?: string; timeoutMs?: string; json?: boolean }) => {
489
- const root = repoRoot(opts.repo);
490
- const readiness = inspectIssueReadiness(root, issueId);
491
- if (!readiness.queueable) throw new Error(`Issue has no queueable Tasks: ${[...readiness.blockers, ...readiness.taskBlockers].map((entry) => entry.code).join(', ') || 'no queueable Tasks'}`);
492
- output({
493
- accepted: false,
494
- code: 'LOCAL_BRIDGE_JOB_RETIRED',
495
- readiness,
496
- message: 'controller launch no longer creates Local Bridge Jobs. Create or claim WorkContract and start an external Controller through the Thin Launcher.',
497
- }, opts.json === true);
498
- });
499
-
500
- command.command('archive')
501
- .description('Archive a done or cancelled Issue')
502
- .argument('<issue-id>', 'Controller Issue ID')
503
- .option('--repo <path>', 'Repository root')
504
- .option('--json', 'Output JSON')
505
- .action((issueId: string, opts: { repo?: string; json?: boolean }) => output(archiveIssue(repoRoot(opts.repo), issueId), opts.json === true));
506
-
507
- command.command('restore')
508
- .description('Restore an archived Issue')
509
- .argument('<issue-id>', 'Controller Issue ID')
510
- .option('--repo <path>', 'Repository root')
511
- .option('--json', 'Output JSON')
512
- .action((issueId: string, opts: { repo?: string; json?: boolean }) => output(restoreIssue(repoRoot(opts.repo), issueId), opts.json === true));
513
-
514
- command.command('policy')
515
- .description('Read or update Controller execution policy')
516
- .option('--repo <path>', 'Repository root')
517
- .option('--issue-creation-mode <mode>', 'open, focus_only, or paused')
518
- .option('--json', 'Output JSON')
519
- .action((opts: { repo?: string; issueCreationMode?: string; json?: boolean }) => {
520
- const root = repoRoot(opts.repo);
521
- const mode = opts.issueCreationMode;
522
- if (mode && !['open', 'focus_only', 'paused'].includes(mode)) throw new Error('issue creation mode must be open, focus_only, or paused');
523
- const result = mode ? saveControllerProjectState(root, { issueCreationMode: mode as 'open' | 'focus_only' | 'paused' }, 'controller-cli') : loadControllerProjectState(root);
524
- output(result, opts.json === true);
525
- });
526
-
527
- command.command('timeline')
528
- .description('Show the unified controller worklog and Run timeline')
529
- .option('--repo <path>', 'Repository root')
530
- .option('--issue <id>', 'Filter by Issue ID')
531
- .option('--task <id>', 'Filter by Task ID')
532
- .option('--run <id>', 'Filter by Run ID')
533
- .option('--category <name>', 'Filter by category')
534
- .option('--limit <count>', 'Maximum events', '100')
535
- .option('--json', 'Output JSON')
536
- .action((opts: { repo?: string; issue?: string; task?: string; run?: string; category?: string; limit?: string; json?: boolean }) => {
537
- const events = getControllerTimeline(repoRoot(opts.repo), {
538
- issueId: opts.issue,
539
- taskId: opts.task,
540
- runId: opts.run,
541
- category: parseWorklogCategory(opts.category),
542
- limit: Math.max(1, Number(opts.limit ?? 100)),
543
- });
544
- if (opts.json) return output({ events }, true);
545
- output(events.map((event) => `${event.at} ${event.category}/${event.action} ${[event.issueId, event.taskId, event.runId].filter(Boolean).join('/')}
546
- ${event.summary}`).join('\n'));
547
- });
548
-
549
- command.command('export-worklog')
550
- .description('Export the controller worklog to a tracked Markdown or JSON report')
551
- .option('--repo <path>', 'Repository root')
552
- .option('--format <format>', 'markdown or json', 'markdown')
553
- .option('--output <path>', 'Repository-relative output path')
554
- .option('--issue <id>', 'Filter by Issue ID')
555
- .option('--task <id>', 'Filter by Task ID')
556
- .option('--run <id>', 'Filter by Run ID')
557
- .option('--json', 'Output command result as JSON')
558
- .action((opts: { repo?: string; format?: string; output?: string; issue?: string; task?: string; run?: string; json?: boolean }) => {
559
- const result = exportControllerWorklog(repoRoot(opts.repo), {
560
- format: opts.format === 'json' ? 'json' : 'markdown',
561
- outputPath: opts.output,
562
- filter: { issueId: opts.issue, taskId: opts.task, runId: opts.run },
563
- });
564
- output(opts.json ? result : `Exported ${result.eventCount} events to ${result.path}`, opts.json === true);
565
- });
566
-
567
- const githubPlugin = command.command('github')
568
- .description('Configure and use the optional GitHub Issue/Project plugin');
569
-
570
- githubPlugin.command('status')
571
- .option('--repo <path>', 'Repository root')
572
- .option('--json', 'Output JSON')
573
- .action((opts: { repo?: string; json?: boolean }) => output(getGitHubPluginStatus(repoRoot(opts.repo)), opts.json === true));
574
-
575
- githubPlugin.command('configure')
576
- .option('--repo <path>', 'Repository root')
577
- .option('--enable', 'Enable the plugin')
578
- .option('--disable', 'Disable the plugin')
579
- .option('--github-repo <owner/repo>', 'Explicit GitHub repository')
580
- .option('--clear-repository', 'Clear the configured GitHub repository')
581
- .option('--sync-mode <mode>', 'manual or checkpoint')
582
- .option('--include-tasks', 'Mirror Tasks as GitHub sub-issues')
583
- .option('--exclude-tasks', 'Do not mirror Tasks')
584
- .option('--project-owner <owner>', 'GitHub Project owner')
585
- .option('--project-number <number>', 'GitHub Project number')
586
- .option('--clear-project', 'Clear GitHub Project owner and number')
587
- .option('--json', 'Output JSON')
588
- .action((opts: { repo?: string; enable?: boolean; disable?: boolean; githubRepo?: string; clearRepository?: boolean; syncMode?: string; includeTasks?: boolean; excludeTasks?: boolean; projectOwner?: string; projectNumber?: string; clearProject?: boolean; json?: boolean }) => {
589
- if (opts.enable && opts.disable) throw new Error('choose either --enable or --disable');
590
- if (opts.includeTasks && opts.excludeTasks) throw new Error('choose either --include-tasks or --exclude-tasks');
591
- const config = saveGitHubPluginConfig(repoRoot(opts.repo), {
592
- enabled: opts.enable ? true : opts.disable ? false : undefined,
593
- repository: opts.clearRepository ? '' : opts.githubRepo,
594
- syncMode: opts.syncMode === 'checkpoint' ? 'checkpoint' : opts.syncMode === 'manual' ? 'manual' : undefined,
595
- includeTasks: opts.includeTasks ? true : opts.excludeTasks ? false : undefined,
596
- projectOwner: opts.clearProject ? '' : opts.projectOwner,
597
- projectNumber: opts.clearProject ? null : opts.projectNumber ? Number(opts.projectNumber) : undefined,
598
- });
599
- output(config, opts.json === true);
600
- });
601
-
602
- githubPlugin.command('publish')
603
- .argument('<issue-id>', 'Controller Issue ID')
604
- .option('--repo <path>', 'Repository root')
605
- .option('--json', 'Output JSON')
606
- .action((issueId: string, opts: { repo?: string; json?: boolean }) => output(publishIssueWithGitHubPlugin(repoRoot(opts.repo), issueId), opts.json === true));
607
-
608
- githubPlugin.command('refresh')
609
- .argument('<issue-id>', 'Controller Issue ID')
610
- .option('--repo <path>', 'Repository root')
611
- .option('--json', 'Output JSON')
612
- .action((issueId: string, opts: { repo?: string; json?: boolean }) => output(refreshIssueWithGitHubPlugin(repoRoot(opts.repo), issueId), opts.json === true));
613
-
614
- githubPlugin.command('close')
615
- .argument('<issue-id>', 'Controller Issue ID')
616
- .option('--repo <path>', 'Repository root')
617
- .option('--json', 'Output JSON')
618
- .action((issueId: string, opts: { repo?: string; json?: boolean }) => output(closeIssueWithGitHubPlugin(repoRoot(opts.repo), issueId), opts.json === true));
619
-
620
- command.command('github-status')
621
- .description('Check gh authentication, repository resolution, Projects access prerequisites, and Copilot agent-task CLI support')
622
- .option('--repo <path>', 'Repository root')
623
- .option('--github-repo <owner/repo>', 'Explicit GitHub repository')
624
- .option('--clear-repository', 'Clear the configured GitHub repository')
625
- .option('--json', 'Output JSON')
626
- .action((opts: { repo?: string; githubRepo?: string; json?: boolean }) => {
627
- output(getGitHubStatus(repoRoot(opts.repo), opts.githubRepo), opts.json === true);
628
- });
629
-
630
-
631
- command.command('change-verify')
632
- .description('Composite: verify checkout, apply bounded patch, run checks, return first failure inline')
633
- .option('--repo <path>', 'Repository root')
634
- .option('--branch <name>', 'Expected branch')
635
- .option('--head <sha>', 'Expected HEAD')
636
- .option('--patch-file <path>', 'Unified diff to apply')
637
- .option('--check <id>', 'Check id to run (repeatable)', (value, previous: string[]) => [...previous, value], [])
638
- .option('--json', 'Output JSON')
639
- .action((opts: { repo?: string; branch?: string; head?: string; patchFile?: string; check?: string[]; json?: boolean }) => {
640
- const patch = opts.patchFile ? readFileSync(opts.patchFile, 'utf8') : undefined;
641
- const result = repositoryChangeVerify({
642
- repo: opts.repo,
643
- expectedBranch: opts.branch,
644
- expectedHead: opts.head,
645
- patch,
646
- checks: opts.check,
647
- });
648
- output(result, opts.json === true);
649
- if (result.status === 'failed') process.exitCode = 1;
650
- });
651
-
652
-
653
- command.command('run-check-process', { hidden: true })
654
- .description('Internal persisted check process entry')
655
- .requiredOption('--repo <path>', 'Repository root')
656
- .requiredOption('--check-id <id>', 'Registered check id')
657
- .option('--timeout-ms <ms>', 'Bounded check timeout')
658
- .requiredOption('--expected-check-fingerprint <sha256>', 'Expected non-secret check snapshot fingerprint')
659
- .action((opts: { repo: string; checkId: string; timeoutMs?: string; expectedCheckFingerprint: string }) => {
660
- const requestedTimeout = opts.timeoutMs === undefined ? undefined : Number(opts.timeoutMs);
661
- if (requestedTimeout !== undefined && (!Number.isFinite(requestedTimeout) || requestedTimeout <= 0)) {
662
- throw new Error('invalid --timeout-ms');
663
- }
664
- const root = repoRoot(opts.repo);
665
- const snapshot = snapshotControllerCheck(root, opts.checkId);
666
- const actualFingerprint = createHash('sha256')
667
- .update(JSON.stringify(snapshot))
668
- .digest('hex');
669
- if (actualFingerprint !== opts.expectedCheckFingerprint) {
670
- throw new Error('CHECK_SNAPSHOT_CHANGED: registered check changed before Process Runtime execution');
671
- }
672
- const result = runControllerCheck(root, opts.checkId, requestedTimeout, snapshot);
673
- if (result.stdout) process.stdout.write(result.stdout);
674
- if (result.stderr) process.stderr.write(result.stderr.endsWith('\n') ? result.stderr : `${result.stderr}\n`);
675
- process.exitCode = result.ok ? 0 : Math.max(1, result.status || 1);
676
- });
677
-
678
-
679
- return command;
680
- }