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
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
3
|
"description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 71 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.
|
|
4
|
+
"version": "3.48.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
|
}
|
|
@@ -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
|
+
})();
|
|
@@ -378,7 +378,7 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
378
378
|
|
|
379
379
|
/* The budgets panel. A cap with no consumption beside it cannot answer the only
|
|
380
380
|
question worth asking of it, so every row carries the state and the spend. */
|
|
381
|
-
.ab-title { font-weight:
|
|
381
|
+
.ab-title { font-weight: 500; margin-bottom: 8px; }
|
|
382
382
|
.ab-row {
|
|
383
383
|
display: flex; align-items: center; gap: 10px;
|
|
384
384
|
padding: 4px 0; border-top: 1px solid var(--border);
|
|
@@ -886,6 +886,10 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
886
886
|
color: var(--text3);
|
|
887
887
|
text-align: center;
|
|
888
888
|
}
|
|
889
|
+
/* A <small> hint inside an empty state rendered at the browser's "smaller"
|
|
890
|
+
(13px × 5/6 = 10.83px), off the type scale; the layout gate caught it whenever
|
|
891
|
+
a fresh board had no logs. Pin it to the caption size. */
|
|
892
|
+
.empty small { font-size: var(--fs-caption); }
|
|
889
893
|
|
|
890
894
|
/* A read failed. Deliberately louder than .empty — the whole point is that this
|
|
891
895
|
must not be mistaken for "there is nothing here". */
|
|
@@ -2565,7 +2569,7 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
2565
2569
|
/* ── Agents installed list (legacy — kept for fallback) ───────────────── */
|
|
2566
2570
|
.agents-installed { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; margin-top: 4px; }
|
|
2567
2571
|
/* Agent budget bars (Paperclip cascading budget pattern) */
|
|
2568
|
-
.ab-title { font-weight:
|
|
2572
|
+
.ab-title { font-weight: 500; color: var(--text2); margin-bottom: 6px; }
|
|
2569
2573
|
.ab-row { display: flex; justify-content: space-between; padding: 2px 0; }
|
|
2570
2574
|
.ab-name { font-family: var(--mono, monospace); color: var(--accent-text); }
|
|
2571
2575
|
.ab-cap { color: var(--text3); }
|
|
@@ -3261,9 +3265,27 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3261
3265
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
3262
3266
|
::-webkit-scrollbar-thumb { background: rgba(15, 17, 21, 0.12); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; }
|
|
3263
3267
|
::-webkit-scrollbar-thumb:hover { background: rgba(15, 17, 21, 0.22); border: 2px solid transparent; background-clip: content-box; }
|
|
3268
|
+
/* Task read-side: reuse the board's measured typography and colour tokens. */
|
|
3269
|
+
.nav-views[hidden], #nav-views-label[hidden], #work-allow-row[hidden] { display: none; }
|
|
3270
|
+
.work-page { padding: 24px; max-width: 1000px; }
|
|
3271
|
+
.work-card, #work-compose { padding: 16px; border: 1px solid var(--border); border-radius: 8px; margin: 12px 0; background: var(--bg-card); }
|
|
3272
|
+
.work-page h1 { font-size: var(--fs-title-l); font-weight: 500; }
|
|
3273
|
+
.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; }
|
|
3274
|
+
.work-page .gate-btn:hover { background: var(--bg-solid-hover); }
|
|
3275
|
+
.work-card a { color: var(--text); text-decoration: underline; }
|
|
3276
|
+
.work-card h2 { font-size: var(--fs-title-s); }
|
|
3277
|
+
.work-card p, #work-compose p { margin: 8px 0; font-size: var(--fs-body); }
|
|
3278
|
+
.work-meta { color: var(--text2); font-size: var(--fs-caption); }
|
|
3279
|
+
.work-warning { color: var(--absent-warn-fg); background: var(--bg-card); border: 1px solid var(--absent-warn-fg); padding: 12px; }
|
|
3280
|
+
.work-page pre { font-weight: 400; white-space: pre-wrap; overflow-wrap: anywhere; margin: 12px 0; }
|
|
3281
|
+
#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; }
|
|
3282
|
+
#tools-nav summary { padding: 12px 16px; cursor: pointer; }
|
|
3283
|
+
@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; } }
|
|
3284
|
+
@media (max-width: 767px) { .work-page { padding: 12px; } .work-page button, #work-compose select { min-height: 44px; } }
|
|
3264
3285
|
</style>
|
|
3265
3286
|
</head>
|
|
3266
3287
|
<body>
|
|
3288
|
+
<script src="/assets/work-view.js"></script>
|
|
3267
3289
|
<div class="board">
|
|
3268
3290
|
<!-- Subtle emerald glow background -->
|
|
3269
3291
|
<div class="board-bg" aria-hidden="true"></div>
|
|
@@ -3299,21 +3321,17 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3299
3321
|
<div id="proj-menu-list"></div>
|
|
3300
3322
|
</div>
|
|
3301
3323
|
</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
3324
|
<nav class="nav" id="nav-tablist" role="tablist" aria-orientation="vertical">
|
|
3312
|
-
<div class="nav-item active" data-tab="
|
|
3325
|
+
<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>
|
|
3326
|
+
<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
3327
|
<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
3328
|
<span>Decisions</span>
|
|
3315
3329
|
<span class="count" id="nav-inbox-count">0</span>
|
|
3316
3330
|
</div>
|
|
3331
|
+
<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>
|
|
3332
|
+
</nav>
|
|
3333
|
+
<details id="tools-nav"><summary>Tools</summary>
|
|
3334
|
+
<nav class="nav" role="tablist" aria-label="Technical tools" aria-orientation="vertical">
|
|
3317
3335
|
<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
3336
|
<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
3337
|
<span>Ledger</span>
|
|
@@ -3336,14 +3354,15 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3336
3354
|
<span class="count" id="nav-harness-count"></span>
|
|
3337
3355
|
</div>
|
|
3338
3356
|
</nav>
|
|
3357
|
+
</details>
|
|
3339
3358
|
<div class="nav-divider"></div>
|
|
3340
|
-
<div class="nav-views-label" id="nav-views-label">Views</div>
|
|
3359
|
+
<div class="nav-views-label" id="nav-views-label" hidden>Views</div>
|
|
3341
3360
|
<!-- Saved views over Fleet (DESIGN §5 point 2). Static for this shell —
|
|
3342
3361
|
filtering the fleet list is the Fleet screen body's job (.8), not this
|
|
3343
3362
|
task's; deliberately not `.nav-item`/`role="tab"`, since they neither
|
|
3344
3363
|
switch a top-level destination nor do anything yet, and a control that
|
|
3345
3364
|
looks live and isn't fails `ui-ux-pro-max` empty-nav-state. -->
|
|
3346
|
-
<div class="nav-views">
|
|
3365
|
+
<div class="nav-views" hidden>
|
|
3347
3366
|
<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
3367
|
<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
3368
|
<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 +3417,24 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3398
3417
|
</div>
|
|
3399
3418
|
</div>
|
|
3400
3419
|
|
|
3420
|
+
<div class="panel active" id="panel-work" role="tabpanel">
|
|
3421
|
+
<div class="work-page"><h1>Work</h1>
|
|
3422
|
+
<form id="work-compose">
|
|
3423
|
+
<label for="work-goal">What result do you need?</label>
|
|
3424
|
+
<textarea id="work-goal" rows="3" required placeholder="Add CSV export with permission checks"></textarea>
|
|
3425
|
+
<label for="work-host">Execution host</label>
|
|
3426
|
+
<select id="work-host"><option value="claude-code">Claude Code</option><option value="codex">Controlled Codex</option></select>
|
|
3427
|
+
<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>
|
|
3428
|
+
<p>Prepares a command. Run it in a terminal in the selected project. The board does not start a host session.</p>
|
|
3429
|
+
<button type="submit" class="gate-btn">Copy command</button><pre id="work-command"></pre>
|
|
3430
|
+
</form>
|
|
3431
|
+
<p id="work-feedback" role="status" aria-live="polite"></p>
|
|
3432
|
+
<div id="work-list">Loading project work…</div>
|
|
3433
|
+
</div>
|
|
3434
|
+
</div>
|
|
3435
|
+
<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
3436
|
<!-- Inbox -->
|
|
3402
|
-
<div class="panel
|
|
3437
|
+
<div class="panel" id="panel-inbox">
|
|
3403
3438
|
<div class="inbox-page">
|
|
3404
3439
|
<!-- Fidelity pass (v3.27.1): the design's header is the screen's name and
|
|
3405
3440
|
its count; the sentence the board used to shout in 52px display
|
|
@@ -3937,7 +3972,13 @@ const PRIORITY_LABEL = ['Urgent','High','Medium','Low'];
|
|
|
3937
3972
|
let allTasks = [];
|
|
3938
3973
|
let metrics = {};
|
|
3939
3974
|
let shareState = { enabled: false, url: null };
|
|
3940
|
-
let currentTab = '
|
|
3975
|
+
let currentTab = 'work';
|
|
3976
|
+
window.GctoWork.bind({
|
|
3977
|
+
project: () => new URLSearchParams(location.search).get('project') || '',
|
|
3978
|
+
visible: () => currentTab === 'work' || currentTab === 'history',
|
|
3979
|
+
read: url => api(url),
|
|
3980
|
+
openIssue: (id, decision) => { if (decision) switchTab('decisions'); openSideById(id); },
|
|
3981
|
+
});
|
|
3941
3982
|
let searchQuery = '';
|
|
3942
3983
|
let allProjects = [];
|
|
3943
3984
|
let currentProject = '';
|
|
@@ -7003,6 +7044,7 @@ async function switchProject(slug) {
|
|
|
7003
7044
|
// Close existing SSE so it doesn't push the wrong project's data
|
|
7004
7045
|
if (window._gctoSSE) { try { window._gctoSSE.close(); } catch {} window._gctoSSE = null; }
|
|
7005
7046
|
currentProject = slug;
|
|
7047
|
+
window.GctoWork.reset();
|
|
7006
7048
|
// Optimistic UI clear
|
|
7007
7049
|
allTasks = [];
|
|
7008
7050
|
renderKanban([]);
|
|
@@ -7069,6 +7111,7 @@ function filterTasks(q) {
|
|
|
7069
7111
|
}
|
|
7070
7112
|
|
|
7071
7113
|
function onSearch(q) {
|
|
7114
|
+
window.GctoWork.search(q);
|
|
7072
7115
|
searchQuery = q;
|
|
7073
7116
|
renderKanban(filterTasks(q));
|
|
7074
7117
|
document.getElementById('topbar-task-count').textContent =
|
|
@@ -7184,12 +7227,13 @@ async function init() {
|
|
|
7184
7227
|
// Build version badge (fire-and-forget — never blocks the board).
|
|
7185
7228
|
api('/api/version').then(v => { const el = document.getElementById('build-version'); if (el && v && v.version) el.textContent = 'v' + v.version; }).catch(() => {});
|
|
7186
7229
|
|
|
7187
|
-
const [tasks, met, share, projs, inbox] = await Promise.all([
|
|
7230
|
+
const [tasks, met, share, projs, inbox, serverWork] = await Promise.all([
|
|
7188
7231
|
api(`/api/tasks${qs}`),
|
|
7189
7232
|
api(`/api/metrics${qs}`),
|
|
7190
7233
|
api(`/api/share${qs}`),
|
|
7191
7234
|
api('/api/projects'),
|
|
7192
7235
|
api(`/api/inbox${qs}`),
|
|
7236
|
+
api(`/api/work${qs}`),
|
|
7193
7237
|
]);
|
|
7194
7238
|
allTasks = tasks || [];
|
|
7195
7239
|
metrics = met || {};
|
|
@@ -7206,11 +7250,13 @@ async function init() {
|
|
|
7206
7250
|
const fromTaskId = (allTasks[0]?.id || '').split('-')[0];
|
|
7207
7251
|
const registered = (allProjects || []).some((p) => p.slug === fromTaskId) ? fromTaskId : '';
|
|
7208
7252
|
const proj = urlProject
|
|
7253
|
+
|| serverWork?.projectName
|
|
7209
7254
|
|| registered
|
|
7210
7255
|
|| (allProjects[0]?.slug)
|
|
7211
7256
|
|| fromTaskId
|
|
7212
7257
|
|| 'project';
|
|
7213
7258
|
currentProject = proj;
|
|
7259
|
+
window.GctoWork.reset();
|
|
7214
7260
|
document.getElementById('project-name').textContent = proj;
|
|
7215
7261
|
document.getElementById('crumb-project').textContent = proj;
|
|
7216
7262
|
document.title = `${proj} · great_cto`;
|
|
@@ -7388,6 +7434,7 @@ function connectSSE() {
|
|
|
7388
7434
|
try { renderKanban(filterTasks(searchQuery)); } catch (e) { console.warn('SSE renderKanban failed', e); }
|
|
7389
7435
|
checkBoardFreshness();
|
|
7390
7436
|
refreshInbox();
|
|
7437
|
+
if (currentTab === 'work' || currentTab === 'history') window.GctoWork.refresh();
|
|
7391
7438
|
};
|
|
7392
7439
|
es.addEventListener('tasks', e => {
|
|
7393
7440
|
const newTasks = JSON.parse(e.data);
|
|
@@ -8285,6 +8332,8 @@ let cmdkIndex = 0;
|
|
|
8285
8332
|
|
|
8286
8333
|
function cmdkActions() {
|
|
8287
8334
|
const out = [
|
|
8335
|
+
{ group: 'Go', label: 'Work — goals and execution', run: () => switchTab('work') },
|
|
8336
|
+
{ group: 'Go', label: 'History — recorded outcomes', run: () => switchTab('history') },
|
|
8288
8337
|
{ group: 'Go', label: 'Decisions — what needs you', run: () => switchTab('decisions') },
|
|
8289
8338
|
{ group: 'Go', label: 'Ledger — what it cost, what is running', run: () => switchTab('ledger') },
|
|
8290
8339
|
{ group: 'Go', label: 'Fleet — the agents and what they may do', run: () => switchTab('fleet') },
|
|
@@ -8481,13 +8530,17 @@ try {
|
|
|
8481
8530
|
// panels, not keys.
|
|
8482
8531
|
const DEST = { decisions: 'inbox', ledger: 'budgets', fleet: 'agents', dashboard: 'budgets', share: 'settings', notifications: 'settings' };
|
|
8483
8532
|
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 };
|
|
8533
|
+
const ROUTES = { work: 1, history: 1, decisions: 1, ledger: 1, fleet: 1, harness: 1, settings: 1, kanban: 1 };
|
|
8485
8534
|
function switchTab(id, el) {
|
|
8486
8535
|
const lid = DEST[id] || id; // the panel's own name
|
|
8487
8536
|
const navKey = NAV[id] || id; // the destination to highlight
|
|
8488
8537
|
if (!el) el = document.querySelector(`.nav-item[data-tab="${navKey}"]`)
|
|
8489
8538
|
|| document.querySelector('.nav-item[data-tab="decisions"]');
|
|
8490
8539
|
currentTab = lid;
|
|
8540
|
+
document.getElementById('nav-views-label').hidden = lid !== 'agents';
|
|
8541
|
+
document.querySelector('.nav-views').hidden = lid !== 'agents';
|
|
8542
|
+
if (['agents', 'budgets', 'harness'].includes(lid)) document.getElementById('tools-nav').open = true;
|
|
8543
|
+
if (lid === 'work' || lid === 'history') window.GctoWork.refresh();
|
|
8491
8544
|
if (ROUTES[id]) {
|
|
8492
8545
|
if (location.hash !== `#/${id}`) history.replaceState(null, '', `#/${id}`);
|
|
8493
8546
|
// BRD-R9: one local line per destination open, never blocking, never surfaced.
|
|
@@ -8507,7 +8560,7 @@ function switchTab(id, el) {
|
|
|
8507
8560
|
// crumb — `labels` used to be referenced here but is only ever a function-local in
|
|
8508
8561
|
// this file, so this threw ReferenceError on every tab switch and skipped the per-tab
|
|
8509
8562
|
// 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' };
|
|
8563
|
+
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
8564
|
const crumb = document.getElementById('crumb-here'); if (crumb) crumb.textContent = TAB_LABELS[id] || TAB_LABELS[lid] || 'Board';
|
|
8512
8565
|
// Budgets render on Metrics, so the heartbeat that feeds them has to be
|
|
8513
8566
|
// fetched when Metrics opens — not only from the agents drill-in it used to
|
|
@@ -8544,7 +8597,7 @@ function switchTab(id, el) {
|
|
|
8544
8597
|
|
|
8545
8598
|
// Sidebar tabs: Enter/Space activate, arrows move (WAI-ARIA tablist).
|
|
8546
8599
|
function navTabKeydown(event, el) {
|
|
8547
|
-
const items = [...document.querySelectorAll('.nav-item[role="tab"]')];
|
|
8600
|
+
const items = [...document.querySelectorAll('.nav-item[role="tab"]')].filter(item => item.getClientRects().length);
|
|
8548
8601
|
const i = items.indexOf(el);
|
|
8549
8602
|
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); el.click(); return; }
|
|
8550
8603
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
@@ -8576,7 +8629,7 @@ function routeFromHash() {
|
|
|
8576
8629
|
return ROUTES[h] ? h : (LEGACY_ROUTES[h] || null);
|
|
8577
8630
|
}
|
|
8578
8631
|
addEventListener('hashchange', () => { const r = routeFromHash(); if (r) switchTab(r); });
|
|
8579
|
-
addEventListener('DOMContentLoaded', () => { const r = routeFromHash(); if (r && r !== '
|
|
8632
|
+
addEventListener('DOMContentLoaded', () => { const r = routeFromHash(); if (r && r !== 'work') switchTab(r); });
|
|
8580
8633
|
|
|
8581
8634
|
async function refreshLogs() {
|
|
8582
8635
|
const data = await api(`/api/logs${pqs()}`);
|