great-cto 3.47.0 → 3.49.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 +8 -2
- 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 +78 -23
- package/board/packages/cli/dist/archetypes.js +1 -1
- package/board/scripts/codex-pipeline.mjs +44 -6
- package/board/scripts/lib/codex-pipeline.mjs +6 -4
- package/board/scripts/lib/codex-role-profiles.mjs +1 -0
- package/board/scripts/lib/required-reviewers.mjs +1 -1
- 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/board/shared/pipeline.toml +14 -0
- package/dist/archetypes.js +1 -1
- 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
package/README.md
CHANGED
|
@@ -20,6 +20,34 @@ That's it. The CLI detects your stack, picks the right archetype, clones the plu
|
|
|
20
20
|
|
|
21
21
|
After install, restart Claude Code and run `/inbox` or `/audit`.
|
|
22
22
|
|
|
23
|
+
## Everyday use (source version)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
great-cto run "build, fix or investigate something"
|
|
27
|
+
great-cto status
|
|
28
|
+
great-cto resume
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run launches interactive Claude Code through `/start`; resume targets its exact
|
|
32
|
+
pre-bound session through `/resume`. The plugin must be loaded. Status reads
|
|
33
|
+
private task metadata without launching a model. `/start` handles existing projects without resetting
|
|
34
|
+
their configuration. For the controlled Codex runtime use `--host codex` and,
|
|
35
|
+
when starting, an explicit scope such as `--allow src,tests,docs`.
|
|
36
|
+
`status --json` works for both hosts. `--task UUID` selects persisted task identity;
|
|
37
|
+
Codex status also includes its sanitized run listing. Optional repeated `--accept`
|
|
38
|
+
criteria are stored and passed to execution and verification. `--operation UUID`
|
|
39
|
+
provides idempotent launch receipts, and `resume --revision N` rejects stale intent.
|
|
40
|
+
See [the shared task contract](../../docs/WORK-TASK-CONTRACT.md) for ownership and recovery.
|
|
41
|
+
`resume --host codex` selects only one unfinished run in the target project;
|
|
42
|
+
ambiguity requires a UUID. Pending gates, runtime locks and verifier checks remain
|
|
43
|
+
in force. `--dir PATH` targets another project; `--dry-run` previews without agents.
|
|
44
|
+
Advanced policies, recovery and approvals stay under `codex-host`.
|
|
45
|
+
Research uses explicit `run --intent research`; controlled research writes reports
|
|
46
|
+
only and preserves its bounded stage retry budget. Advanced host approvals, outcome
|
|
47
|
+
attestations and cohort reports are under `task work`, documented in the shared
|
|
48
|
+
contract. Resume progress is measured from bound stage evidence, not exit zero.
|
|
49
|
+
Published npm versions may not include these source additions yet.
|
|
50
|
+
|
|
23
51
|
## What the plugin gives you
|
|
24
52
|
|
|
25
53
|
The CLI installs it; these are what it installs.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "great-cto",
|
|
3
|
-
"description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of
|
|
4
|
-
"version": "3.
|
|
3
|
+
"description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 72 specialist agents \u2014 an independent model checks each stage before the next builds on it, spending caps refuse rather than warn, and three decisions stay yours: what gets built, how, and whether it ships.",
|
|
4
|
+
"version": "3.49.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexander Velikiy",
|
|
7
7
|
"url": "https://hashnode.com/@Greatcto"
|
|
@@ -81,6 +81,11 @@
|
|
|
81
81
|
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed \"s|/$||\")}; [ -n \"$PLUGIN_DIR\" ] && node \"${PLUGIN_DIR}/scripts/hooks/register-project.mjs\" </dev/null 2>/dev/null || true",
|
|
82
82
|
"timeout": 10,
|
|
83
83
|
"statusMessage": "Registering project on the board..."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"type": "command",
|
|
87
|
+
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; PLUGIN_DIR=${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,\"/\"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed \"s|/$||\")}; [ -n \"$PLUGIN_DIR\" ] && node \"${PLUGIN_DIR}/scripts/hooks/learner-report.mjs\" </dev/null 2>/dev/null || true",
|
|
88
|
+
"timeout": 10
|
|
84
89
|
}
|
|
85
90
|
]
|
|
86
91
|
}
|
|
@@ -464,6 +469,7 @@
|
|
|
464
469
|
"./agents-full/regulated-reviewer.md",
|
|
465
470
|
"./agents-full/security-officer.md",
|
|
466
471
|
"./agents-full/senior-dev.md",
|
|
472
|
+
"./agents-full/smart-contract-auditor.md",
|
|
467
473
|
"./agents-full/streaming-reviewer.md",
|
|
468
474
|
"./agents-full/subscription-billing-engineer.md",
|
|
469
475
|
"./agents-full/tax-reviewer.md",
|
|
@@ -40,6 +40,7 @@ import { readVerdicts } from './verdicts.mjs';
|
|
|
40
40
|
import { parseAgentBudgets, upsertAgentBudget, removeAgentBudget } from '../../../scripts/lib/agent-budget.mjs';
|
|
41
41
|
import { resolveSecondOpinion, SECOND_OPINION_PROVIDERS } from '../../../scripts/lib/second-opinion.mjs';
|
|
42
42
|
import { detectCodex } from '../../../scripts/lib/codex-exec.mjs';
|
|
43
|
+
import { getWork } from './work.mjs';
|
|
43
44
|
import { listCodexRuns } from '../../../scripts/lib/codex-host-state.mjs';
|
|
44
45
|
import { upsertCapability, capabilitiesFromProjectMd } from '../../../scripts/lib/stack-capabilities.mjs';
|
|
45
46
|
// Moved to scripts/lib so the cross-review Stop hook can ask the same question
|
|
@@ -99,6 +100,24 @@ async function dispatch(req, res, url, cwd) {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
// New read model refuses project fallback rather than relabeling another project.
|
|
104
|
+
if (pathname === '/api/work') {
|
|
105
|
+
if (requestedProject && resolveProjectInfo(requestedProject).resolved === 'fallback') {
|
|
106
|
+
res.writeHead(404, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
107
|
+
res.end(JSON.stringify({ error: 'Unknown project; work projection was not read' }));
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (req.method !== 'GET') {
|
|
111
|
+
res.writeHead(405, { 'Content-Type': 'application/json', Allow: 'GET' });
|
|
112
|
+
res.end(JSON.stringify({ error: 'Work execution is not connected; use the host CLI' }));
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const snapshot = getWork(cwd);
|
|
116
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
117
|
+
res.end(JSON.stringify(snapshot));
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
// SSE
|
|
103
122
|
if (pathname === '/api/sse') {
|
|
104
123
|
res.writeHead(200, {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** Project-scoped read model. Never invent task/run links or dispatch from a GET. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { join, basename } from 'node:path';
|
|
5
|
+
import { getTasks, getReadDegradation } from './beads.mjs';
|
|
6
|
+
import { listCodexRuns } from '../../../scripts/lib/codex-host-state.mjs';
|
|
7
|
+
import { listWorkTasks, publicWorkTask } from '../../../scripts/lib/work-tasks.mjs';
|
|
8
|
+
import { decisionCapabilities } from '../../../scripts/lib/work-decisions.mjs';
|
|
9
|
+
import { measureWork, compareWork } from '../../../scripts/lib/work-metrics.mjs';
|
|
10
|
+
import { readSessionStatus } from '../../../scripts/lib/session-status.mjs';
|
|
11
|
+
|
|
12
|
+
const text = v => typeof v === 'string' ? v : null;
|
|
13
|
+
const date = v => typeof v === 'string' && Number.isFinite(Date.parse(v)) ? v : null;
|
|
14
|
+
const RUN_PHASE = { ready: 'accepted', 'awaiting-gate': 'needs_decision',
|
|
15
|
+
'awaiting-release': 'needs_decision', blocked: 'blocked', 'manual-action': 'blocked' };
|
|
16
|
+
|
|
17
|
+
export function projectWork({ projectId, issues = [], codex = { state: 'absent', runs: [] },
|
|
18
|
+
tasks = [], sessions = [], sources = [], observedAt = new Date().toISOString() }) {
|
|
19
|
+
const entries = [];
|
|
20
|
+
for (const r of codex.runs || []) {
|
|
21
|
+
// Completion of a controller run is not proof of the user's acceptance criteria.
|
|
22
|
+
const terminal = ['done', 'cancelled'].includes(r.status);
|
|
23
|
+
const active = !!r.active || r.wave?.status === 'running';
|
|
24
|
+
const phase = active ? 'working' : RUN_PHASE[r.status] || null;
|
|
25
|
+
const pending = !!r.pending || ['awaiting-gate', 'awaiting-release'].includes(r.status);
|
|
26
|
+
const canCopyResume = !terminal && !pending && !active && r.status === 'ready';
|
|
27
|
+
const updatedAt = (r.attempts || []).flatMap(a => [date(a.startedAt), date(a.finishedAt)])
|
|
28
|
+
.filter(Boolean).sort((a, b) => Date.parse(b) - Date.parse(a))[0] || date(r.pending?.createdAt);
|
|
29
|
+
entries.push({ key: `run:${r.id}`, kind: 'run', taskId: null, runId: r.id, issueIds: [],
|
|
30
|
+
title: `Controlled run ${r.id}`, goal: null, acceptance: [], host: 'codex',
|
|
31
|
+
phase, nativeState: text(r.status), terminal, updatedAt: updatedAt || null,
|
|
32
|
+
outcome: r.status === 'done' ? 'Run finished; task acceptance is not recorded'
|
|
33
|
+
: r.status === 'cancelled' ? 'Run cancelled' : null,
|
|
34
|
+
reason: text(r.reason), decisions: pending ? [{ id: `run:${r.id}:pending`, engine: 'codex',
|
|
35
|
+
label: r.status === 'awaiting-release' ? 'Release approval in host controller' : 'Approval in host controller' }] : [],
|
|
36
|
+
capabilities: [{ action: 'copy_resume', enabled: canCopyResume,
|
|
37
|
+
reason: canCopyResume ? null : pending ? 'Resolve the host decision first' : active ? 'A worker owns this run'
|
|
38
|
+
: terminal ? 'This run is terminal' : 'This state does not permit ordinary resume' }],
|
|
39
|
+
command: canCopyResume ? `great-cto resume ${r.id} --host codex` : null,
|
|
40
|
+
evidence: (r.rolesCompleted || []).map(role => ({ kind: 'recorded_role', label: role })),
|
|
41
|
+
release: r.release ? { status: text(r.release.status), url: text(r.release.url), verifiedAt: date(r.release.verifiedAt) } : null,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
for (const t of issues) {
|
|
45
|
+
const terminal = ['closed', 'done'].includes(t.raw_status || t.status);
|
|
46
|
+
const pending = !!t.is_gate && !terminal && t.raw_status !== 'blocked' && t.status !== 'blocked';
|
|
47
|
+
entries.push({ key: `issue:${t.id}`, kind: 'issue', taskId: null, runId: null, issueIds: [t.id],
|
|
48
|
+
title: text(t.title) || t.id, goal: null, acceptance: text(t.acceptance) ? [t.acceptance] : [],
|
|
49
|
+
host: null, phase: null, nativeState: text(t.raw_status || t.status), terminal,
|
|
50
|
+
updatedAt: date(t.updated_at), outcome: terminal ? 'Issue closed; task acceptance is not recorded' : null,
|
|
51
|
+
reason: 'Legacy work item; no execution run is linked',
|
|
52
|
+
decisions: pending ? [{ id: t.id, engine: 'beads', label: 'Review decision' }] : [],
|
|
53
|
+
capabilities: [{ action: pending ? 'review_decision' : 'view_issue', enabled: true, reason: null }],
|
|
54
|
+
command: null, evidence: [], release: null,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (const t of tasks) {
|
|
58
|
+
const linkedRuns = entries.filter(e => e.kind === 'run' && t.links.runs.includes(e.runId));
|
|
59
|
+
const linkedIssues = entries.filter(e => e.kind === 'issue' && e.issueIds.some(id => t.links.issues.includes(id)));
|
|
60
|
+
for (const linked of [...linkedRuns, ...linkedIssues]) {
|
|
61
|
+
linked.taskId = t.taskId; linked.goal = t.goal; linked.acceptance = t.acceptance;
|
|
62
|
+
if (!linked.terminal) entries.splice(entries.indexOf(linked), 1);
|
|
63
|
+
}
|
|
64
|
+
const owner = t.operations.some(o => o.state === 'running');
|
|
65
|
+
const run = linkedRuns.length === 1 ? linkedRuns[0] : null;
|
|
66
|
+
const approval = decisionCapabilities(t).find(d => d.capability.enabled);
|
|
67
|
+
const approveCommand = approval ? `great-cto task work approve --task ${t.taskId} --decision ${approval.decisionId} --revision ${t.revision}` : null;
|
|
68
|
+
const enabled = !['cancelled', 'verified', 'completed'].includes(t.phase) && !owner && (t.host === 'codex'
|
|
69
|
+
? !!run?.capabilities.some(c => c.action === 'copy_resume' && c.enabled)
|
|
70
|
+
: t.managed !== false && t.links.sessions.length === 1 && !['needs_decision', 'working'].includes(t.phase));
|
|
71
|
+
entries.push({ key: `task:${t.taskId}`, kind: 'task', taskId: t.taskId, runId: run?.runId || null,
|
|
72
|
+
issueIds: t.links.issues, title: t.goal, goal: t.goal, acceptance: t.acceptance,
|
|
73
|
+
host: t.host, phase: t.phase, nativeState: run?.nativeState || t.phase,
|
|
74
|
+
terminal: ['cancelled', 'completed'].includes(t.phase), updatedAt: t.updatedAt, outcome: t.outcome?.state === 'completed' ? `${t.intent || 'delivery'} outcome explicitly completed` : null, reason: t.reason,
|
|
75
|
+
intent: t.intent || 'delivery', stage: t.stage || null, taskOutcome: t.outcome || null,
|
|
76
|
+
decisions: decisionCapabilities(t).map(d => ({ ...d, id: d.decisionId, label: d.label || 'Host decision' })), evidence: (t.evidence || []).map(e => ({ kind: 'verdict', label: `${e.role}: ${e.verdict || 'not recorded'}` })),
|
|
77
|
+
release: run?.release || null, revision: t.revision, metrics: t.metrics,
|
|
78
|
+
capabilities: [...(approval ? [{ action: 'copy_approve', enabled: true, reason: null }] : []), { action: 'copy_resume', enabled, reason: enabled ? null : owner ? 'Host operation is active; duplicate resume is refused'
|
|
79
|
+
: t.managed === false ? 'Continue this observed session inside its native host'
|
|
80
|
+
: t.phase === 'needs_decision' ? 'Resolve the native host decision first' : 'Execution link or resumable host state is unavailable' }],
|
|
81
|
+
command: approveCommand || (enabled ? `great-cto resume --task ${t.taskId} --host ${t.host} --revision ${t.revision}` : null),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
entries.sort((a, b) => (a.terminal - b.terminal) || (b.decisions.length - a.decisions.length)
|
|
85
|
+
|| (Date.parse(b.updatedAt || '') || 0) - (Date.parse(a.updatedAt || '') || 0) || a.key.localeCompare(b.key));
|
|
86
|
+
const health = sources.some(s => ['degraded', 'unavailable'].includes(s.health)) ? 'degraded' : 'current';
|
|
87
|
+
const measurement = measureWork(tasks);
|
|
88
|
+
const payload = { schemaVersion: 1, projectId, observedAt, health, sources, tasks, entries, measurement, simplification: compareWork(measurement, null),
|
|
89
|
+
sessions: sessions.map(s => ({ session: s.session, state: s.state, since: date(s.since), reason: text(s.reason) })),
|
|
90
|
+
decisions: entries.flatMap(e => e.decisions.map(d => ({ ...d, entryKey: e.key }))),
|
|
91
|
+
execution: { enabled: false, reason: 'This board prepares commands; host execution is not connected' } };
|
|
92
|
+
// A timestamp is not a revision. Stable revision changes only with projected content.
|
|
93
|
+
const { observedAt: ignored, sources: sourceRows, ...content } = payload;
|
|
94
|
+
payload.revision = createHash('sha256').update(JSON.stringify({ ...content,
|
|
95
|
+
sources: sourceRows.map(({ observedAt: ignoredSourceTime, ...s }) => s) })).digest('hex');
|
|
96
|
+
return payload;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getWork(cwd) {
|
|
100
|
+
const observedAt = new Date().toISOString();
|
|
101
|
+
const sources = []; let issues = [], codex = { state: 'absent', runs: [] }, sessions = [], tasks = [];
|
|
102
|
+
const source = (id, health, reason = null) => sources.push({ id, health, reason, observedAt });
|
|
103
|
+
try {
|
|
104
|
+
const listing = listWorkTasks(cwd); tasks = listing.tasks.map(publicWorkTask);
|
|
105
|
+
source('tasks', listing.state === 'degraded' ? 'degraded' : 'current', listing.unreadable ? `${listing.unreadable} task state file(s) could not be read` : null);
|
|
106
|
+
} catch { source('tasks', 'unavailable', 'Cannot read shared task metadata'); }
|
|
107
|
+
try {
|
|
108
|
+
issues = getTasks(cwd);
|
|
109
|
+
const reason = getReadDegradation(cwd);
|
|
110
|
+
source('issues', reason ? 'degraded' : 'current', reason);
|
|
111
|
+
} catch { source('issues', 'unavailable', 'Cannot read project work items'); }
|
|
112
|
+
try {
|
|
113
|
+
codex = listCodexRuns({ root: cwd });
|
|
114
|
+
source('codex', codex.state === 'degraded' ? 'degraded' : 'current', codex.unreadable
|
|
115
|
+
? `${codex.unreadable} run state file(s) could not be read; project membership is unknown` : null);
|
|
116
|
+
} catch { source('codex', 'unavailable', 'Cannot read the host run store'); }
|
|
117
|
+
try {
|
|
118
|
+
// Native reader drops malformed files. Check separately so absence stays honest.
|
|
119
|
+
let files = [];
|
|
120
|
+
try { files = readdirSync(join(cwd, '.great_cto', 'status')).filter(f => f.endsWith('.json')); }
|
|
121
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
122
|
+
let unreadable = 0;
|
|
123
|
+
for (const f of files) {
|
|
124
|
+
try {
|
|
125
|
+
const s = JSON.parse(readFileSync(join(cwd, '.great_cto', 'status', f), 'utf8'));
|
|
126
|
+
if (!text(s.session) || !date(s.since) || !['blocked', 'waiting', 'working'].includes(s.state)) unreadable++;
|
|
127
|
+
} catch { unreadable++; }
|
|
128
|
+
}
|
|
129
|
+
sessions = readSessionStatus(cwd);
|
|
130
|
+
source('claude_sessions', unreadable ? 'degraded' : 'current', unreadable ? `${unreadable} session state file(s) could not be read` : null);
|
|
131
|
+
} catch { source('claude_sessions', 'unavailable', 'Cannot read Claude session status'); }
|
|
132
|
+
let canonical = cwd;
|
|
133
|
+
try { canonical = realpathSync(cwd); } catch { /* source health already reports inaccessible data */ }
|
|
134
|
+
const projectId = 'project:' + createHash('sha256').update(canonical).digest('hex');
|
|
135
|
+
return { ...projectWork({ projectId, issues, codex, tasks, sessions, sources, observedAt }), projectName: basename(cwd) };
|
|
136
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* Read-side work UI; injected callbacks retain existing board authority. */
|
|
2
|
+
(function () {
|
|
3
|
+
const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
4
|
+
const quote = value => "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
5
|
+
function commandFor(goal, host, allow) {
|
|
6
|
+
if (!goal.trim()) throw Error('Describe the result first.');
|
|
7
|
+
if (!['claude-code', 'codex'].includes(host)) throw Error('Choose a supported host.');
|
|
8
|
+
if (host === 'codex' && (!allow.trim() || allow.split(',').some(p => !p.trim()))) throw Error('Codex requires an explicit comma-separated write scope.');
|
|
9
|
+
return `great-cto run --host ${host}` + (host === 'codex' ? ` --allow ${quote(allow.trim())}` : '') + ` -- ${quote(goal.trim())}`;
|
|
10
|
+
}
|
|
11
|
+
const phaseLabel = e => e.outcome || ({ waiting: 'Waiting; acceptance is not confirmed', unknown: 'Execution state is unknown', verified: 'Verified outcome; explicit completion pending', completed: 'Requested outcome explicitly completed', publishing: 'Host publication in progress', cancelled: 'Cancelled', working: 'Host activity recorded; liveness is not measured', accepted: 'Ready for host execution', needs_decision: 'Needs a decision', blocked: 'Blocked' }[e.phase])
|
|
12
|
+
|| (e.kind === 'issue' ? `Issue: ${e.nativeState || 'unknown'}` : `Run: ${e.nativeState || 'unknown'}`);
|
|
13
|
+
function render(snapshot, history = false, filter = '') {
|
|
14
|
+
const warnings = snapshot.sources.filter(s => ['degraded', 'unavailable'].includes(s.health));
|
|
15
|
+
const rows = snapshot.entries.filter(e => e.terminal === history && (!filter || `${e.title} ${e.key}`.toLowerCase().includes(filter.toLowerCase())));
|
|
16
|
+
return `<p class="work-meta">Observed ${esc(snapshot.observedAt)} · ${snapshot.health === 'current' ? 'Sources read' : 'Sources incomplete'}</p>`
|
|
17
|
+
+ warnings.map(s => `<p class="work-warning">${esc(s.id)}: ${esc(s.reason || s.health)}</p>`).join('')
|
|
18
|
+
+ (rows.length ? rows.map(e => {
|
|
19
|
+
const cap = e.capabilities.find(c => c.enabled);
|
|
20
|
+
const action = cap ? `<button type="button" class="gate-btn" data-work-key="${esc(e.key)}" data-work-action="${esc(cap.action)}">${cap.action === 'copy_approve' ? 'Copy approval command' : cap.action === 'copy_resume' ? 'Copy resume command' : cap.action === 'review_decision' ? 'Review decision' : 'Open issue'}</button>` : '';
|
|
21
|
+
return `<article class="work-card" data-entry-key="${esc(e.key)}"><h2>${esc(e.title)}</h2><p>${esc(phaseLabel(e))}</p>`
|
|
22
|
+
+ `<p class="work-meta">${esc(e.host || 'Host not linked')} · ${e.updatedAt ? esc(e.updatedAt) : 'Update time not recorded'}</p>`
|
|
23
|
+
+ (e.acceptance.length ? `<p>Acceptance: ${esc(e.acceptance.join('; '))}</p>` : '')
|
|
24
|
+
+ `<p>${esc(e.reason || 'Execution observation is recorded; acceptance is not inferred.')}</p>`
|
|
25
|
+
+ e.decisions.map(d => `<p>${esc(d.label)}${d.engine === 'codex' ? ' · resolve in the host controller' : ''}</p>`).join('')
|
|
26
|
+
+ action + (e.command ? `<pre>${esc(e.command)}</pre>` : '')
|
|
27
|
+
+ (!cap && e.capabilities[0]?.reason ? `<p>${esc(e.capabilities[0].reason)}</p>` : '')
|
|
28
|
+
+ `<details><summary>Technical details</summary><p>${esc(e.key)} · ${esc(e.nativeState || 'unknown')}</p>`
|
|
29
|
+
+ `<p>Task linkage: ${esc(e.taskId || 'not recorded')}</p>${e.revision ? `<p>Task revision: ${esc(e.revision)}</p>` : ''}`
|
|
30
|
+
+ (e.taskOutcome ? `<p>Outcome evidence: ${esc(e.taskOutcome.source || 'operator-attestation')} · ${esc(e.taskOutcome.state)}</p>` : '')
|
|
31
|
+
+ `<p>Recorded roles: ${esc(e.evidence.map(v => v.label).join(', ') || 'none recorded; verification is not inferred')}</p>`
|
|
32
|
+
+ (e.release ? `<p>Release: ${esc(e.release.status || 'unknown')} · verified ${esc(e.release.verifiedAt || 'not recorded')}</p>` + (typeof e.release.url === 'string' && /^https?:\/\//i.test(e.release.url) ? `<p><a href="${esc(e.release.url)}" target="_blank" rel="noopener noreferrer">Open release result</a></p>` : '') : '')
|
|
33
|
+
+ `</details></article>`;
|
|
34
|
+
}).join('') : `<p>${warnings.length ? 'No readable entries match this view; sources are incomplete.' : filter ? 'No entries match the search.' : history ? 'No closed work records or terminal runs recorded.' : 'No active work records or runs recorded. Describe a result to prepare a host command.'}</p>`)
|
|
35
|
+
+ (!history && snapshot.sessions.length ? `<details><summary>Claude sessions (${snapshot.sessions.length}) · task linkage not recorded</summary>`
|
|
36
|
+
+ snapshot.sessions.map(s => `<p>${esc(s.session)} · ${esc(s.state)} · ${esc(s.reason || '')}</p>`).join('') + '</details>' : '');
|
|
37
|
+
}
|
|
38
|
+
let config, snapshot, generation = 0, filter = '';
|
|
39
|
+
const status = message => { document.getElementById('work-feedback').textContent = message; };
|
|
40
|
+
async function refresh() {
|
|
41
|
+
const seq = ++generation, project = config.project();
|
|
42
|
+
try {
|
|
43
|
+
const data = await config.read(`/api/work${project ? '?project=' + encodeURIComponent(project) : ''}`);
|
|
44
|
+
if (seq !== generation || project !== config.project()) return;
|
|
45
|
+
if (!data || data.error || data.schemaVersion !== 1) throw Error(data?.error || 'Cannot read work state. Check the board connection and refresh.');
|
|
46
|
+
if (snapshot?.revision === data.revision) return;
|
|
47
|
+
snapshot = data; paint();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (seq !== generation || project !== config.project()) return;
|
|
50
|
+
snapshot = null;
|
|
51
|
+
for (const id of ['work-list', 'history-list']) document.getElementById(id).textContent = error.message;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function paint() {
|
|
55
|
+
if (!snapshot) return;
|
|
56
|
+
const expanded = [...document.querySelectorAll('.work-card details[open]')].map(d => d.closest('[data-entry-key]').dataset.entryKey);
|
|
57
|
+
const focused = document.activeElement;
|
|
58
|
+
const focusKey = focused?.dataset?.workKey, focusAction = focused?.dataset?.workAction;
|
|
59
|
+
document.getElementById('work-list').innerHTML = render(snapshot, false, filter);
|
|
60
|
+
document.getElementById('history-list').innerHTML = render(snapshot, true, filter);
|
|
61
|
+
for (const card of document.querySelectorAll('[data-entry-key]')) if (expanded.includes(card.dataset.entryKey)) card.querySelector('details').open = true;
|
|
62
|
+
if (focusKey) [...document.querySelectorAll('[data-work-key]')].find(b => b.dataset.workKey === focusKey && b.dataset.workAction === focusAction)?.focus();
|
|
63
|
+
}
|
|
64
|
+
function reset() {
|
|
65
|
+
generation++; snapshot = null; status('');
|
|
66
|
+
for (const id of ['work-list', 'history-list']) document.getElementById(id).textContent = 'Loading project work…';
|
|
67
|
+
refresh();
|
|
68
|
+
}
|
|
69
|
+
async function copy(command) {
|
|
70
|
+
try { await navigator.clipboard.writeText(command); status('Command copied. Run it in a terminal in the selected project; execution has not started here.'); }
|
|
71
|
+
catch { status('Clipboard unavailable. Select and copy the displayed command.'); }
|
|
72
|
+
}
|
|
73
|
+
function bind(options) {
|
|
74
|
+
config = options;
|
|
75
|
+
document.getElementById('work-compose').addEventListener('submit', event => {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
try {
|
|
78
|
+
const cmd = commandFor(document.getElementById('work-goal').value, document.getElementById('work-host').value, document.getElementById('work-allow').value);
|
|
79
|
+
document.getElementById('work-command').textContent = cmd; copy(cmd);
|
|
80
|
+
} catch (error) { document.getElementById('work-command').textContent = ''; status(error.message); }
|
|
81
|
+
});
|
|
82
|
+
document.getElementById('work-host').addEventListener('change', event => {
|
|
83
|
+
document.getElementById('work-allow-row').hidden = event.target.value !== 'codex';
|
|
84
|
+
document.getElementById('work-command').textContent = ''; status('');
|
|
85
|
+
});
|
|
86
|
+
for (const id of ['work-list', 'history-list']) document.getElementById(id).addEventListener('click', event => {
|
|
87
|
+
const button = event.target.closest('[data-work-action]'); if (!button || !snapshot) return;
|
|
88
|
+
const entry = snapshot.entries.find(e => e.key === button.dataset.workKey);
|
|
89
|
+
const action = entry?.capabilities.find(c => c.action === button.dataset.workAction && c.enabled);
|
|
90
|
+
if (!action) return;
|
|
91
|
+
if (action.action === 'copy_resume' || action.action === 'copy_approve') copy(entry.command);
|
|
92
|
+
else options.openIssue(entry.issueIds[0], action.action === 'review_decision');
|
|
93
|
+
});
|
|
94
|
+
setInterval(() => { if (!document.hidden && options.visible()) refresh(); }, 10000);
|
|
95
|
+
}
|
|
96
|
+
window.GctoWork = { bind, refresh, reset, render, commandFor, search(value) { filter = value; paint(); } };
|
|
97
|
+
})();
|
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
--status-review-text: #00d97e; /* bright on dark: the bed and the ink can be one colour */
|
|
109
109
|
--status-done: #00d97e;
|
|
110
110
|
--status-blocked: #ff5466;
|
|
111
|
+
--status-blocked-ink: #ff5466; /* blocked-status TEXT on a tinted surface */
|
|
111
112
|
--status-gate: #a78bfa;
|
|
112
113
|
|
|
113
114
|
--p0-bg: rgba(255, 84, 102, 0.14); --p0-fg: #fca5a5;
|
|
@@ -198,6 +199,7 @@
|
|
|
198
199
|
--status-review-text: #097448;
|
|
199
200
|
--status-done: #0a7d4d;
|
|
200
201
|
--status-blocked: #c43030;
|
|
202
|
+
--status-blocked-ink: #a32626; /* #c43030 reads 4.08:1 on the red-tinted banner; this is 5.45:1 */
|
|
201
203
|
--status-gate: #7c3aed;
|
|
202
204
|
--p0-bg: rgba(196, 48, 48, 0.12); --p0-fg: #b91c1c;
|
|
203
205
|
--p1-bg: rgba(180, 83, 9, 0.12); --p1-fg: #a64c08;
|
|
@@ -378,7 +380,7 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
378
380
|
|
|
379
381
|
/* The budgets panel. A cap with no consumption beside it cannot answer the only
|
|
380
382
|
question worth asking of it, so every row carries the state and the spend. */
|
|
381
|
-
.ab-title { font-weight:
|
|
383
|
+
.ab-title { font-weight: 500; margin-bottom: 8px; }
|
|
382
384
|
.ab-row {
|
|
383
385
|
display: flex; align-items: center; gap: 10px;
|
|
384
386
|
padding: 4px 0; border-top: 1px solid var(--border);
|
|
@@ -886,6 +888,10 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
886
888
|
color: var(--text3);
|
|
887
889
|
text-align: center;
|
|
888
890
|
}
|
|
891
|
+
/* A <small> hint inside an empty state rendered at the browser's "smaller"
|
|
892
|
+
(13px × 5/6 = 10.83px), off the type scale; the layout gate caught it whenever
|
|
893
|
+
a fresh board had no logs. Pin it to the caption size. */
|
|
894
|
+
.empty small { font-size: var(--fs-caption); }
|
|
889
895
|
|
|
890
896
|
/* A read failed. Deliberately louder than .empty — the whole point is that this
|
|
891
897
|
must not be mistaken for "there is nothing here". */
|
|
@@ -917,7 +923,7 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
917
923
|
border-color: var(--status-blocked);
|
|
918
924
|
background: var(--rail-fail-bg);
|
|
919
925
|
}
|
|
920
|
-
.project-fallback-banner strong { color: var(--status-blocked); }
|
|
926
|
+
.project-fallback-banner strong { color: var(--status-blocked-ink); }
|
|
921
927
|
.project-fallback-banner span { color: var(--text); font-family: var(--sans); }
|
|
922
928
|
|
|
923
929
|
/* ── Side panel ───────────────────────────────────────────────────────────── */
|
|
@@ -2565,7 +2571,7 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
2565
2571
|
/* ── Agents installed list (legacy — kept for fallback) ───────────────── */
|
|
2566
2572
|
.agents-installed { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; margin-top: 4px; }
|
|
2567
2573
|
/* Agent budget bars (Paperclip cascading budget pattern) */
|
|
2568
|
-
.ab-title { font-weight:
|
|
2574
|
+
.ab-title { font-weight: 500; color: var(--text2); margin-bottom: 6px; }
|
|
2569
2575
|
.ab-row { display: flex; justify-content: space-between; padding: 2px 0; }
|
|
2570
2576
|
.ab-name { font-family: var(--mono, monospace); color: var(--accent-text); }
|
|
2571
2577
|
.ab-cap { color: var(--text3); }
|
|
@@ -3261,9 +3267,27 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3261
3267
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
3262
3268
|
::-webkit-scrollbar-thumb { background: rgba(15, 17, 21, 0.12); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; }
|
|
3263
3269
|
::-webkit-scrollbar-thumb:hover { background: rgba(15, 17, 21, 0.22); border: 2px solid transparent; background-clip: content-box; }
|
|
3270
|
+
/* Task read-side: reuse the board's measured typography and colour tokens. */
|
|
3271
|
+
.nav-views[hidden], #nav-views-label[hidden], #work-allow-row[hidden] { display: none; }
|
|
3272
|
+
.work-page { padding: 24px; max-width: 1000px; }
|
|
3273
|
+
.work-card, #work-compose { padding: 16px; border: 1px solid var(--border); border-radius: 8px; margin: 12px 0; background: var(--bg-card); }
|
|
3274
|
+
.work-page h1 { font-size: var(--fs-title-l); font-weight: 500; }
|
|
3275
|
+
.work-page .gate-btn { padding: 8px 12px; min-height: 36px; border: 1px solid var(--border-strong); border-radius: 4px; background: var(--bg-solid); color: var(--on-solid); cursor: pointer; }
|
|
3276
|
+
.work-page .gate-btn:hover { background: var(--bg-solid-hover); }
|
|
3277
|
+
.work-card a { color: var(--text); text-decoration: underline; }
|
|
3278
|
+
.work-card h2 { font-size: var(--fs-title-s); }
|
|
3279
|
+
.work-card p, #work-compose p { margin: 8px 0; font-size: var(--fs-body); }
|
|
3280
|
+
.work-meta { color: var(--text2); font-size: var(--fs-caption); }
|
|
3281
|
+
.work-warning { color: var(--absent-warn-fg); background: var(--bg-card); border: 1px solid var(--absent-warn-fg); padding: 12px; }
|
|
3282
|
+
.work-page pre { font-weight: 400; white-space: pre-wrap; overflow-wrap: anywhere; margin: 12px 0; }
|
|
3283
|
+
#work-compose textarea, #work-compose input, #work-compose select { display: block; width: 100%; margin: 8px 0 12px; padding: 8px; background: var(--bg-muted); color: var(--text); border: 1px solid var(--border-strong); border-radius: 4px; }
|
|
3284
|
+
#tools-nav summary { padding: 12px 16px; cursor: pointer; }
|
|
3285
|
+
@media (min-width: 768px) and (max-width: 1199px) { .nav-item[data-tab="work"] > span, .nav-item[data-tab="history"] > span { display: inline; font-size: var(--fs-caption); } #tools-nav summary { padding: 12px 4px; } }
|
|
3286
|
+
@media (max-width: 767px) { .work-page { padding: 12px; } .work-page button, #work-compose select { min-height: 44px; } }
|
|
3264
3287
|
</style>
|
|
3265
3288
|
</head>
|
|
3266
3289
|
<body>
|
|
3290
|
+
<script src="/assets/work-view.js"></script>
|
|
3267
3291
|
<div class="board">
|
|
3268
3292
|
<!-- Subtle emerald glow background -->
|
|
3269
3293
|
<div class="board-bg" aria-hidden="true"></div>
|
|
@@ -3299,21 +3323,17 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3299
3323
|
<div id="proj-menu-list"></div>
|
|
3300
3324
|
</div>
|
|
3301
3325
|
</div>
|
|
3302
|
-
<!-- Nav shell cutover (great_cto-ki1x.5): eight tabs (+ a topbar share
|
|
3303
|
-
button and bell) become four destinations. Tasks/kanban, Metrics/
|
|
3304
|
-
dashboard, Docs, Activity/logs, Notifications and Share lose their
|
|
3305
|
-
entry point here — their panels and functions are untouched, so nothing
|
|
3306
|
-
that already linked to `#dashboard`/`switchTab('budgets', …)`/etc. goes
|
|
3307
|
-
dead; a bookmark just stops appearing in the menu. Settings — the
|
|
3308
|
-
future home for the Share/Notifications rows this detaches — is
|
|
3309
|
-
reachable from the topbar gear, not a fifth item here (kept under the
|
|
3310
|
-
≤5 ceiling `ui-ux-pro-max` sets for the same nav to also serve mobile). -->
|
|
3311
3326
|
<nav class="nav" id="nav-tablist" role="tablist" aria-orientation="vertical">
|
|
3312
|
-
<div class="nav-item active" data-tab="
|
|
3327
|
+
<div class="nav-item active" data-tab="work" role="tab" tabindex="0" aria-label="Work" title="Work" onkeydown="navTabKeydown(event, this)" onclick="switchTab('work', this)"><span>Work</span></div>
|
|
3328
|
+
<div class="nav-item" data-tab="decisions" role="tab" tabindex="0" aria-label="Decisions" title="Decisions" onkeydown="navTabKeydown(event, this)" onclick="switchTab('decisions', this)">
|
|
3313
3329
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 12h-6l-2 3h-4l-2-3H2 M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
|
|
3314
3330
|
<span>Decisions</span>
|
|
3315
3331
|
<span class="count" id="nav-inbox-count">0</span>
|
|
3316
3332
|
</div>
|
|
3333
|
+
<div class="nav-item" data-tab="history" role="tab" tabindex="0" aria-label="History" title="History" onkeydown="navTabKeydown(event, this)" onclick="switchTab('history', this)"><span>History</span></div>
|
|
3334
|
+
</nav>
|
|
3335
|
+
<details id="tools-nav"><summary>Tools</summary>
|
|
3336
|
+
<nav class="nav" role="tablist" aria-label="Technical tools" aria-orientation="vertical">
|
|
3317
3337
|
<div class="nav-item" data-tab="ledger" role="tab" tabindex="0" aria-label="Ledger" title="Ledger" onkeydown="navTabKeydown(event, this)" onclick="switchTab('ledger', this)">
|
|
3318
3338
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 1v22 M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
|
3319
3339
|
<span>Ledger</span>
|
|
@@ -3336,14 +3356,15 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3336
3356
|
<span class="count" id="nav-harness-count"></span>
|
|
3337
3357
|
</div>
|
|
3338
3358
|
</nav>
|
|
3359
|
+
</details>
|
|
3339
3360
|
<div class="nav-divider"></div>
|
|
3340
|
-
<div class="nav-views-label" id="nav-views-label">Views</div>
|
|
3361
|
+
<div class="nav-views-label" id="nav-views-label" hidden>Views</div>
|
|
3341
3362
|
<!-- Saved views over Fleet (DESIGN §5 point 2). Static for this shell —
|
|
3342
3363
|
filtering the fleet list is the Fleet screen body's job (.8), not this
|
|
3343
3364
|
task's; deliberately not `.nav-item`/`role="tab"`, since they neither
|
|
3344
3365
|
switch a top-level destination nor do anything yet, and a control that
|
|
3345
3366
|
looks live and isn't fails `ui-ux-pro-max` empty-nav-state. -->
|
|
3346
|
-
<div class="nav-views">
|
|
3367
|
+
<div class="nav-views" hidden>
|
|
3347
3368
|
<div class="nav-view-item" id="nav-view-needs" role="button" tabindex="0" onclick="setFleetView('needs')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();setFleetView('needs')}">Needs attention</div>
|
|
3348
3369
|
<div class="nav-view-item" id="nav-view-expensive" role="button" tabindex="0" onclick="setFleetView('expensive')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();setFleetView('expensive')}">Expensive grants</div>
|
|
3349
3370
|
<div class="nav-view-item" id="nav-view-never" role="button" tabindex="0" onclick="setFleetView('never')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();setFleetView('never')}">Never observed</div>
|
|
@@ -3398,8 +3419,24 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3398
3419
|
</div>
|
|
3399
3420
|
</div>
|
|
3400
3421
|
|
|
3422
|
+
<div class="panel active" id="panel-work" role="tabpanel">
|
|
3423
|
+
<div class="work-page"><h1>Work</h1>
|
|
3424
|
+
<form id="work-compose">
|
|
3425
|
+
<label for="work-goal">What result do you need?</label>
|
|
3426
|
+
<textarea id="work-goal" rows="3" required placeholder="Add CSV export with permission checks"></textarea>
|
|
3427
|
+
<label for="work-host">Execution host</label>
|
|
3428
|
+
<select id="work-host"><option value="claude-code">Claude Code</option><option value="codex">Controlled Codex</option></select>
|
|
3429
|
+
<div id="work-allow-row" hidden><label for="work-allow">Codex write scope (comma-separated paths)</label><input id="work-allow" placeholder="src,tests,docs" /></div>
|
|
3430
|
+
<p>Prepares a command. Run it in a terminal in the selected project. The board does not start a host session.</p>
|
|
3431
|
+
<button type="submit" class="gate-btn">Copy command</button><pre id="work-command"></pre>
|
|
3432
|
+
</form>
|
|
3433
|
+
<p id="work-feedback" role="status" aria-live="polite"></p>
|
|
3434
|
+
<div id="work-list">Loading project work…</div>
|
|
3435
|
+
</div>
|
|
3436
|
+
</div>
|
|
3437
|
+
<div class="panel" id="panel-history" role="tabpanel"><div class="work-page"><h1>History</h1><p>Closed issues and terminal runs. Closing a record does not prove task acceptance.</p><div id="history-list">Loading project work…</div></div></div>
|
|
3401
3438
|
<!-- Inbox -->
|
|
3402
|
-
<div class="panel
|
|
3439
|
+
<div class="panel" id="panel-inbox">
|
|
3403
3440
|
<div class="inbox-page">
|
|
3404
3441
|
<!-- Fidelity pass (v3.27.1): the design's header is the screen's name and
|
|
3405
3442
|
its count; the sentence the board used to shout in 52px display
|
|
@@ -3937,7 +3974,13 @@ const PRIORITY_LABEL = ['Urgent','High','Medium','Low'];
|
|
|
3937
3974
|
let allTasks = [];
|
|
3938
3975
|
let metrics = {};
|
|
3939
3976
|
let shareState = { enabled: false, url: null };
|
|
3940
|
-
let currentTab = '
|
|
3977
|
+
let currentTab = 'work';
|
|
3978
|
+
window.GctoWork.bind({
|
|
3979
|
+
project: () => new URLSearchParams(location.search).get('project') || '',
|
|
3980
|
+
visible: () => currentTab === 'work' || currentTab === 'history',
|
|
3981
|
+
read: url => api(url),
|
|
3982
|
+
openIssue: (id, decision) => { if (decision) switchTab('decisions'); openSideById(id); },
|
|
3983
|
+
});
|
|
3941
3984
|
let searchQuery = '';
|
|
3942
3985
|
let allProjects = [];
|
|
3943
3986
|
let currentProject = '';
|
|
@@ -5934,7 +5977,7 @@ async function renderHarnesses() {
|
|
|
5934
5977
|
|
|
5935
5978
|
body.innerHTML = `
|
|
5936
5979
|
<div class="mp-card harness-hosts">
|
|
5937
|
-
<div><span class="dot ok"></span><b>Claude Code</b> <span class="mono cap muted">host · v${esc(h.claude_code?.version || '')}</span><span class="cap muted" style="margin-left:auto" title="
|
|
5980
|
+
<div><span class="dot ok"></span><b>Claude Code</b> <span class="mono cap muted">host · v${esc(h.claude_code?.version || '')}</span><span class="cap muted" style="margin-left:auto" title="72 agents · commands · skills · hooks">full pipeline</span></div>
|
|
5938
5981
|
<div><span class="${codexDot}"></span><b>OpenAI Codex</b> <span class="mono cap">${codexState}</span>${cx.version ? `<span class="mono cap muted">${esc(cx.version)}</span>` : ''}${cx.model ? `<span class="mono cap muted" style="margin-left:auto">${esc(cx.model)}</span>` : ''}</div>
|
|
5939
5982
|
</div>
|
|
5940
5983
|
<div class="mp-card" style="margin-top:12px">
|
|
@@ -7003,6 +7046,7 @@ async function switchProject(slug) {
|
|
|
7003
7046
|
// Close existing SSE so it doesn't push the wrong project's data
|
|
7004
7047
|
if (window._gctoSSE) { try { window._gctoSSE.close(); } catch {} window._gctoSSE = null; }
|
|
7005
7048
|
currentProject = slug;
|
|
7049
|
+
window.GctoWork.reset();
|
|
7006
7050
|
// Optimistic UI clear
|
|
7007
7051
|
allTasks = [];
|
|
7008
7052
|
renderKanban([]);
|
|
@@ -7069,6 +7113,7 @@ function filterTasks(q) {
|
|
|
7069
7113
|
}
|
|
7070
7114
|
|
|
7071
7115
|
function onSearch(q) {
|
|
7116
|
+
window.GctoWork.search(q);
|
|
7072
7117
|
searchQuery = q;
|
|
7073
7118
|
renderKanban(filterTasks(q));
|
|
7074
7119
|
document.getElementById('topbar-task-count').textContent =
|
|
@@ -7184,12 +7229,13 @@ async function init() {
|
|
|
7184
7229
|
// Build version badge (fire-and-forget — never blocks the board).
|
|
7185
7230
|
api('/api/version').then(v => { const el = document.getElementById('build-version'); if (el && v && v.version) el.textContent = 'v' + v.version; }).catch(() => {});
|
|
7186
7231
|
|
|
7187
|
-
const [tasks, met, share, projs, inbox] = await Promise.all([
|
|
7232
|
+
const [tasks, met, share, projs, inbox, serverWork] = await Promise.all([
|
|
7188
7233
|
api(`/api/tasks${qs}`),
|
|
7189
7234
|
api(`/api/metrics${qs}`),
|
|
7190
7235
|
api(`/api/share${qs}`),
|
|
7191
7236
|
api('/api/projects'),
|
|
7192
7237
|
api(`/api/inbox${qs}`),
|
|
7238
|
+
api(`/api/work${qs}`),
|
|
7193
7239
|
]);
|
|
7194
7240
|
allTasks = tasks || [];
|
|
7195
7241
|
metrics = met || {};
|
|
@@ -7206,11 +7252,13 @@ async function init() {
|
|
|
7206
7252
|
const fromTaskId = (allTasks[0]?.id || '').split('-')[0];
|
|
7207
7253
|
const registered = (allProjects || []).some((p) => p.slug === fromTaskId) ? fromTaskId : '';
|
|
7208
7254
|
const proj = urlProject
|
|
7255
|
+
|| serverWork?.projectName
|
|
7209
7256
|
|| registered
|
|
7210
7257
|
|| (allProjects[0]?.slug)
|
|
7211
7258
|
|| fromTaskId
|
|
7212
7259
|
|| 'project';
|
|
7213
7260
|
currentProject = proj;
|
|
7261
|
+
window.GctoWork.reset();
|
|
7214
7262
|
document.getElementById('project-name').textContent = proj;
|
|
7215
7263
|
document.getElementById('crumb-project').textContent = proj;
|
|
7216
7264
|
document.title = `${proj} · great_cto`;
|
|
@@ -7388,6 +7436,7 @@ function connectSSE() {
|
|
|
7388
7436
|
try { renderKanban(filterTasks(searchQuery)); } catch (e) { console.warn('SSE renderKanban failed', e); }
|
|
7389
7437
|
checkBoardFreshness();
|
|
7390
7438
|
refreshInbox();
|
|
7439
|
+
if (currentTab === 'work' || currentTab === 'history') window.GctoWork.refresh();
|
|
7391
7440
|
};
|
|
7392
7441
|
es.addEventListener('tasks', e => {
|
|
7393
7442
|
const newTasks = JSON.parse(e.data);
|
|
@@ -8285,6 +8334,8 @@ let cmdkIndex = 0;
|
|
|
8285
8334
|
|
|
8286
8335
|
function cmdkActions() {
|
|
8287
8336
|
const out = [
|
|
8337
|
+
{ group: 'Go', label: 'Work — goals and execution', run: () => switchTab('work') },
|
|
8338
|
+
{ group: 'Go', label: 'History — recorded outcomes', run: () => switchTab('history') },
|
|
8288
8339
|
{ group: 'Go', label: 'Decisions — what needs you', run: () => switchTab('decisions') },
|
|
8289
8340
|
{ group: 'Go', label: 'Ledger — what it cost, what is running', run: () => switchTab('ledger') },
|
|
8290
8341
|
{ group: 'Go', label: 'Fleet — the agents and what they may do', run: () => switchTab('fleet') },
|
|
@@ -8481,13 +8532,17 @@ try {
|
|
|
8481
8532
|
// panels, not keys.
|
|
8482
8533
|
const DEST = { decisions: 'inbox', ledger: 'budgets', fleet: 'agents', dashboard: 'budgets', share: 'settings', notifications: 'settings' };
|
|
8483
8534
|
const NAV = { inbox: 'decisions', budgets: 'ledger', agents: 'fleet', dashboard: 'ledger', share: 'settings', notifications: 'settings' };
|
|
8484
|
-
const ROUTES = { decisions: 1, ledger: 1, fleet: 1, harness: 1, settings: 1, kanban: 1 };
|
|
8535
|
+
const ROUTES = { work: 1, history: 1, decisions: 1, ledger: 1, fleet: 1, harness: 1, settings: 1, kanban: 1 };
|
|
8485
8536
|
function switchTab(id, el) {
|
|
8486
8537
|
const lid = DEST[id] || id; // the panel's own name
|
|
8487
8538
|
const navKey = NAV[id] || id; // the destination to highlight
|
|
8488
8539
|
if (!el) el = document.querySelector(`.nav-item[data-tab="${navKey}"]`)
|
|
8489
8540
|
|| document.querySelector('.nav-item[data-tab="decisions"]');
|
|
8490
8541
|
currentTab = lid;
|
|
8542
|
+
document.getElementById('nav-views-label').hidden = lid !== 'agents';
|
|
8543
|
+
document.querySelector('.nav-views').hidden = lid !== 'agents';
|
|
8544
|
+
if (['agents', 'budgets', 'harness'].includes(lid)) document.getElementById('tools-nav').open = true;
|
|
8545
|
+
if (lid === 'work' || lid === 'history') window.GctoWork.refresh();
|
|
8491
8546
|
if (ROUTES[id]) {
|
|
8492
8547
|
if (location.hash !== `#/${id}`) history.replaceState(null, '', `#/${id}`);
|
|
8493
8548
|
// BRD-R9: one local line per destination open, never blocking, never surfaced.
|
|
@@ -8507,7 +8562,7 @@ function switchTab(id, el) {
|
|
|
8507
8562
|
// crumb — `labels` used to be referenced here but is only ever a function-local in
|
|
8508
8563
|
// this file, so this threw ReferenceError on every tab switch and skipped the per-tab
|
|
8509
8564
|
// loaders below (e.g. logs stuck on "Loading…"). Use a local label map.
|
|
8510
|
-
const TAB_LABELS = { decisions: 'Decisions', ledger: 'Ledger', fleet: 'Fleet', harness: 'Harness', settings: 'Settings', budgets: 'Budgets', docs: 'Docs', dashboard: 'Board', inbox: 'Inbox', tasks: 'Tasks', agents: 'Agents', logs: 'Activity', notifications: 'Notifications', metrics: 'Metrics', share: 'Share' };
|
|
8565
|
+
const TAB_LABELS = { work: 'Work', history: 'History', decisions: 'Decisions', ledger: 'Ledger', fleet: 'Fleet', harness: 'Harness', settings: 'Settings', budgets: 'Budgets', docs: 'Docs', dashboard: 'Board', inbox: 'Inbox', tasks: 'Tasks', agents: 'Agents', logs: 'Activity', notifications: 'Notifications', metrics: 'Metrics', share: 'Share' };
|
|
8511
8566
|
const crumb = document.getElementById('crumb-here'); if (crumb) crumb.textContent = TAB_LABELS[id] || TAB_LABELS[lid] || 'Board';
|
|
8512
8567
|
// Budgets render on Metrics, so the heartbeat that feeds them has to be
|
|
8513
8568
|
// fetched when Metrics opens — not only from the agents drill-in it used to
|
|
@@ -8544,7 +8599,7 @@ function switchTab(id, el) {
|
|
|
8544
8599
|
|
|
8545
8600
|
// Sidebar tabs: Enter/Space activate, arrows move (WAI-ARIA tablist).
|
|
8546
8601
|
function navTabKeydown(event, el) {
|
|
8547
|
-
const items = [...document.querySelectorAll('.nav-item[role="tab"]')];
|
|
8602
|
+
const items = [...document.querySelectorAll('.nav-item[role="tab"]')].filter(item => item.getClientRects().length);
|
|
8548
8603
|
const i = items.indexOf(el);
|
|
8549
8604
|
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); el.click(); return; }
|
|
8550
8605
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
@@ -8576,7 +8631,7 @@ function routeFromHash() {
|
|
|
8576
8631
|
return ROUTES[h] ? h : (LEGACY_ROUTES[h] || null);
|
|
8577
8632
|
}
|
|
8578
8633
|
addEventListener('hashchange', () => { const r = routeFromHash(); if (r) switchTab(r); });
|
|
8579
|
-
addEventListener('DOMContentLoaded', () => { const r = routeFromHash(); if (r && r !== '
|
|
8634
|
+
addEventListener('DOMContentLoaded', () => { const r = routeFromHash(); if (r && r !== 'work') switchTab(r); });
|
|
8580
8635
|
|
|
8581
8636
|
async function refreshLogs() {
|
|
8582
8637
|
const data = await api(`/api/logs${pqs()}`);
|