great-cto 3.70.1 → 3.71.1
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 +9 -1
- package/board/.claude-plugin/plugin.json +93 -1
- package/board/packages/board/lib/routes.mjs +14 -1
- package/board/packages/board/lib/work.mjs +34 -3
- package/board/packages/board/public/assets/work-view.js +146 -18
- package/board/scripts/codex-pipeline.mjs +2 -0
- package/board/scripts/lib/claude-exec.mjs +49 -6
- package/board/scripts/lib/codex-exec.mjs +42 -18
- package/board/scripts/lib/codex-host-state.mjs +18 -2
- package/board/scripts/lib/codex-pipeline.mjs +44 -7
- package/board/scripts/lib/host-graph-bindings.mjs +135 -0
- package/board/scripts/lib/host-graph-events.mjs +157 -0
- package/board/scripts/lib/host-graph-journal.mjs +210 -0
- package/board/scripts/lib/host-graph-observer.mjs +61 -0
- package/board/scripts/lib/host-graph-runtime.mjs +38 -0
- package/board/scripts/work-task.mjs +17 -4
- package/dist/adapt.js +10 -21
- package/dist/codex.js +4 -15
- package/dist/companion.js +6 -122
- package/dist/installer.js +4 -72
- package/dist/main.js +44 -117
- package/dist/release-activation.mjs +561 -0
- package/dist/release-archive.mjs +165 -0
- package/dist/release-trust.mjs +514 -0
- package/dist/release-update.mjs +303 -0
- package/dist/self-upgrade.js +10 -39
- package/dist/update-check.js +1 -0
- package/dist/upgrade.js +5 -120
- package/package.json +1 -1
- package/board/packages/board/fixtures/beads-read-owner.mjs +0 -3
- package/board/packages/board/fixtures/fake-bd-controlled.mjs +0 -55
- package/board/packages/board/fixtures/fake-bd-fail.sh +0 -9
- package/board/packages/board/fixtures/fake-bd-slow.sh +0 -12
- package/board/packages/board/fixtures/held-receipt-git.mjs +0 -21
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# great-cto
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> CLI for the [great_cto](https://github.com/avelikiy/great_cto) engineering workspace, controlled runs and verified release updates.
|
|
4
|
+
|
|
5
|
+
**Verified distribution boundary:** ADR-045 replaces mutable installation and update
|
|
6
|
+
paths with signed artifact admission. This implementation refuses `init`, native
|
|
7
|
+
plugin refresh and automatic companion installation before mutation. The plain
|
|
8
|
+
npm self-update adapter requires an independently configured verifier and exact
|
|
9
|
+
signed release assets. See [verified updates](../../docs/reference/verified-updates.md).
|
|
10
|
+
The installation walkthrough below describes the legacy 3.70.1 flow; it is not
|
|
11
|
+
an activation route for this implementation.
|
|
4
12
|
|
|
5
13
|
```bash
|
|
6
14
|
npx great-cto init
|
|
@@ -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 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.
|
|
4
|
+
"version": "3.71.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexander Velikiy",
|
|
7
7
|
"url": "https://hashnode.com/@Greatcto"
|
|
@@ -88,6 +88,16 @@
|
|
|
88
88
|
"timeout": 10
|
|
89
89
|
}
|
|
90
90
|
]
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"matcher": "",
|
|
94
|
+
"hooks": [
|
|
95
|
+
{
|
|
96
|
+
"type": "command",
|
|
97
|
+
"timeout": 5,
|
|
98
|
+
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; [ -d \"${GREAT_CTO_GRAPH_BINDINGS_DIR:-$HOME/.great_cto/graph-bindings}\" ] || exit 0; _pd=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -n \"$_pd\" ] || exit 0; GREAT_CTO_HOST=claude-code node \"$_pd/scripts/hooks/host-graph-observer.mjs\""
|
|
99
|
+
}
|
|
100
|
+
]
|
|
91
101
|
}
|
|
92
102
|
],
|
|
93
103
|
"PreCompact": [
|
|
@@ -120,6 +130,16 @@
|
|
|
120
130
|
"statusMessage": "Loading orchestrator contract..."
|
|
121
131
|
}
|
|
122
132
|
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"matcher": "",
|
|
136
|
+
"hooks": [
|
|
137
|
+
{
|
|
138
|
+
"type": "command",
|
|
139
|
+
"timeout": 5,
|
|
140
|
+
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; [ -d \"${GREAT_CTO_GRAPH_BINDINGS_DIR:-$HOME/.great_cto/graph-bindings}\" ] || exit 0; _pd=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -n \"$_pd\" ] || exit 0; GREAT_CTO_HOST=claude-code node \"$_pd/scripts/hooks/host-graph-observer.mjs\""
|
|
141
|
+
}
|
|
142
|
+
]
|
|
123
143
|
}
|
|
124
144
|
],
|
|
125
145
|
"SubagentStop": [
|
|
@@ -138,6 +158,16 @@
|
|
|
138
158
|
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/turn-snapshot.mjs\" 2>/dev/null || true"
|
|
139
159
|
}
|
|
140
160
|
]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"matcher": "",
|
|
164
|
+
"hooks": [
|
|
165
|
+
{
|
|
166
|
+
"type": "command",
|
|
167
|
+
"timeout": 5,
|
|
168
|
+
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; [ -d \"${GREAT_CTO_GRAPH_BINDINGS_DIR:-$HOME/.great_cto/graph-bindings}\" ] || exit 0; _pd=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -n \"$_pd\" ] || exit 0; GREAT_CTO_HOST=claude-code node \"$_pd/scripts/hooks/host-graph-observer.mjs\""
|
|
169
|
+
}
|
|
170
|
+
]
|
|
141
171
|
}
|
|
142
172
|
],
|
|
143
173
|
"PreToolUse": [
|
|
@@ -208,6 +238,16 @@
|
|
|
208
238
|
"statusMessage": "Checking recorded lessons..."
|
|
209
239
|
}
|
|
210
240
|
]
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"matcher": "",
|
|
244
|
+
"hooks": [
|
|
245
|
+
{
|
|
246
|
+
"type": "command",
|
|
247
|
+
"timeout": 5,
|
|
248
|
+
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; [ -d \"${GREAT_CTO_GRAPH_BINDINGS_DIR:-$HOME/.great_cto/graph-bindings}\" ] || exit 0; _pd=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -n \"$_pd\" ] || exit 0; GREAT_CTO_HOST=claude-code node \"$_pd/scripts/hooks/host-graph-observer.mjs\""
|
|
249
|
+
}
|
|
250
|
+
]
|
|
211
251
|
}
|
|
212
252
|
],
|
|
213
253
|
"PostToolUse": [
|
|
@@ -293,6 +333,16 @@
|
|
|
293
333
|
"statusMessage": "Computing next pipeline stage..."
|
|
294
334
|
}
|
|
295
335
|
]
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"matcher": "",
|
|
339
|
+
"hooks": [
|
|
340
|
+
{
|
|
341
|
+
"type": "command",
|
|
342
|
+
"timeout": 5,
|
|
343
|
+
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; [ -d \"${GREAT_CTO_GRAPH_BINDINGS_DIR:-$HOME/.great_cto/graph-bindings}\" ] || exit 0; _pd=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -n \"$_pd\" ] || exit 0; GREAT_CTO_HOST=claude-code node \"$_pd/scripts/hooks/host-graph-observer.mjs\""
|
|
344
|
+
}
|
|
345
|
+
]
|
|
296
346
|
}
|
|
297
347
|
],
|
|
298
348
|
"SessionEnd": [
|
|
@@ -311,6 +361,16 @@
|
|
|
311
361
|
"timeout": 5
|
|
312
362
|
}
|
|
313
363
|
]
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"matcher": "",
|
|
367
|
+
"hooks": [
|
|
368
|
+
{
|
|
369
|
+
"type": "command",
|
|
370
|
+
"timeout": 5,
|
|
371
|
+
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; [ -d \"${GREAT_CTO_GRAPH_BINDINGS_DIR:-$HOME/.great_cto/graph-bindings}\" ] || exit 0; _pd=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -n \"$_pd\" ] || exit 0; GREAT_CTO_HOST=claude-code node \"$_pd/scripts/hooks/host-graph-observer.mjs\""
|
|
372
|
+
}
|
|
373
|
+
]
|
|
314
374
|
}
|
|
315
375
|
],
|
|
316
376
|
"UserPromptSubmit": [
|
|
@@ -338,6 +398,16 @@
|
|
|
338
398
|
"timeout": 5
|
|
339
399
|
}
|
|
340
400
|
]
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
"matcher": "",
|
|
404
|
+
"hooks": [
|
|
405
|
+
{
|
|
406
|
+
"type": "command",
|
|
407
|
+
"timeout": 5,
|
|
408
|
+
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; [ -d \"${GREAT_CTO_GRAPH_BINDINGS_DIR:-$HOME/.great_cto/graph-bindings}\" ] || exit 0; _pd=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -n \"$_pd\" ] || exit 0; GREAT_CTO_HOST=claude-code node \"$_pd/scripts/hooks/host-graph-observer.mjs\""
|
|
409
|
+
}
|
|
410
|
+
]
|
|
341
411
|
}
|
|
342
412
|
],
|
|
343
413
|
"PermissionDenied": [
|
|
@@ -384,6 +454,16 @@
|
|
|
384
454
|
"timeout": 5
|
|
385
455
|
}
|
|
386
456
|
]
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
"matcher": "",
|
|
460
|
+
"hooks": [
|
|
461
|
+
{
|
|
462
|
+
"type": "command",
|
|
463
|
+
"timeout": 5,
|
|
464
|
+
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; [ -d \"${GREAT_CTO_GRAPH_BINDINGS_DIR:-$HOME/.great_cto/graph-bindings}\" ] || exit 0; _pd=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -n \"$_pd\" ] || exit 0; GREAT_CTO_HOST=claude-code node \"$_pd/scripts/hooks/host-graph-observer.mjs\""
|
|
465
|
+
}
|
|
466
|
+
]
|
|
387
467
|
}
|
|
388
468
|
],
|
|
389
469
|
"Notification": [
|
|
@@ -397,6 +477,18 @@
|
|
|
397
477
|
}
|
|
398
478
|
]
|
|
399
479
|
}
|
|
480
|
+
],
|
|
481
|
+
"PostToolUseFailure": [
|
|
482
|
+
{
|
|
483
|
+
"matcher": "",
|
|
484
|
+
"hooks": [
|
|
485
|
+
{
|
|
486
|
+
"type": "command",
|
|
487
|
+
"timeout": 5,
|
|
488
|
+
"command": "_gd=\"$PWD\"; while [ -n \"$_gd\" ] && { [ \"$_gd\" = \"$HOME\" ] || [ ! -f \"$_gd/.great_cto/PROJECT.md\" ]; }; do _gd=\"${_gd%/*}\"; done; [ -n \"$_gd\" ] && cd \"$_gd\"; [ -d \"${GREAT_CTO_GRAPH_BINDINGS_DIR:-$HOME/.great_cto/graph-bindings}\" ] || exit 0; _pd=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -n \"$_pd\" ] || exit 0; GREAT_CTO_HOST=claude-code node \"$_pd/scripts/hooks/host-graph-observer.mjs\""
|
|
489
|
+
}
|
|
490
|
+
]
|
|
491
|
+
}
|
|
400
492
|
]
|
|
401
493
|
},
|
|
402
494
|
"homepage": "https://greatcto.systems",
|
|
@@ -45,7 +45,7 @@ import { readVerdicts } from './verdicts.mjs';
|
|
|
45
45
|
import { parseAgentBudgets, upsertAgentBudget, removeAgentBudget } from '../../../scripts/lib/agent-budget.mjs';
|
|
46
46
|
import { resolveSecondOpinion, SECOND_OPINION_PROVIDERS } from '../../../scripts/lib/second-opinion.mjs';
|
|
47
47
|
import { detectCodex } from '../../../scripts/lib/codex-exec.mjs';
|
|
48
|
-
import { getWork } from './work.mjs';
|
|
48
|
+
import { getWork, getHostGraphPage } from './work.mjs';
|
|
49
49
|
let publicationPreviewBusy = false;
|
|
50
50
|
let publicationWriteBusy = false;
|
|
51
51
|
import { publicationTickets, localPublicationOrigin } from './publication-tickets.mjs';
|
|
@@ -237,6 +237,19 @@ async function dispatch(req, res, url, cwd) {
|
|
|
237
237
|
return true;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
if (pathname === '/api/host-graph') {
|
|
241
|
+
if (requestedProject && resolveProjectInfo(requestedProject).resolved === 'fallback') {
|
|
242
|
+
res.writeHead(404, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify({ error: 'Unknown project' })); return true;
|
|
243
|
+
}
|
|
244
|
+
if (req.method !== 'GET') { res.writeHead(405, { Allow: 'GET' }); res.end(); return true; }
|
|
245
|
+
try {
|
|
246
|
+
const graph = getHostGraphPage(cwd, { host: url.searchParams.get('host'), sessionId: url.searchParams.get('session'),
|
|
247
|
+
runId: url.searchParams.get('run'), cursor: url.searchParams.get('cursor') });
|
|
248
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(graph));
|
|
249
|
+
} catch { res.writeHead(404, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify({ error: 'Bound graph page is unavailable' })); }
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
240
253
|
// New read model refuses project fallback rather than relabeling another project.
|
|
241
254
|
if (pathname === '/api/work') {
|
|
242
255
|
if (requestedProject && resolveProjectInfo(requestedProject).resolved === 'fallback') {
|
|
@@ -10,6 +10,8 @@ import { measureWork, compareWork } from '../../../scripts/lib/work-metrics.mjs'
|
|
|
10
10
|
import { readSessionStatus } from '../../../scripts/lib/session-status.mjs';
|
|
11
11
|
import { agentActivity } from './agent-activity.mjs';
|
|
12
12
|
import { taskInspector } from './task-inspector.mjs';
|
|
13
|
+
import { graphBinding, projectHostObservations } from '../../../scripts/lib/host-graph-observer.mjs';
|
|
14
|
+
import { nativeGraphObservations, readNativeGraphPage } from '../../../scripts/lib/host-graph-bindings.mjs';
|
|
13
15
|
|
|
14
16
|
const text = v => typeof v === 'string' ? v : null;
|
|
15
17
|
const date = v => typeof v === 'string' && Number.isFinite(Date.parse(v)) ? v : null;
|
|
@@ -30,7 +32,7 @@ function executionAttempts(run) {
|
|
|
30
32
|
}));
|
|
31
33
|
const truncated = attempts.length > 200;
|
|
32
34
|
const graph = run?.executionGraph || { state: 'unavailable', complete: false, nodes: [], edges: [], snapshots: [], reason: 'No linked controller-recorded graph is available.' };
|
|
33
|
-
return { state: nodes.length ? 'recorded' : 'unknown', nodes, topology: graph.state === 'recorded' ? 'recorded' : 'unknown', graph, truncated,
|
|
35
|
+
return { state: nodes.length ? 'recorded' : 'unknown', nodes, observations: run?.hostObservations || [], observationHealth: run?.hostObservationHealth || null, omittedObservationAttempts: run?.hostObservationOmittedAttempts || 0, topology: graph.state === 'recorded' ? 'recorded' : 'unknown', graph, truncated,
|
|
34
36
|
partial: !run || truncated || nodes.length !== window.length,
|
|
35
37
|
reason: !run ? 'Stable attempt observations are unavailable without a single explicitly linked controlled run.'
|
|
36
38
|
: !nodes.length ? 'No usable attempt observations are recorded.' : null };
|
|
@@ -63,7 +65,7 @@ function evidenceSections(entry) {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export function projectWork({ projectId, issues = [], codex = { state: 'absent', runs: [] },
|
|
66
|
-
tasks = [], sessions = [], sources = [], activity = { state: 'none', events: [] }, observedAt = new Date().toISOString() }) {
|
|
68
|
+
tasks = [], sessions = [], nativeGraphs = [], sources = [], activity = { state: 'none', events: [] }, observedAt = new Date().toISOString() }) {
|
|
67
69
|
const entries = [];
|
|
68
70
|
for (const r of codex.runs || []) {
|
|
69
71
|
// Completion of a controller run is not proof of the user's acceptance criteria.
|
|
@@ -139,6 +141,20 @@ export function projectWork({ projectId, issues = [], codex = { state: 'absent',
|
|
|
139
141
|
command: approveCommand || (enabled ? `great-cto resume --task ${t.taskId} --host ${t.host} --revision ${t.revision}` : null),
|
|
140
142
|
});
|
|
141
143
|
}
|
|
144
|
+
for (const observed of nativeGraphs) {
|
|
145
|
+
const linked = entries.filter(e => e.taskId && tasks.some(t => t.taskId === e.taskId && t.links.sessions.includes(observed.sessionId) && t.host === observed.host));
|
|
146
|
+
if (linked.length === 1) {
|
|
147
|
+
const entry = linked[0]; entry.execution ||= executionAttempts(null);
|
|
148
|
+
if (entry.execution.graph?.source !== 'host-observation' && entry.execution.topology !== 'recorded') {
|
|
149
|
+
entry.execution.graph = observed.graph; entry.execution.topology = 'observed';
|
|
150
|
+
} else (entry.execution.observations ||= []).push(observed.graph);
|
|
151
|
+
} else if (!linked.length) entries.push({ key: `native:${observed.host}:${observed.sessionId}`, kind: 'native', taskId: null,
|
|
152
|
+
runId: null, issueIds: [], title: `Observed ${observed.host} session`, goal: null, acceptance: [],
|
|
153
|
+
host: observed.host, phase: 'unknown', nativeState: 'observed', terminal: false, updatedAt: null,
|
|
154
|
+
outcome: null, reason: 'Enrolled native observations; task acceptance and execution authority are not established.',
|
|
155
|
+
decisions: [], capabilities: [], command: null, evidence: [], release: null,
|
|
156
|
+
execution: { state: observed.graph.state, nodes: [], topology: 'observed', graph: observed.graph, partial: true, observations: [] } });
|
|
157
|
+
}
|
|
142
158
|
for (const entry of entries) entry.evidenceSections = evidenceSections(entry);
|
|
143
159
|
entries.sort((a, b) => (a.terminal - b.terminal) || (b.decisions.length - a.decisions.length)
|
|
144
160
|
|| (Date.parse(b.updatedAt || '') || 0) - (Date.parse(a.updatedAt || '') || 0) || a.key.localeCompare(b.key));
|
|
@@ -193,8 +209,23 @@ export function getWork(cwd) {
|
|
|
193
209
|
sessions = readSessionStatus(cwd);
|
|
194
210
|
source('claude_sessions', unreadable ? 'degraded' : 'current', unreadable ? `${unreadable} session state file(s) could not be read` : null);
|
|
195
211
|
} catch { source('claude_sessions', 'unavailable', 'Cannot read Claude session status'); }
|
|
212
|
+
let nativeGraphs = [];
|
|
213
|
+
try { nativeGraphs = nativeGraphObservations(cwd); source('native_graphs', nativeGraphs.some(r => r.graph.health === 'degraded') ? 'degraded' : 'current', null); }
|
|
214
|
+
catch { source('native_graphs', 'unavailable', 'Native graph enrollment is unreadable'); }
|
|
196
215
|
let canonical = cwd;
|
|
197
216
|
try { canonical = realpathSync(cwd); } catch { /* source health already reports inaccessible data */ }
|
|
198
217
|
const projectId = 'project:' + createHash('sha256').update(canonical).digest('hex');
|
|
199
|
-
return { ...projectWork({ projectId, issues, codex, tasks, sessions, sources, activity, observedAt }), projectName: basename(cwd) };
|
|
218
|
+
return { ...projectWork({ projectId, issues, codex, tasks, sessions, nativeGraphs, sources, activity, observedAt }), projectName: basename(cwd) };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Read-only pagination, with exact enrollment or controlled attempt membership. */
|
|
222
|
+
export function getHostGraphPage(root, { host, sessionId, runId = null, cursor = null }) {
|
|
223
|
+
if (!['codex', 'claude-code'].includes(host)) throw Error('invalid graph host');
|
|
224
|
+
if (!runId || runId === sessionId) return readNativeGraphPage({ root, host, sessionId, cursor });
|
|
225
|
+
const listing = listCodexRuns({ root });
|
|
226
|
+
if (listing.state === 'degraded') throw Error('controlled membership is unreadable');
|
|
227
|
+
const run = listing.runs.find(r => r.id === runId);
|
|
228
|
+
const binding = run?.hostObservationBindings.find(b => b.sessionId === sessionId && b.host === host);
|
|
229
|
+
if (!binding) throw Error('controlled observation does not belong to this project');
|
|
230
|
+
return projectHostObservations(graphBinding({ root, runId, sessionId, attemptId: binding.attemptId, host }), { cursor });
|
|
200
231
|
}
|
|
@@ -69,11 +69,34 @@
|
|
|
69
69
|
function cockpitEntries(data, category, search) {
|
|
70
70
|
return data.entries.filter(e => inCategory(e, category) && (!search || `${e.title} ${e.key}`.toLowerCase().includes(search.toLowerCase())));
|
|
71
71
|
}
|
|
72
|
+
function renderDeclaredWorkflow(graph) {
|
|
73
|
+
if (!graph || graph.source === 'host-observation') return '';
|
|
74
|
+
const snapshots = Array.isArray(graph.snapshots) ? graph.snapshots.slice(0, 64) : [];
|
|
75
|
+
return '<details class="work-declared-workflow"><summary>Declared workflow' + (snapshots.length ? ` · ${snapshots.length} snapshots` : '') + '</summary>'
|
|
76
|
+
+ '<p>Declared possibilities only. These rules do not prove that a stage ran, a dependency was satisfied or a gate was approved.</p>'
|
|
77
|
+
+ (snapshots.length ? snapshots.map(snapshot => `<details><summary>Workflow snapshot ${esc(snapshot.digest || 'identity unavailable')}</summary>`
|
|
78
|
+
+ '<table><caption>Declared transition rules</caption><thead><tr><th>Role</th><th>On</th><th>Next</th><th>Join</th><th>Gates</th></tr></thead><tbody>'
|
|
79
|
+
+ (Array.isArray(snapshot.rules) ? snapshot.rules.slice(0, 256) : []).map(rule => '<tr>' + [rule.role, ...['on', 'next', 'join', 'gates'].map(key => Array.isArray(rule[key]) ? rule[key].slice(0, 16).join(', ') : '')].map(value => `<td>${esc(value || 'None declared')}</td>`).join('') + '</tr>').join('') + '</tbody></table></details>').join('')
|
|
80
|
+
: '<p>No declared workflow snapshot is recorded.</p>') + '</details>';
|
|
81
|
+
}
|
|
82
|
+
function renderHostAttempt(graph, attempts, options) {
|
|
83
|
+
const attemptId = graph.binding?.attemptId || graph.nodes?.find(node => node.attemptId)?.attemptId;
|
|
84
|
+
const attempt = attempts.find(node => node.attemptId === attemptId);
|
|
85
|
+
const hostKey = graph.binding?.host || graph.nodes?.[0]?.host;
|
|
86
|
+
const host = hostKey === 'codex' ? 'Codex' : hostKey === 'claude-code' ? 'Claude Code' : 'Host unknown';
|
|
87
|
+
const label = attempt ? `${attempt.role || 'Role unknown'} · attempt ${attempt.number || attemptId}` : attemptId ? `attempt ${attemptId}` : 'session observations';
|
|
88
|
+
const key = JSON.stringify([graph.binding?.host || null, graph.binding?.runId || null, graph.binding?.sessionId || null, attemptId || null]);
|
|
89
|
+
return `<details class="work-host-attempt" data-host-attempt="${esc(key)}" data-observation-key="${esc(key)}"><summary>Host observations · ${esc(host)} · ${esc(label)}</summary>${renderExecutionGraph(graph, options)}</details>`;
|
|
90
|
+
}
|
|
72
91
|
function renderExecution(entry, options = {}) {
|
|
73
92
|
const roles = Array.isArray(entry.evidence) ? entry.evidence : [];
|
|
74
93
|
const execution = entry.execution, nodes = Array.isArray(execution?.nodes) ? execution.nodes.slice(0, 200) : [];
|
|
75
94
|
return (options.canvas ? '<section class="work-execution work-execution-flat">' : '<section class="work-execution"><h3>Recorded observations</h3>')
|
|
95
|
+
+ (execution?.observationHealth?.state === 'degraded' ? '<p class="work-warning">Host observation delivery is incomplete.</p>' : '')
|
|
96
|
+
+ (execution?.omittedObservationAttempts ? `<p class="work-warning">${esc(execution.omittedObservationAttempts)} earlier attempt observation journals are outside this bounded view.</p>` : '')
|
|
97
|
+
+ renderDeclaredWorkflow(execution?.graph)
|
|
76
98
|
+ renderExecutionGraph(execution?.graph, options)
|
|
99
|
+
+ (Array.isArray(execution?.observations) ? execution.observations.slice(0, 16).map(graph => renderHostAttempt(graph, nodes, options)).join('') : '')
|
|
77
100
|
+ (execution?.graph?.state === 'recorded' ? '' : '<p>Dependencies not recorded. This is an observation list, not an inferred execution graph. Event order does not establish dependencies.</p>')
|
|
78
101
|
+ `<p>Recorded stage: ${esc(entry.stage || 'not recorded')}</p>`
|
|
79
102
|
+ (execution?.reason ? `<p>${esc(execution.reason)}</p>` : '')
|
|
@@ -90,25 +113,81 @@
|
|
|
90
113
|
+ (options.canvas ? '</details>' : '')
|
|
91
114
|
+ '</section>';
|
|
92
115
|
}
|
|
116
|
+
function renderHostCoverage(graph) {
|
|
117
|
+
const dimensions = [['lifecycle', 'Lifecycle'], ['parentChild', 'Parent / child'], ['tools', 'Tools'], ['verification', 'Verification'], ['gates', 'Gates'], ['artifacts', 'Artifacts']];
|
|
118
|
+
const total = Number.isSafeInteger(graph.total) && graph.total >= 0 ? graph.total : 'unavailable';
|
|
119
|
+
return `<div class="execution-host-coverage"><p class="work-meta">Source: host observation · Health: ${esc(graph.health || 'unknown')}. Observation metadata does not establish current liveness, controller transitions, gate approval or task acceptance.</p>`
|
|
120
|
+
+ (graph.observedInterval ? `<p>Observed interval · collector receipt times: ${esc(graph.observedInterval.from || 'Unavailable')} to ${esc(graph.observedInterval.to || 'Unavailable')}</p>` : '')
|
|
121
|
+
+ (Array.isArray(graph.diagnostics) && graph.diagnostics.length ? '<details class="work-observation-diagnostics"><summary>Observation diagnostics</summary><ul>' + graph.diagnostics.slice(0, 16).map(code => `<li><code>${esc(code)}</code></li>`).join('') + '</ul></details>' : '')
|
|
122
|
+
+ `<p>Showing ${Array.isArray(graph.nodes) ? graph.nodes.length : 0} of ${esc(total)} recorded observations.${graph.cursor ? ' More observations are available outside this page.' : ''}</p>`
|
|
123
|
+
+ (graph.cursor ? `<details><summary>Observation page cursor · read-only</summary><code>${esc(graph.cursor)}</code></details>` : '')
|
|
124
|
+
+ (graph.cursor && graph.binding?.host && graph.binding?.sessionId ? `<button type="button" data-host-graph-next="${esc(JSON.stringify({ ...graph.binding, cursor: graph.cursor }))}">Next observation page</button>` : '')
|
|
125
|
+
+ '<dl aria-label="Host observation coverage">' + dimensions.map(([key, label]) => {
|
|
126
|
+
const dimension = graph.coverage?.[key];
|
|
127
|
+
const support = dimension?.supported === true ? 'Supported' : dimension?.supported === false ? 'Unsupported' : 'Support unknown';
|
|
128
|
+
const state = dimension?.supported === false ? 'Unavailable' : dimension?.state === 'partial' ? 'Partial'
|
|
129
|
+
: dimension?.supported === true && dimension?.state === 'complete' ? 'Complete' : 'Unavailable';
|
|
130
|
+
return `<dt>${label} · ${support} · ${state}</dt><dd>${esc(dimension?.reason || 'Coverage has not been established.')}</dd>`;
|
|
131
|
+
}).join('') + '</dl></div>';
|
|
132
|
+
}
|
|
133
|
+
const controllerOwnedKinds = new Set(['transition', 'context', 'checks', 'gate', 'approval', 'rework', 'recovery', 'wave', 'manual', 'cancel', 'release', 'reuse']);
|
|
134
|
+
const graphLane = node => ['codex', 'claude-code'].includes(node.host) ? node.host
|
|
135
|
+
: !node.host && controllerOwnedKinds.has(node.kind) ? 'controller' : 'unknown';
|
|
136
|
+
const graphHostLabel = node => node.host || (graphLane(node) === 'controller' ? 'controller' : 'Host not recorded');
|
|
137
|
+
const graphRoleLabel = node => node.role || (graphLane(node) === 'controller' ? 'controller' : node.kind || 'Role not recorded');
|
|
93
138
|
function renderExecutionGraph(graph, options = {}) {
|
|
139
|
+
const hostObserved = graph?.source === 'host-observation';
|
|
140
|
+
const coverage = hostObserved ? renderHostCoverage(graph) : '';
|
|
94
141
|
if (graph?.state !== 'recorded' || !Array.isArray(graph.nodes) || !graph.nodes.length)
|
|
95
|
-
return
|
|
96
|
-
|
|
142
|
+
return (hostObserved ? '<section class="execution-graph" aria-label="Host-observed execution graph"><h3>Host-observed graph</h3>' + coverage : '')
|
|
143
|
+
+ `<p class="work-meta">${esc(graph?.reason || 'Controller-recorded graph unavailable. Native and legacy observations remain partial.')}</p>` + (hostObserved ? '</section>' : '');
|
|
144
|
+
if (graph.nodes.length > 512 || !Array.isArray(graph.edges) || graph.edges.length > 2048) return coverage + '<p>Graph exceeds the display limit.</p>';
|
|
97
145
|
const horizontal = options.canvas && options.direction !== 'tb';
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
146
|
+
let nodes = graph.nodes;
|
|
147
|
+
const ids = new Map(nodes.map((n, i) => [n.id, i]));
|
|
148
|
+
if (ids.size !== nodes.length || graph.edges.some(e => !ids.has(e.from) || !ids.has(e.to) || e.from === e.to || (!hostObserved && ids.get(e.from) >= ids.get(e.to))))
|
|
149
|
+
return coverage + '<p>Graph dependencies cannot be displayed safely.</p>';
|
|
150
|
+
if (hostObserved) {
|
|
151
|
+
// Explicit parents may arrive late. Reorder for layout only; never add an edge.
|
|
152
|
+
const indegree = new Map(nodes.map(n => [n.id, 0])), children = new Map(nodes.map(n => [n.id, []]));
|
|
153
|
+
for (const edge of graph.edges) { indegree.set(edge.to, indegree.get(edge.to) + 1); children.get(edge.from).push(edge.to); }
|
|
154
|
+
const pending = nodes.filter(n => indegree.get(n.id) === 0), ordered = [], byId = new Map(nodes.map(n => [n.id, n]));
|
|
155
|
+
for (let index = 0; index < pending.length; index += 1) {
|
|
156
|
+
const node = pending[index]; ordered.push(node);
|
|
157
|
+
for (const child of children.get(node.id)) { indegree.set(child, indegree.get(child) - 1); if (indegree.get(child) === 0) pending.push(byId.get(child)); }
|
|
158
|
+
}
|
|
159
|
+
if (ordered.length !== nodes.length) return coverage + '<p>Graph dependencies cannot be displayed safely.</p>';
|
|
160
|
+
nodes = ordered;
|
|
161
|
+
}
|
|
101
162
|
const levels = new Map(nodes.map(n => [n.id, 0]));
|
|
102
163
|
const parents = new Map(nodes.map(n => [n.id, []]));
|
|
103
164
|
for (const e of graph.edges) parents.get(e.to).push(e);
|
|
104
|
-
const
|
|
165
|
+
const laneFor = graphLane;
|
|
166
|
+
const lanes = ['controller', 'codex', 'claude-code', 'unknown'].filter(host => nodes.some(node => laneFor(node) === host)), laneLayout = lanes.length > 1;
|
|
167
|
+
const rows = new Map(), positions = new Map(), nodeRows = new Map(), laneCounts = new Map();
|
|
105
168
|
for (const n of nodes) {
|
|
106
169
|
const level = Math.max(0, ...parents.get(n.id).map(e => levels.get(e.from) + 1));
|
|
107
170
|
levels.set(n.id, level);
|
|
108
|
-
const
|
|
109
|
-
|
|
171
|
+
const key = laneLayout ? `${laneFor(n)}:${level}` : level, row = rows.get(key) || 0;
|
|
172
|
+
rows.set(key, row + 1); nodeRows.set(n.id, row);
|
|
173
|
+
laneCounts.set(laneFor(n), Math.max(laneCounts.get(laneFor(n)) || 0, row + 1));
|
|
174
|
+
}
|
|
175
|
+
const offsets = new Map(), extents = new Map(); let extent = 0;
|
|
176
|
+
if (laneLayout) for (const host of lanes) {
|
|
177
|
+
const size = (laneCounts.get(host) || 1) * (horizontal ? 112 : 236) + 48;
|
|
178
|
+
offsets.set(host, extent); extents.set(host, size); extent += size;
|
|
110
179
|
}
|
|
111
|
-
|
|
180
|
+
for (const n of nodes) {
|
|
181
|
+
const level = levels.get(n.id), row = nodeRows.get(n.id), offset = laneLayout ? offsets.get(laneFor(n)) : 0;
|
|
182
|
+
positions.set(n.id, horizontal ? { x: 24 + level * 272, y: (laneLayout ? offset + 48 : 24) + row * 112 }
|
|
183
|
+
: { x: offset + 24 + row * 236, y: (laneLayout ? 48 : 24) + level * 112 });
|
|
184
|
+
}
|
|
185
|
+
const width = horizontal ? (Math.max(...levels.values()) + 1) * 272 : laneLayout ? Math.max(500, extent) : Math.max(500, Math.max(...rows.values()) * 236 + 24);
|
|
186
|
+
const height = horizontal ? laneLayout ? extent : Math.max(240, Math.max(...rows.values()) * 112 + 24) : (Math.max(...levels.values()) + 1) * 112 + (laneLayout ? 48 : 24);
|
|
187
|
+
const laneBoxes = laneLayout ? lanes.map(host => {
|
|
188
|
+
const label = ({ controller: 'Controller', codex: 'Codex', 'claude-code': 'Claude Code', unknown: 'Host not recorded' })[host], offset = offsets.get(host);
|
|
189
|
+
return `<g class="execution-host-lane" role="group" aria-label="Host lane: ${label}"><rect x="${horizontal ? 0 : offset}" y="${horizontal ? offset : 0}" width="${horizontal ? width : extents.get(host)}" height="${horizontal ? extents.get(host) : height}" fill="var(--bg-page)" stroke="var(--border-strong)"/><text x="${horizontal ? 12 : offset + 12}" y="${horizontal ? offset + 24 : 24}" fill="currentColor">${label}</text></g>`;
|
|
190
|
+
}).join('') : '';
|
|
112
191
|
const edges = graph.edges.map(e => {
|
|
113
192
|
const a = positions.get(e.from), b = positions.get(e.to);
|
|
114
193
|
if (horizontal) return `<path d="M${a.x + 208} ${a.y + 39} C${a.x + 240} ${a.y + 39},${b.x - 32} ${b.y + 39},${b.x} ${b.y + 39}" marker-end="url(#execution-arrow)"><title>${esc(e.kind)}: ${esc(e.from)} to ${esc(e.to)}</title></path>`;
|
|
@@ -117,22 +196,44 @@
|
|
|
117
196
|
const short = value => String(value || '').length > 27 ? String(value).slice(0, 26) + '…' : String(value || '');
|
|
118
197
|
const boxes = nodes.map(n => {
|
|
119
198
|
const p = positions.get(n.id);
|
|
120
|
-
return `<g class="execution-node" role="button" tabindex="0" data-graph-node="${esc(n.id)}" aria-label="${esc(n.kind)} ${esc(n.role || '')}: ${esc(n.status)}. Open recorded details." transform="translate(${p.x},${p.y})">${options.canvas ? `<title>${esc(n.kind)} · ${esc(n
|
|
199
|
+
return `<g class="execution-node" role="button" tabindex="0" data-graph-node="${esc(n.id)}" data-graph-search-text="${esc([n.id, n.role || n.kind, graphHostLabel(n), n.attemptId || ''].join(' '))}" aria-label="${esc(n.kind)} ${esc(n.role || '')}: ${esc(n.status)}. Open recorded details." transform="translate(${p.x},${p.y})">${options.canvas ? `<title>${esc(n.kind)} · ${esc(graphRoleLabel(n))} · ${esc(n.status)}</title>` : ''}<rect width="208" height="78" rx="6"/><text x="12" y="23">${esc(short(n.role || n.kind))}</text><text x="12" y="45">${options.canvas ? esc(short(`${n.kind} · ${n.status}`)) : `${esc(n.kind)} · ${esc(n.status)}`}</text><text x="12" y="64">${esc(graphHostLabel(n))}</text></g>`;
|
|
121
200
|
}).join('');
|
|
122
|
-
const details = nodes.map(n => `<details class="execution-node-detail" data-graph-detail="${esc(n.id)}"><summary>${esc(n.id)} · ${esc(n.kind)} · ${esc(n
|
|
123
|
-
+ [['Attempt', n.attemptId], ['Host', n.host], ['Recorded at', n.at], ['Verdict', n.verdict], ['Wave', n.waveId], ['Gates', (n.gates || []).join(', ')],
|
|
201
|
+
const details = nodes.map(n => `<details class="execution-node-detail" data-graph-detail="${esc(n.id)}"><summary>${esc(n.id)} · ${esc(n.kind)} · ${esc(graphRoleLabel(n))} · ${esc(n.status)}</summary><dl>`
|
|
202
|
+
+ [['Attempt', n.attemptId], ['Host', n.host], ...(hostObserved ? [['Source', n.source || 'host-observation'], ['Source version', n.sourceVersion || 'unverified'], ['Phase', n.phase], ['Outcome', n.outcome]] : []), ['Recorded at', n.at], ['Verdict', n.verdict], ['Wave', n.waveId], ['Gates', (n.gates || []).join(', ')],
|
|
124
203
|
['Workflow snapshot SHA-256', n.graphDigest], ['Input receipt SHA-256', n.inputDigest], ['Output receipt SHA-256', n.outputDigest],
|
|
125
204
|
[n.kind === 'reuse' ? 'Historical report SHA-256 (worker not invoked)' : 'Stage context SHA-256', n.contextDigest], ['Result SHA-256', n.resultDigest]]
|
|
126
205
|
.map(([key, value]) => `<dt>${esc(key)}</dt><dd><code>${esc(value || 'Not recorded')}</code></dd>`).join('')
|
|
127
206
|
+ `<dt>Recorded predecessors</dt><dd>${parents.get(n.id).map(e => `${esc(e.from)} (${esc(e.kind)})`).join(', ') || 'None recorded; not proof of complete external inputs'}</dd></dl></details>`).join('');
|
|
128
|
-
|
|
129
|
-
|
|
207
|
+
const latestAttempts = graph.nodes.filter(node => node.kind === 'attempt'), latestPool = latestAttempts.length ? latestAttempts : graph.nodes;
|
|
208
|
+
const latest = [...latestPool].sort((a, b) => (Date.parse(a.at) || 0) - (Date.parse(b.at) || 0)).at(-1);
|
|
209
|
+
const navigation = `<div class="execution-graph-tools" role="group" aria-label="Graph node navigation"><label>Find graph node <input type="search" data-graph-find="query" maxlength="160" placeholder="Role, host or node ID"></label><button type="button" class="gate-btn" data-graph-search="find">Find node</button>${latest ? `<button type="button" class="gate-btn" data-graph-latest="${esc(latest.id)}">Latest recorded ${latestAttempts.length ? 'attempt' : 'observation'}</button>` : ''}<span data-graph-search-feedback role="status" aria-live="polite"></span></div>`;
|
|
210
|
+
return `<section class="execution-graph" aria-label="${hostObserved ? 'Host-observed' : 'Controller-recorded'} execution graph"><h3>${hostObserved ? 'Host-observed graph' : 'Controller-recorded graph'}</h3><p>${nodes.length} nodes · ${graph.edges.length} recorded edges${hostObserved ? '. Partial history: host delivery completeness is not attested.' : ` · ${graph.snapshots?.length || 0} workflow snapshots. ${graph.complete ? 'Controller history recorded from run creation.' : 'Partial history: missing edges are not inferred.'}`}</p>`
|
|
211
|
+
+ (hostObserved ? coverage : `<p class="work-meta">Dispatch, input, verification, gate and join edges describe controller observations. Intent is not proof of a live worker; recorded approval is not deployment or task acceptance. Historical reuse does not invoke a worker. Stage context digests exclude separately admitted domain packets.</p>`)
|
|
130
212
|
+ (graph.reason ? `<p class="work-warning">${esc(graph.reason)}</p>` : '')
|
|
213
|
+
+ (laneLayout ? '<p class="work-meta">Visual grouping only; host lanes do not establish dependency or concurrency.</p>' : '') + navigation
|
|
131
214
|
+ `<div class="execution-graph-tools execution-graph-toolbar" role="group" aria-label="Graph zoom"><button type="button" class="gate-btn" data-graph-scale="0.75">Zoom out</button><button type="button" class="gate-btn" data-graph-scale="1.333333">Zoom in</button><button type="button" class="gate-btn" data-graph-scale="reset">Reset zoom</button>${options.canvas ? `<button type="button" class="gate-btn" data-graph-scale="fit">Fit graph</button><button type="button" class="gate-btn" data-graph-direction="lr" aria-pressed="${horizontal}">Left to right</button><button type="button" class="gate-btn" data-graph-direction="tb" aria-pressed="${!horizontal}">Top to bottom</button>` : ''}</div>`
|
|
132
|
-
+ `<div class="execution-graph-canvas" tabindex="0" aria-label="Scrollable graph. Tab to a node and press Enter for details."><svg data-graph-layout="${horizontal ? 'lr' : 'tb'}" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="group" aria-label="Recorded execution dependencies"><defs><marker id="execution-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0 0 L8 4 L0 8"/></marker></defs
|
|
215
|
+
+ `<div class="execution-graph-canvas" tabindex="0" aria-label="Scrollable graph. Tab to a node and press Enter for details."><svg data-graph-layout="${horizontal ? 'lr' : 'tb'}" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="group" aria-label="Recorded execution dependencies"><defs><marker id="execution-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0 0 L8 4 L0 8"/></marker></defs>${laneBoxes}<g class="execution-edges" fill="none">${edges}</g>${boxes}</svg></div>`
|
|
133
216
|
+ (options.canvas ? '<details class="execution-node-list"><summary>Recorded node details · keyboard list</summary>' : '<h3>Recorded node details</h3>') + `<p class="work-meta">Use the graph or expand a node below. Digests bind recorded inputs, not correctness.</p>${details}${options.canvas ? '</details>' : ''}</section>`;
|
|
134
217
|
}
|
|
218
|
+
function focusGraphNode(node) {
|
|
219
|
+
if (!node) return false;
|
|
220
|
+
node.focus({ preventScroll: true }); node.scrollIntoView?.({ block: 'nearest', inline: 'nearest' }); return true;
|
|
221
|
+
}
|
|
222
|
+
function focusGraphMatch(graph, value) {
|
|
223
|
+
if (!graph) return;
|
|
224
|
+
const query = String(value || '').trim().slice(0, 160).toLowerCase();
|
|
225
|
+
const matches = [...graph.querySelectorAll('[data-graph-node]')].filter(node => String(node.dataset.graphSearchText || '').toLowerCase().includes(query));
|
|
226
|
+
const feedback = graph.querySelector('[data-graph-search-feedback]');
|
|
227
|
+
if (!query) { if (feedback) feedback.textContent = 'Enter a role, host or node ID.'; return; }
|
|
228
|
+
if (feedback) feedback.textContent = matches.length ? `Focused first of ${matches.length} matching nodes. All dependencies remain visible.` : 'No matching node on this page.';
|
|
229
|
+
if (matches.length) focusGraphNode(matches[0]);
|
|
230
|
+
}
|
|
135
231
|
function graphInteraction(target) {
|
|
232
|
+
const search = target.closest('[data-graph-search]');
|
|
233
|
+
if (search?.dataset.graphSearch === 'find') { const graph = search.closest('.execution-graph'); focusGraphMatch(graph, graph?.querySelector('[data-graph-find]')?.value); return true; }
|
|
234
|
+
const latest = target.closest('[data-graph-latest]');
|
|
235
|
+
if (latest?.dataset.graphLatest) { const graph = latest.closest('.execution-graph'); focusGraphNode([...graph?.querySelectorAll('[data-graph-node]') || []].find(node => node.dataset.graphNode === latest.dataset.graphLatest)); return true; }
|
|
236
|
+
|
|
136
237
|
const direction = target.closest('[data-graph-direction]');
|
|
137
238
|
if (config?.canvas && ['lr', 'tb'].includes(direction?.dataset.graphDirection)) { graphDirection = direction.dataset.graphDirection; closeWorkDialog(false); paint(); document.querySelector?.('[data-graph-direction][aria-pressed="true"]')?.focus(); return true; }
|
|
138
239
|
const node = target.closest('[data-graph-node]');
|
|
@@ -218,7 +319,7 @@
|
|
|
218
319
|
: render({ ...data, sources: [], sessions: [], entries: [{ ...selected, inspector: null, evidenceSections: null }] }, selected.terminal);
|
|
219
320
|
const runLabel = selected.title.match(/^Controlled run ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
|
|
220
321
|
const title = runLabel ? `Controlled run <small class="work-meta work-run-identifier">${esc(runLabel[1].slice(0, 8))}</small>` : esc(selected.title);
|
|
221
|
-
return `<section class="work-workspace work-detail-only" data-selected-work="${esc(selected.key)}"><header class="work-task-header"><div class="work-task-navigation">${back}<span class="work-meta">${esc(selected.kind)} · ${esc(selected.host || 'Host not linked')}</span><button type="button" data-work-switch>Switch task</button><button type="button" data-work-previous${rows.findIndex(e => e.key === selected.key) > 0 ? '' : ' disabled'} aria-label="Previous task in current filter">Previous</button><button type="button" data-work-next${rows.findIndex(e => e.key === selected.key) >= 0 && rows.findIndex(e => e.key === selected.key) < rows.length - 1 ? '' : ' disabled'} aria-label="Next task in current filter">Next</button></div><h2>${title}</h2><p>${esc(phaseLabel(selected))}</p></header>${tabs}${feedback}${tab === 'overview' ? '' : observation}<div id="work-detail-panel" role="tabpanel" aria-labelledby="work-tab-${tab}" tabindex="0">${detail}</div></section>`;
|
|
322
|
+
return `<section class="work-workspace work-detail-only" data-selected-work="${esc(selected.key)}" data-observation-scope="${esc(JSON.stringify([options.observationProject || data.projectId || null, selected.key]))}"><header class="work-task-header"><div class="work-task-navigation">${back}<span class="work-meta">${esc(selected.kind)} · ${esc(selected.host || 'Host not linked')}</span><button type="button" data-work-switch>Switch task</button><button type="button" data-work-previous${rows.findIndex(e => e.key === selected.key) > 0 ? '' : ' disabled'} aria-label="Previous task in current filter">Previous</button><button type="button" data-work-next${rows.findIndex(e => e.key === selected.key) >= 0 && rows.findIndex(e => e.key === selected.key) < rows.length - 1 ? '' : ' disabled'} aria-label="Next task in current filter">Next</button></div><h2>${title}</h2><p>${esc(phaseLabel(selected))}</p></header>${tabs}${feedback}${tab === 'overview' ? '' : observation}<div id="work-detail-panel" role="tabpanel" aria-labelledby="work-tab-${tab}" tabindex="0">${detail}</div></section>`;
|
|
222
323
|
}
|
|
223
324
|
function renderEvidence(sections) {
|
|
224
325
|
if (!Array.isArray(sections)) return '';
|
|
@@ -344,6 +445,25 @@
|
|
|
344
445
|
const dialog = makeWorkDialog('work-node-inspector', 'Recorded node details', opener, '<p class="work-meta">Recorded inputs, not proof of correctness, current activity or acceptance.</p>' + content, false);
|
|
345
446
|
dialog.querySelector('summary')?.focus();
|
|
346
447
|
}
|
|
448
|
+
async function nextObservationPage(button) {
|
|
449
|
+
const project = config.project(), entryKey = selectedKey, oldSnapshot = snapshot;
|
|
450
|
+
try {
|
|
451
|
+
const binding = JSON.parse(button.dataset.hostGraphNext), query = new URLSearchParams({ host: binding.host, session: binding.sessionId, run: binding.runId, cursor: binding.cursor });
|
|
452
|
+
if (project) query.set('project', project);
|
|
453
|
+
button.disabled = true;
|
|
454
|
+
const page = await config.read('/api/host-graph?' + query.toString());
|
|
455
|
+
if (project !== config.project() || entryKey !== selectedKey || snapshot !== oldSnapshot) return;
|
|
456
|
+
if (!page || page.error || page.source !== 'host-observation') throw Error('unavailable');
|
|
457
|
+
const entry = snapshot.entries.find(e => e.key === entryKey), graphs = [entry?.execution?.graph, ...(entry?.execution?.observations || [])];
|
|
458
|
+
const graph = graphs.find(g => g?.binding?.sessionId === binding.sessionId && g?.binding?.host === binding.host && g?.binding?.runId === binding.runId);
|
|
459
|
+
if (!graph) throw Error('unavailable');
|
|
460
|
+
Object.assign(graph, page); paint();
|
|
461
|
+
status(`Observation page loaded: ${page.nodes?.length || 0} recorded nodes.`);
|
|
462
|
+
const first = page.nodes?.[0]?.id;
|
|
463
|
+
if (first) [...document.querySelectorAll('[data-graph-node]')].find(node => node.dataset.graphNode === first)?.focus();
|
|
464
|
+
} catch { status('Observation page is unavailable. Refresh the selected project and retry.'); }
|
|
465
|
+
finally { if (button.isConnected) button.disabled = false; }
|
|
466
|
+
}
|
|
347
467
|
async function refresh() {
|
|
348
468
|
const seq = ++generation, project = config.project();
|
|
349
469
|
try {
|
|
@@ -362,6 +482,9 @@
|
|
|
362
482
|
function paint() {
|
|
363
483
|
if (!snapshot || (config?.canvas && snapshotProject !== config.project())) return;
|
|
364
484
|
previews.clear(); // Repaint removes the reviewed diff and its confirmation controls.
|
|
485
|
+
const oldWorkspace = document.querySelector?.('[data-observation-scope]');
|
|
486
|
+
const sameObservationScope = oldWorkspace?.dataset.observationScope === JSON.stringify([snapshotProject, selectedKey]);
|
|
487
|
+
const expandedHosts = sameObservationScope ? [...oldWorkspace.querySelectorAll('[data-observation-key][open]')].slice(0, 16).map(detail => detail.dataset.observationKey).filter(key => typeof key === 'string' && key.length <= 1024) : [];
|
|
365
488
|
const expanded = [...document.querySelectorAll('.work-card details[open]')].map(d => `${d.closest('[data-entry-key]').dataset.entryKey}:${d.dataset.workDetail}`);
|
|
366
489
|
const focused = document.activeElement;
|
|
367
490
|
const focusKey = focused?.dataset?.workKey, focusAction = focused?.dataset?.workAction;
|
|
@@ -370,12 +493,13 @@
|
|
|
370
493
|
const oldCanvas = config.canvas ? document.querySelector?.('.execution-graph-canvas') : null;
|
|
371
494
|
const oldSvg = oldCanvas?.querySelector('svg'), oldZoom = oldSvg ? Number(oldSvg.dataset.zoom || 1) : 1;
|
|
372
495
|
const graphScroll = oldCanvas?.closest('[data-selected-work]')?.dataset.selectedWork === selectedKey && oldSvg?.dataset.graphLayout === graphDirection ? { left: oldCanvas.scrollLeft, top: oldCanvas.scrollTop } : null;
|
|
373
|
-
document.getElementById('work-list').innerHTML = renderCockpit(snapshot, { search: filter, selectedKey, category, tab, canvas: !!config.canvas, direction: graphDirection });
|
|
496
|
+
document.getElementById('work-list').innerHTML = renderCockpit(snapshot, { search: filter, selectedKey, category, tab, canvas: !!config.canvas, direction: graphDirection, observationProject: snapshotProject });
|
|
374
497
|
document.getElementById('history-list').innerHTML = render(snapshot, true, filter);
|
|
375
498
|
if (graphScroll) {
|
|
376
499
|
const canvas = document.querySelector?.('.execution-graph-canvas'), svg = canvas?.querySelector('svg');
|
|
377
500
|
if (svg && Number.isFinite(oldZoom) && oldZoom > 0) { const bounds = svg.viewBox.baseVal; svg.dataset.zoom = String(oldZoom); svg.setAttribute('width', String(bounds.width * oldZoom)); svg.setAttribute('height', String(bounds.height * oldZoom)); canvas.scrollLeft = graphScroll.left; canvas.scrollTop = graphScroll.top; }
|
|
378
501
|
}
|
|
502
|
+
for (const detail of document.querySelectorAll('[data-observation-key]')) if (expandedHosts.includes(detail.dataset.observationKey)) detail.open = true;
|
|
379
503
|
for (const card of document.querySelectorAll('[data-entry-key]')) for (const detail of card.querySelectorAll('details'))
|
|
380
504
|
if (expanded.includes(`${card.dataset.entryKey}:${detail.dataset.workDetail}`)) detail.open = true;
|
|
381
505
|
if (focusKey) [...document.querySelectorAll('[data-work-key]')].find(b => b.dataset.workKey === focusKey && b.dataset.workAction === focusAction)?.focus();
|
|
@@ -409,6 +533,8 @@
|
|
|
409
533
|
});
|
|
410
534
|
for (const id of ['work-list', 'history-list']) document.getElementById(id).addEventListener('click', event => {
|
|
411
535
|
if (config.canvas && snapshotProject !== config.project()) return;
|
|
536
|
+
const pageButton = event.target.closest('[data-host-graph-next]');
|
|
537
|
+
if (pageButton?.dataset.hostGraphNext) { nextObservationPage(pageButton); return; }
|
|
412
538
|
if (graphInteraction(event.target)) return;
|
|
413
539
|
if (!snapshot || (config.canvas && snapshotProject !== config.project())) return;
|
|
414
540
|
if (config.canvas) {
|
|
@@ -438,7 +564,9 @@
|
|
|
438
564
|
});
|
|
439
565
|
document.getElementById('work-list').addEventListener('keydown', event => {
|
|
440
566
|
if (config.canvas && snapshotProject !== config.project()) return;
|
|
441
|
-
|
|
567
|
+
const graphFind = event.target.closest('[data-graph-find]');
|
|
568
|
+
if (event.key === 'Enter' && graphFind?.dataset.graphFind === 'query') { event.preventDefault(); focusGraphMatch(graphFind.closest('.execution-graph'), graphFind.value); return; }
|
|
569
|
+
if (['Enter', ' '].includes(event.key) && event.target.closest('[data-graph-node]')?.dataset.graphNode) { event.preventDefault(); graphInteraction(event.target); return; }
|
|
442
570
|
const button = event.target.closest('[data-work-tab]');
|
|
443
571
|
if (!button || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
|
444
572
|
const tabs = ['overview', 'execution', 'evidence'], current = tabs.indexOf(button.dataset.workTab);
|
|
@@ -61,6 +61,7 @@ try {
|
|
|
61
61
|
if (command === 'start') {
|
|
62
62
|
const root = realpathSync(resolve(value('--dir') || '.'));
|
|
63
63
|
const taskId = value('--task-id');
|
|
64
|
+
const observeHostGraph = args.includes('--observe-graph');
|
|
64
65
|
const allowed = (value('--allow') || '').split(',').map(p => p.trim()).filter(Boolean);
|
|
65
66
|
let acceptance = [], intent = value('--intent') || 'delivery', maxAttempts = value('--max-attempts') === null ? 3 : Number(value('--max-attempts'));
|
|
66
67
|
if (taskId) {
|
|
@@ -119,6 +120,7 @@ try {
|
|
|
119
120
|
state = newRun({ root, prompt: value('--prompt'), checkPolicy, releasePolicy, specialistPolicy, domainPolicy,
|
|
120
121
|
allowed, intent, entry: intent === 'research' ? 'project-auditor' : value('--entry') || 'product-owner',
|
|
121
122
|
maxAttempts, hostRoutes, gatePolicy });
|
|
123
|
+
state.observeHostGraph = observeHostGraph;
|
|
122
124
|
state.taskId = taskId || work.task.taskId;
|
|
123
125
|
state.acceptance = acceptance;
|
|
124
126
|
if (args.includes('--benchmark-trial')) {
|