great-cto 3.47.0 → 3.48.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 +28 -0
- package/board/.claude-plugin/plugin.json +6 -1
- package/board/packages/board/lib/routes.mjs +19 -0
- package/board/packages/board/lib/work.mjs +136 -0
- package/board/packages/board/public/assets/work-view.js +97 -0
- package/board/packages/board/public/index.html +74 -21
- package/board/scripts/codex-pipeline.mjs +44 -6
- package/board/scripts/lib/codex-pipeline.mjs +6 -4
- package/board/scripts/lib/session-status.mjs +2 -0
- package/board/scripts/lib/work-decisions.mjs +33 -0
- package/board/scripts/lib/work-metrics.mjs +31 -0
- package/board/scripts/lib/work-outcomes.mjs +78 -0
- package/board/scripts/lib/work-tasks.mjs +302 -0
- package/board/scripts/work-task.mjs +26 -0
- package/dist/codex-host.js +1 -1
- package/dist/main.js +44 -3
- package/dist/run.js +321 -0
- package/dist/work-task.js +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Advanced operator contract behind `great-cto task work`; no new daily flow. */
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { listWorkTasks, readWorkTask } from './lib/work-tasks.mjs';
|
|
5
|
+
import { approveWorkDecision, decisionCapabilities } from './lib/work-decisions.mjs';
|
|
6
|
+
import { applyOutcome, readOutcomeEvidence } from './lib/work-outcomes.mjs';
|
|
7
|
+
import { measureWork, compareWork } from './lib/work-metrics.mjs';
|
|
8
|
+
const args = process.argv.slice(2), action = args.shift();
|
|
9
|
+
try {
|
|
10
|
+
const supported = new Set(['--dir','--task','--decision','--revision','--operation','--evidence','--baseline']);
|
|
11
|
+
const opts = {};
|
|
12
|
+
for (let i = 0; i < args.length; i += 2) { if (!supported.has(args[i]) || !args[i+1] || args[i+1].startsWith('--') || opts[args[i]] !== undefined) throw Error('invalid or duplicate operator option'); opts[args[i]] = args[i+1]; }
|
|
13
|
+
const perAction = { metrics: ['--dir','--baseline'], decisions: ['--dir','--task'], approve: ['--dir','--task','--decision','--revision','--operation'], verify: ['--dir','--task','--revision','--operation','--evidence'], complete: ['--dir','--task','--revision','--operation'] };
|
|
14
|
+
if (perAction[action] && Object.keys(opts).some(k => !perAction[action].includes(k))) throw Error('option is not supported for this operation');
|
|
15
|
+
const root = resolve(opts['--dir'] || '.'), taskId = opts['--task'];
|
|
16
|
+
let result;
|
|
17
|
+
if (action === 'metrics') {
|
|
18
|
+
const listing = listWorkTasks(root); if (listing.state === 'degraded') throw Error('task source is degraded; measurements refused');
|
|
19
|
+
const current = measureWork(listing.tasks), baseline = opts['--baseline'] ? readOutcomeEvidence(root, opts['--baseline']).document : null;
|
|
20
|
+
result = { ...current, comparison: compareWork(current, baseline) };
|
|
21
|
+
} else if (action === 'decisions') result = decisionCapabilities(readWorkTask(taskId, { root }));
|
|
22
|
+
else if (action === 'approve') result = approveWorkDecision({ root, taskId, decisionId: opts['--decision'], expectedRevision: Number(opts['--revision']), operationId: opts['--operation'] });
|
|
23
|
+
else if (['verify','complete'].includes(action)) result = applyOutcome({ root, taskId, kind: action, expectedRevision: Number(opts['--revision']), operationId: opts['--operation'], evidenceFile: opts['--evidence'] });
|
|
24
|
+
else throw Error('usage: great-cto task work decisions|approve|verify|complete|metrics --dir PATH [--task UUID --revision N --operation UUID --decision ID --evidence EXTERNAL_FILE --baseline EXTERNAL_FILE]');
|
|
25
|
+
console.log(JSON.stringify(result, null, 2));
|
|
26
|
+
} catch (e) { console.error('work-task: ' + e.message); process.exitCode = 2; }
|
package/dist/codex-host.js
CHANGED
|
@@ -6,8 +6,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
6
6
|
export function codexControllerCandidates(moduleUrl = import.meta.url) {
|
|
7
7
|
const cliRoot = join(dirname(fileURLToPath(moduleUrl)), '..');
|
|
8
8
|
return [
|
|
9
|
-
join(cliRoot, 'board', 'scripts', 'codex-pipeline.mjs'),
|
|
10
9
|
join(cliRoot, '..', '..', 'scripts', 'codex-pipeline.mjs'),
|
|
10
|
+
join(cliRoot, 'board', 'scripts', 'codex-pipeline.mjs'),
|
|
11
11
|
];
|
|
12
12
|
}
|
|
13
13
|
export function runCodexHost(args, { exists = existsSync, spawn = spawnSync } = {}) {
|
package/dist/main.js
CHANGED
|
@@ -101,10 +101,12 @@ function parseArgs(argv) {
|
|
|
101
101
|
args.noLlm = true;
|
|
102
102
|
else if (a === "--host") {
|
|
103
103
|
const v = argv[++i] ?? "";
|
|
104
|
+
args.hostInput = v;
|
|
104
105
|
args.host = (v === "codex" || v === "claude-code") ? v : null;
|
|
105
106
|
}
|
|
106
107
|
else if (a.startsWith("--host=")) {
|
|
107
108
|
const v = a.slice("--host=".length);
|
|
109
|
+
args.hostInput = v;
|
|
108
110
|
args.host = (v === "codex" || v === "claude-code") ? v : null;
|
|
109
111
|
}
|
|
110
112
|
else if (a === "--bind")
|
|
@@ -158,11 +160,16 @@ function parseArgs(argv) {
|
|
|
158
160
|
args.taskArgs = argv.slice(i + 1);
|
|
159
161
|
break;
|
|
160
162
|
}
|
|
163
|
+
else if (a === "run" || a === "status" || a === "resume") {
|
|
164
|
+
args.command = a;
|
|
165
|
+
args.taskArgs = argv.slice(i + 1);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
161
168
|
// Slash-commands surfaced as CLI subcommands so users get a clear hint
|
|
162
169
|
// instead of a confusing usage error. These work only in the chat plugin.
|
|
163
170
|
else if (a === "start" || a === "audit" || a === "inbox" || a === "digest" ||
|
|
164
171
|
a === "review" || a === "doctor" || a === "burn" || a === "save" ||
|
|
165
|
-
a === "
|
|
172
|
+
a === "learn" || a === "agent" ||
|
|
166
173
|
a === "rfc" || a === "release" || a === "ownership" ||
|
|
167
174
|
a === "sec" || a === "poc" || a === "crystallize" ||
|
|
168
175
|
a === "migrate") {
|
|
@@ -719,10 +726,34 @@ async function runUninstall(args) {
|
|
|
719
726
|
warn(`${plan.keptVersions.length} plugin version(s) kept — a running session uses them; run uninstall again after closing it`);
|
|
720
727
|
return r.failed.length ? 1 : 0;
|
|
721
728
|
}
|
|
722
|
-
function printHelp() {
|
|
729
|
+
function printHelp(advanced = false) {
|
|
730
|
+
if (!advanced) {
|
|
731
|
+
log(`great-cto — describe the task, get a checked result
|
|
732
|
+
|
|
733
|
+
Get started:
|
|
734
|
+
great-cto init
|
|
735
|
+
|
|
736
|
+
Everyday use:
|
|
737
|
+
great-cto run "describe the task"
|
|
738
|
+
great-cto status
|
|
739
|
+
great-cto resume
|
|
740
|
+
|
|
741
|
+
Default host: interactive Claude Code with the plugin loaded.
|
|
742
|
+
For controlled Codex add --host codex; run also needs --allow src,tests,docs.
|
|
743
|
+
Use --dir PATH to choose a project, --dry-run to preview, or run --help for options.
|
|
744
|
+
Resume preserves pending decisions; it never approves them.
|
|
745
|
+
|
|
746
|
+
Details:
|
|
747
|
+
great-cto board Open the local dashboard
|
|
748
|
+
great-cto help --advanced All installation, runtime and operator commands`);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
723
751
|
log(`${bold("great-cto")} — one-command install for the great_cto Claude Code plugin
|
|
724
752
|
|
|
725
753
|
${bold("Usage:")}
|
|
754
|
+
npx great-cto run "describe the task" [--host claude-code|codex] [--dir PATH]
|
|
755
|
+
npx great-cto status [RUN_UUID] [--host codex] [--json]
|
|
756
|
+
npx great-cto resume [RUN_UUID] [--host codex]
|
|
726
757
|
npx great-cto install [options] Same as init
|
|
727
758
|
npx great-cto [init] [options] Detect + bootstrap
|
|
728
759
|
npx great-cto board [--port 3141] [--no-open]
|
|
@@ -731,6 +762,7 @@ ${bold("Usage:")}
|
|
|
731
762
|
npx great-cto ci [path] [--no-archetype] [--no-budget]
|
|
732
763
|
npx great-cto mcp [--sse --port N]
|
|
733
764
|
npx great-cto adapt [--dry-run]
|
|
765
|
+
npx great-cto task work decisions|approve|verify|complete|metrics ...
|
|
734
766
|
npx great-cto codex-host doctor|list|start|resume|status ...
|
|
735
767
|
npx great-cto serve [--port 3142]
|
|
736
768
|
npx great-cto upgrade [superpowers|beads] Re-clone companions to latest tag + re-apply overlays
|
|
@@ -1528,7 +1560,7 @@ async function main() {
|
|
|
1528
1560
|
await finish(exitCode);
|
|
1529
1561
|
}
|
|
1530
1562
|
if (args.command === "help") {
|
|
1531
|
-
printHelp();
|
|
1563
|
+
printHelp(rawArgv.includes('--advanced'));
|
|
1532
1564
|
await finish(0);
|
|
1533
1565
|
}
|
|
1534
1566
|
if (args.command === "unknown") {
|
|
@@ -1622,6 +1654,10 @@ async function main() {
|
|
|
1622
1654
|
await finish(2);
|
|
1623
1655
|
}
|
|
1624
1656
|
}
|
|
1657
|
+
if (args.command === "task" && args.taskArgs?.[0] === "work") {
|
|
1658
|
+
const { runWorkTask } = await import("./work-task.js");
|
|
1659
|
+
await finish(runWorkTask(args.taskArgs.slice(1)));
|
|
1660
|
+
}
|
|
1625
1661
|
if (args.command === "task") {
|
|
1626
1662
|
const { runTask } = await import("./worker.js");
|
|
1627
1663
|
await finish(await runTask(args.taskArgs ?? []));
|
|
@@ -1634,6 +1670,11 @@ async function main() {
|
|
|
1634
1670
|
const { runCodexHost } = await import('./codex-host.js');
|
|
1635
1671
|
await finish(runCodexHost(args.taskArgs ?? []));
|
|
1636
1672
|
}
|
|
1673
|
+
if (args.command === "run" || args.command === "status" || args.command === "resume") {
|
|
1674
|
+
const { runDaily } = await import('./run.js');
|
|
1675
|
+
const dailyArgs = [...(args.hostInput !== undefined ? ['--host', args.hostInput] : []), ...(args.taskArgs ?? [])];
|
|
1676
|
+
await finish(runDaily(args.command, dailyArgs, { cwd: args.dir }));
|
|
1677
|
+
}
|
|
1637
1678
|
if (args.command === "serve") {
|
|
1638
1679
|
try {
|
|
1639
1680
|
const { runServe } = await import("./serve.js");
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/** Daily entry points. Adapters reuse host-owned state, checks and permissions. */
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { join, resolve, dirname } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { codexControllerCandidates } from './codex-host.js';
|
|
8
|
+
const sharedTaskPath = codexControllerCandidates().map(p => join(dirname(p), 'lib', 'work-tasks.mjs')).find(existsSync);
|
|
9
|
+
// Variable URL keeps one runtime implementation shared with hooks/controller/board.
|
|
10
|
+
const sharedTasks = sharedTaskPath ? await import(pathToFileURL(sharedTaskPath).href) : null;
|
|
11
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
12
|
+
const terminal = new Set(['done', 'cancelled']);
|
|
13
|
+
export const dailyUsage = `great-cto run "describe the task" [--host claude-code|codex] [--dir PATH]
|
|
14
|
+
great-cto status [--task TASK_UUID | RUN_UUID] [--host claude-code|codex] [--dir PATH] [--json]
|
|
15
|
+
great-cto resume [--task TASK_UUID | RUN_UUID] [--host claude-code|codex] [--dir PATH]
|
|
16
|
+
|
|
17
|
+
Default host: Claude Code (interactive /start and exact-session /resume).
|
|
18
|
+
Status reads task metadata; it does not launch a model.
|
|
19
|
+
Tasks persist goal, optional repeated --accept criteria and host links.
|
|
20
|
+
Use --task UUID for exact task status/resume; --json works for either host.
|
|
21
|
+
Use --operation UUID to replay a launch receipt without dispatching twice.
|
|
22
|
+
Codex runs require --allow PATHS (comma-separated write scope).
|
|
23
|
+
Codex resume selects the only unfinished run in this project; gates stay pending.
|
|
24
|
+
Research tasks use --intent research; controlled scope is report-only.
|
|
25
|
+
Advanced decisions/outcomes/measurements: great-cto task work.
|
|
26
|
+
Use --dry-run to preview a launch without starting an agent.
|
|
27
|
+
Advanced controller options remain available through great-cto codex-host.
|
|
28
|
+
`;
|
|
29
|
+
function parse(action, args, cwd) {
|
|
30
|
+
const out = { host: 'claude-code', dir: cwd, prompt: '', dryRun: false, json: false, help: false, acceptance: [], hostExplicit: false, intent: 'delivery', maxAttempts: 3 };
|
|
31
|
+
const positional = [];
|
|
32
|
+
let literal = false;
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
const arg = args[i];
|
|
35
|
+
if (literal) {
|
|
36
|
+
positional.push(arg);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (arg === '--') {
|
|
40
|
+
literal = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (arg === '--dry-run') {
|
|
44
|
+
out.dryRun = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (arg === '--json') {
|
|
48
|
+
out.json = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (arg === '--help' || arg === '-h') {
|
|
52
|
+
out.help = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const key = arg.split('=')[0];
|
|
56
|
+
if (['--host', '--dir', '--allow', '--accept', '--task', '--operation', '--revision', '--intent', '--max-attempts'].includes(key)) {
|
|
57
|
+
const value = arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : args[++i];
|
|
58
|
+
if (!value || value.startsWith('--'))
|
|
59
|
+
throw Error(`${key} requires a value`);
|
|
60
|
+
if (key === '--host') {
|
|
61
|
+
if (value !== 'codex' && value !== 'claude-code')
|
|
62
|
+
throw Error('host must be claude-code or codex');
|
|
63
|
+
out.host = value;
|
|
64
|
+
out.hostExplicit = true;
|
|
65
|
+
}
|
|
66
|
+
else if (key === '--dir')
|
|
67
|
+
out.dir = resolve(cwd, value);
|
|
68
|
+
else if (key === '--allow')
|
|
69
|
+
out.allow = value;
|
|
70
|
+
else if (key === '--accept')
|
|
71
|
+
out.acceptance.push(value);
|
|
72
|
+
else if (key === '--intent') {
|
|
73
|
+
if (value !== 'delivery' && value !== 'research')
|
|
74
|
+
throw Error('--intent must be delivery or research');
|
|
75
|
+
out.intent = value;
|
|
76
|
+
}
|
|
77
|
+
else if (key === '--max-attempts') {
|
|
78
|
+
out.maxAttempts = Number(value);
|
|
79
|
+
if (!Number.isInteger(out.maxAttempts) || out.maxAttempts < 1 || out.maxAttempts > 5)
|
|
80
|
+
throw Error('--max-attempts must be 1..5');
|
|
81
|
+
}
|
|
82
|
+
else if (key === '--task') {
|
|
83
|
+
if (!uuid.test(value))
|
|
84
|
+
throw Error('--task requires a task UUID');
|
|
85
|
+
out.taskId = value;
|
|
86
|
+
}
|
|
87
|
+
else if (key === '--operation') {
|
|
88
|
+
if (!uuid.test(value))
|
|
89
|
+
throw Error('--operation requires an operation UUID');
|
|
90
|
+
out.operationId = value;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
out.revision = Number(value);
|
|
94
|
+
if (!Number.isInteger(out.revision) || out.revision < 1)
|
|
95
|
+
throw Error('--revision requires a positive integer');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (arg.startsWith('-'))
|
|
99
|
+
throw Error(`unknown option: ${arg}`);
|
|
100
|
+
else
|
|
101
|
+
positional.push(arg);
|
|
102
|
+
}
|
|
103
|
+
if (out.help)
|
|
104
|
+
return out;
|
|
105
|
+
if (action === 'run') {
|
|
106
|
+
out.prompt = positional.join(' ').trim();
|
|
107
|
+
if (!out.prompt)
|
|
108
|
+
throw Error('describe the task: great-cto run "..."');
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
if (positional.length > 1 || (positional[0] && !uuid.test(positional[0])))
|
|
112
|
+
throw Error('expected one run UUID');
|
|
113
|
+
out.id = positional[0];
|
|
114
|
+
}
|
|
115
|
+
if (out.allow && (action !== 'run' || out.host !== 'codex'))
|
|
116
|
+
throw Error('--allow is only supported for run --host codex');
|
|
117
|
+
if (out.json && action !== 'status')
|
|
118
|
+
throw Error('--json is only supported for status');
|
|
119
|
+
if (out.acceptance.length && action !== 'run')
|
|
120
|
+
throw Error('--accept is only supported for run');
|
|
121
|
+
if (out.taskId && action === 'run')
|
|
122
|
+
throw Error('--task is for existing task status/resume');
|
|
123
|
+
if (out.taskId && out.id)
|
|
124
|
+
throw Error('select task or run UUID, not both');
|
|
125
|
+
if (action !== 'run' && args.some(a => a === '--intent' || a.startsWith('--intent=') || a === '--max-attempts' || a.startsWith('--max-attempts=')))
|
|
126
|
+
throw Error('--intent and --max-attempts are start-only');
|
|
127
|
+
if (out.host === 'claude-code' && args.some(a => a === '--max-attempts' || a.startsWith('--max-attempts=')))
|
|
128
|
+
throw Error('native rework budget is host-owned');
|
|
129
|
+
if (out.operationId && action === 'status')
|
|
130
|
+
throw Error('--operation is for execution only');
|
|
131
|
+
if (out.revision && action !== 'resume')
|
|
132
|
+
throw Error('--revision is for resume only');
|
|
133
|
+
if (out.id && out.host !== 'codex')
|
|
134
|
+
throw Error('run UUIDs belong to the controlled host; use --host codex');
|
|
135
|
+
if (action === 'run' && out.host === 'codex' && (!out.allow || out.allow.split(',').some(p => !p.trim()))) {
|
|
136
|
+
throw Error('Codex requires explicit write scope: --allow src,tests,docs');
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
/** Selection never uses filesystem order or UUID order as a proxy for recency. */
|
|
141
|
+
export function selectRun(runs, id) {
|
|
142
|
+
const candidates = id ? runs.filter(r => r.id === id) : runs.filter(r => !terminal.has(r.status));
|
|
143
|
+
if (!candidates.length)
|
|
144
|
+
throw Error(id ? 'run not found in this project' : 'no unfinished run in this project');
|
|
145
|
+
if (candidates.length !== 1)
|
|
146
|
+
throw Error('multiple unfinished runs; choose a UUID from great-cto status --host codex');
|
|
147
|
+
return candidates[0];
|
|
148
|
+
}
|
|
149
|
+
export function runDaily(action, args, { cwd = process.cwd(), exists = existsSync, spawn = spawnSync, write = (text) => { process.stdout.write(text); }, tasks = sharedTasks, taskStore = undefined, fail = (text) => { process.stderr.write(`great-cto: ${text}\n`); }, } = {}) {
|
|
150
|
+
let heldWork = null;
|
|
151
|
+
const release = () => { const lease = heldWork?.lease; heldWork = null; lease?.release(); };
|
|
152
|
+
try {
|
|
153
|
+
const options = parse(action, args, cwd);
|
|
154
|
+
if (options.help) {
|
|
155
|
+
write(dailyUsage);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
if (!exists(options.dir))
|
|
159
|
+
throw Error(`project directory does not exist: ${options.dir}`);
|
|
160
|
+
if (!tasks)
|
|
161
|
+
throw Error('shared task runtime missing; reinstall great-cto');
|
|
162
|
+
const storeOptions = taskStore ? { store: taskStore } : {};
|
|
163
|
+
// Dry runs stay pure; no metadata lookup is needed to preview an explicit host.
|
|
164
|
+
if (options.dryRun) {
|
|
165
|
+
write(JSON.stringify({ host: options.host, action, cwd: options.dir, dir: options.dir,
|
|
166
|
+
...(options.host === 'claude-code' && action !== 'status' ? { command: 'claude', args: [action === 'run' ? `${options.intent === 'research' ? '/audit' : '/start'} ${options.prompt}` : '/resume'] } : {}),
|
|
167
|
+
intent: options.intent, maxAttempts: options.host === 'codex' ? options.maxAttempts : null,
|
|
168
|
+
taskId: options.taskId || null, id: options.id || 'project-scoped selection' }) + '\n');
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
const taskListing = tasks.listWorkTasks(options.dir, storeOptions);
|
|
172
|
+
if (taskListing.state === 'degraded')
|
|
173
|
+
throw Error('task state is unreadable; selection is blocked');
|
|
174
|
+
let selectedTask = null;
|
|
175
|
+
if (options.taskId) {
|
|
176
|
+
selectedTask = taskListing.tasks.find((t) => t.taskId === options.taskId);
|
|
177
|
+
if (!selectedTask)
|
|
178
|
+
throw Error('task not found in this project');
|
|
179
|
+
if (options.hostExplicit && selectedTask.host !== options.host)
|
|
180
|
+
throw Error('task host cannot change during resume');
|
|
181
|
+
options.host = selectedTask.host;
|
|
182
|
+
}
|
|
183
|
+
if (action === 'run' && options.operationId && taskListing.tasks.some((t) => t.operations.some((o) => o.operationId === options.operationId))) {
|
|
184
|
+
const previous = tasks.beginWork({ root: options.dir, host: options.host, goal: options.prompt, acceptance: options.acceptance, intent: options.intent, maxAttempts: options.maxAttempts,
|
|
185
|
+
authority: options.host === 'codex' ? { mode: 'explicit-paths', writeScope: options.allow.split(',').map(p => p.trim()) } : { mode: 'native-interactive', writeScope: null },
|
|
186
|
+
operationId: options.operationId }, storeOptions);
|
|
187
|
+
write(JSON.stringify(tasks.publicWorkTask(previous.task)) + '\n');
|
|
188
|
+
return previous.operation.exitCode ?? 2;
|
|
189
|
+
}
|
|
190
|
+
if (action === 'resume' && options.operationId) {
|
|
191
|
+
const receiptTask = taskListing.tasks.find((t) => t.operations.some((o) => o.operationId === options.operationId));
|
|
192
|
+
if (receiptTask) {
|
|
193
|
+
if ((selectedTask && selectedTask.taskId !== receiptTask.taskId) || (options.id && !receiptTask.links.runs.includes(options.id))
|
|
194
|
+
|| (options.hostExplicit && options.host !== receiptTask.host))
|
|
195
|
+
throw Error('idempotency key conflicts with another task');
|
|
196
|
+
const replay = tasks.beginWork({ root: options.dir, host: receiptTask.host, kind: 'resume', taskId: receiptTask.taskId,
|
|
197
|
+
operationId: options.operationId, expectedRevision: options.revision ?? null }, storeOptions);
|
|
198
|
+
write(JSON.stringify(tasks.publicWorkTask(replay.task)) + '\n');
|
|
199
|
+
return replay.operation.exitCode ?? 2;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (options.host === 'claude-code') {
|
|
203
|
+
if (action === 'status') {
|
|
204
|
+
const shown = selectedTask ? [selectedTask] : taskListing.tasks.filter((t) => t.host === 'claude-code');
|
|
205
|
+
if (options.json)
|
|
206
|
+
write(JSON.stringify({ schemaVersion: 1, state: taskListing.state, tasks: shown.map(tasks.publicWorkTask) }, null, 2) + '\n');
|
|
207
|
+
else
|
|
208
|
+
write(shown.length ? shown.map((t) => `${t.taskId} ${t.phase} ${t.goal}`).join('\n') + '\n' : 'No tracked Claude tasks in this project.\n');
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
if (action === 'resume' && !selectedTask) {
|
|
212
|
+
const candidates = taskListing.tasks.filter((t) => t.host === 'claude-code' && t.phase !== 'cancelled');
|
|
213
|
+
if (candidates.length > 1)
|
|
214
|
+
throw Error('multiple Claude tasks; select --task UUID');
|
|
215
|
+
selectedTask = candidates[0] || null;
|
|
216
|
+
}
|
|
217
|
+
if (action === 'resume' && !selectedTask)
|
|
218
|
+
throw Error('no tracked Claude task; use native /resume for legacy context');
|
|
219
|
+
const work = tasks.beginWork({ root: options.dir, host: options.host, kind: action === 'run' ? 'start' : 'resume',
|
|
220
|
+
goal: action === 'run' ? options.prompt : null, acceptance: options.acceptance, intent: options.intent, maxAttempts: options.maxAttempts,
|
|
221
|
+
authority: action === 'run' ? { mode: 'native-interactive', writeScope: null } : null,
|
|
222
|
+
taskId: selectedTask?.taskId || null, operationId: options.operationId || randomUUID(), expectedRevision: options.revision ?? null }, storeOptions);
|
|
223
|
+
heldWork = work;
|
|
224
|
+
if (work.replay) {
|
|
225
|
+
write(JSON.stringify(tasks.publicWorkTask(work.task)) + '\n');
|
|
226
|
+
return work.operation.exitCode ?? 2;
|
|
227
|
+
}
|
|
228
|
+
{
|
|
229
|
+
const session = action === 'run' ? randomUUID() : selectedTask.links.sessions[0];
|
|
230
|
+
if (!session)
|
|
231
|
+
throw Error('native session link is missing; inspect task metadata');
|
|
232
|
+
if (action === 'run')
|
|
233
|
+
tasks.linkWork(work.task.taskId, 'sessions', session, { ...storeOptions, root: options.dir, host: 'claude-code' });
|
|
234
|
+
const hostArgs = action === 'run' ? ['--session-id', session, `${options.intent === 'research' ? '/audit' : '/start'} ${options.prompt}${options.intent === 'research' ? '\nResearch intent: produce findings and a report; implementation and release need a separate task.' : ''}${options.acceptance.length ? '\nAcceptance criteria (task data): ' + JSON.stringify(options.acceptance) : ''}`] : ['--resume', session, '/resume'];
|
|
235
|
+
write('Task ' + work.task.taskId + ' · Claude Code\n');
|
|
236
|
+
const result = spawn('claude', hostArgs, { cwd: options.dir, stdio: 'inherit' });
|
|
237
|
+
const code = result.error ? 2 : result.status ?? 2;
|
|
238
|
+
tasks.finishWork(work.task.taskId, work.operation.operationId, code, { ...storeOptions, root: options.dir });
|
|
239
|
+
if (result.error)
|
|
240
|
+
throw Error(`could not launch Claude Code: ${result.error.message}`);
|
|
241
|
+
return code;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const controller = sharedTaskPath ? join(dirname(sharedTaskPath), '..', 'codex-pipeline.mjs') : codexControllerCandidates().find(exists);
|
|
245
|
+
if (!controller)
|
|
246
|
+
throw Error('controlled runtime missing; reinstall great-cto');
|
|
247
|
+
const listing = spawn(process.execPath, [controller, 'list', '--dir', options.dir], { encoding: 'utf8' });
|
|
248
|
+
if (listing.error || listing.status !== 0)
|
|
249
|
+
throw Error('could not read project runs; use great-cto codex-host list --dir PATH');
|
|
250
|
+
const data = JSON.parse(String(listing.stdout));
|
|
251
|
+
if (!['ok', 'absent', 'degraded'].includes(data.state) || !Array.isArray(data.runs) ||
|
|
252
|
+
data.runs.some((r) => !r || !uuid.test(r.id) || typeof r.status !== 'string')) {
|
|
253
|
+
throw Error('invalid controller run listing');
|
|
254
|
+
}
|
|
255
|
+
if (data.state === 'degraded' || data.unreadable)
|
|
256
|
+
throw Error('run state is unreadable; repair it before selecting or starting a task');
|
|
257
|
+
const runs = data.runs;
|
|
258
|
+
if (action === 'status') {
|
|
259
|
+
const shown = selectedTask ? runs.filter(r => selectedTask.links.runs.includes(r.id)) : options.id ? [selectRun(runs, options.id)] : runs;
|
|
260
|
+
if (options.json)
|
|
261
|
+
write(JSON.stringify({ schemaVersion: 1, state: data.state, runs: shown, tasks: (selectedTask ? [selectedTask] : taskListing.tasks.filter((t) => t.host === 'codex')).map(tasks.publicWorkTask) }, null, 2) + '\n');
|
|
262
|
+
else {
|
|
263
|
+
write(shown.length ? shown.map(r => `${r.id} ${r.status}${r.pending?.gates?.length ? ` Decision needed: ${r.pending.gates.join(', ')}` : ''}${r.reason ? ` ${r.reason}` : ''}`).join('\n') + '\n' : 'No controlled runs in this project.\n');
|
|
264
|
+
}
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
let forwarded;
|
|
268
|
+
let work = null;
|
|
269
|
+
if (action === 'resume') {
|
|
270
|
+
if (selectedTask && selectedTask.links.runs.length !== 1)
|
|
271
|
+
throw Error('task run link is missing or ambiguous');
|
|
272
|
+
const selected = selectRun(runs, selectedTask?.links.runs[0] || options.id);
|
|
273
|
+
selectedTask ||= taskListing.tasks.find((t) => t.links.runs.includes(selected.id));
|
|
274
|
+
if (terminal.has(selected.status))
|
|
275
|
+
throw Error(`run is ${selected.status}; start a new task instead`);
|
|
276
|
+
if (selectedTask)
|
|
277
|
+
heldWork = work = tasks.beginWork({ root: options.dir, host: 'codex', kind: 'resume', taskId: selectedTask.taskId,
|
|
278
|
+
operationId: options.operationId || randomUUID(), expectedRevision: options.revision ?? null }, storeOptions);
|
|
279
|
+
if (!selectedTask && (options.operationId || options.revision))
|
|
280
|
+
throw Error('legacy run has no task receipt; use advanced controller resume');
|
|
281
|
+
forwarded = ['resume', selected.id];
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
if (runs.some(r => !terminal.has(r.status)))
|
|
285
|
+
throw Error('unfinished task exists; use great-cto resume --host codex or choose a run from status');
|
|
286
|
+
// Existing projects enter architecture; new projects enter product discovery.
|
|
287
|
+
const entry = options.intent === 'research' ? 'project-auditor' : exists(join(options.dir, '.great_cto', 'PROJECT.md')) ? 'architect' : 'product-owner';
|
|
288
|
+
heldWork = work = tasks.beginWork({ root: options.dir, host: 'codex', goal: options.prompt, acceptance: options.acceptance, intent: options.intent, maxAttempts: options.maxAttempts,
|
|
289
|
+
authority: { mode: 'explicit-paths', writeScope: options.allow.split(',').map(p => p.trim()) },
|
|
290
|
+
operationId: options.operationId || randomUUID() }, storeOptions);
|
|
291
|
+
forwarded = ['start', '--dir', options.dir, '--prompt', options.prompt, '--allow', options.allow, '--entry', entry, '--task-id', work.task.taskId];
|
|
292
|
+
}
|
|
293
|
+
if (work?.replay) {
|
|
294
|
+
write(JSON.stringify(tasks.publicWorkTask(work.task)) + '\n');
|
|
295
|
+
return work.operation.exitCode ?? 2;
|
|
296
|
+
}
|
|
297
|
+
{
|
|
298
|
+
const result = spawn(process.execPath, [controller, ...forwarded], { stdio: 'inherit',
|
|
299
|
+
env: { ...process.env, ...(taskStore ? { GREAT_CTO_TASKS_DIR: taskStore } : {}), ...(work ? { GREAT_CTO_WORK_LEASE: work.lease.token } : {}) } });
|
|
300
|
+
const code = result.error ? 2 : result.status ?? 2;
|
|
301
|
+
if (work)
|
|
302
|
+
tasks.finishWork(work.task.taskId, work.operation.operationId, code, { ...storeOptions, root: options.dir });
|
|
303
|
+
if (result.error)
|
|
304
|
+
throw Error(`could not launch controlled runtime: ${result.error.message}`);
|
|
305
|
+
return code;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
if (heldWork && !heldWork.replay) {
|
|
310
|
+
try {
|
|
311
|
+
tasks?.finishWork(heldWork.task.taskId, heldWork.operation.operationId, 2, taskStore ? { store: taskStore } : {});
|
|
312
|
+
}
|
|
313
|
+
catch { /* preserve the original failure; no dispatch retry */ }
|
|
314
|
+
}
|
|
315
|
+
fail(error.message);
|
|
316
|
+
return 2;
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
release();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Advanced task operations share the same source/bundled runtime as daily entry. */
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { codexControllerCandidates } from './codex-host.js';
|
|
6
|
+
export function runWorkTask(args) {
|
|
7
|
+
const script = codexControllerCandidates().map(p => join(dirname(p), 'work-task.mjs')).find(existsSync);
|
|
8
|
+
if (!script) {
|
|
9
|
+
process.stderr.write('great-cto: task operations runtime is missing; reinstall package\n');
|
|
10
|
+
return 2;
|
|
11
|
+
}
|
|
12
|
+
const result = spawnSync(process.execPath, [script, ...args], { stdio: 'inherit' });
|
|
13
|
+
if (result.error) {
|
|
14
|
+
process.stderr.write('great-cto: ' + result.error.message + '\n');
|
|
15
|
+
return 2;
|
|
16
|
+
}
|
|
17
|
+
return result.status ?? 2;
|
|
18
|
+
}
|
package/package.json
CHANGED