agentxchain 0.8.7 → 2.1.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.
Files changed (94) hide show
  1. package/README.md +123 -154
  2. package/bin/agentxchain.js +240 -8
  3. package/dashboard/app.js +305 -0
  4. package/dashboard/components/blocked.js +145 -0
  5. package/dashboard/components/cross-repo.js +126 -0
  6. package/dashboard/components/gate.js +311 -0
  7. package/dashboard/components/hooks.js +177 -0
  8. package/dashboard/components/initiative.js +147 -0
  9. package/dashboard/components/ledger.js +165 -0
  10. package/dashboard/components/timeline.js +222 -0
  11. package/dashboard/index.html +352 -0
  12. package/package.json +16 -7
  13. package/scripts/agentxchain-autonudge.applescript +32 -5
  14. package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
  15. package/scripts/publish-from-tag.sh +88 -0
  16. package/scripts/release-postflight.sh +231 -0
  17. package/scripts/release-preflight.sh +167 -0
  18. package/scripts/run-autonudge.sh +1 -1
  19. package/src/adapters/claude-code.js +7 -14
  20. package/src/adapters/cursor-local.js +17 -16
  21. package/src/commands/accept-turn.js +160 -0
  22. package/src/commands/approve-completion.js +80 -0
  23. package/src/commands/approve-transition.js +85 -0
  24. package/src/commands/branch.js +2 -2
  25. package/src/commands/claim.js +84 -9
  26. package/src/commands/config.js +16 -0
  27. package/src/commands/dashboard.js +70 -0
  28. package/src/commands/doctor.js +9 -1
  29. package/src/commands/init.js +540 -5
  30. package/src/commands/migrate.js +348 -0
  31. package/src/commands/multi.js +549 -0
  32. package/src/commands/plugin.js +157 -0
  33. package/src/commands/reject-turn.js +204 -0
  34. package/src/commands/resume.js +389 -0
  35. package/src/commands/status.js +196 -3
  36. package/src/commands/step.js +947 -0
  37. package/src/commands/stop.js +65 -33
  38. package/src/commands/template-list.js +33 -0
  39. package/src/commands/template-set.js +279 -0
  40. package/src/commands/update.js +24 -3
  41. package/src/commands/validate.js +20 -11
  42. package/src/commands/verify.js +71 -0
  43. package/src/commands/watch.js +112 -25
  44. package/src/lib/adapters/api-proxy-adapter.js +1076 -0
  45. package/src/lib/adapters/local-cli-adapter.js +337 -0
  46. package/src/lib/adapters/manual-adapter.js +169 -0
  47. package/src/lib/blocked-state.js +94 -0
  48. package/src/lib/config.js +143 -12
  49. package/src/lib/context-compressor.js +121 -0
  50. package/src/lib/context-section-parser.js +220 -0
  51. package/src/lib/coordinator-acceptance.js +428 -0
  52. package/src/lib/coordinator-config.js +461 -0
  53. package/src/lib/coordinator-dispatch.js +276 -0
  54. package/src/lib/coordinator-gates.js +487 -0
  55. package/src/lib/coordinator-hooks.js +239 -0
  56. package/src/lib/coordinator-recovery.js +523 -0
  57. package/src/lib/coordinator-state.js +365 -0
  58. package/src/lib/cross-repo-context.js +247 -0
  59. package/src/lib/dashboard/bridge-server.js +284 -0
  60. package/src/lib/dashboard/file-watcher.js +93 -0
  61. package/src/lib/dashboard/state-reader.js +96 -0
  62. package/src/lib/dispatch-bundle.js +568 -0
  63. package/src/lib/dispatch-manifest.js +252 -0
  64. package/src/lib/filter-agents.js +12 -0
  65. package/src/lib/gate-evaluator.js +285 -0
  66. package/src/lib/generate-vscode.js +158 -68
  67. package/src/lib/governed-state.js +2139 -0
  68. package/src/lib/governed-templates.js +145 -0
  69. package/src/lib/hook-runner.js +788 -0
  70. package/src/lib/next-owner.js +61 -6
  71. package/src/lib/normalized-config.js +539 -0
  72. package/src/lib/notify.js +14 -12
  73. package/src/lib/plugin-config-schema.js +192 -0
  74. package/src/lib/plugins.js +692 -0
  75. package/src/lib/prompt-core.js +108 -0
  76. package/src/lib/protocol-conformance.js +291 -0
  77. package/src/lib/reference-conformance-adapter.js +717 -0
  78. package/src/lib/repo-observer.js +597 -0
  79. package/src/lib/repo.js +0 -31
  80. package/src/lib/safe-write.js +44 -0
  81. package/src/lib/schema.js +189 -0
  82. package/src/lib/schemas/turn-result.schema.json +205 -0
  83. package/src/lib/seed-prompt-polling.js +15 -73
  84. package/src/lib/seed-prompt.js +17 -63
  85. package/src/lib/token-budget.js +206 -0
  86. package/src/lib/token-counter.js +27 -0
  87. package/src/lib/turn-paths.js +67 -0
  88. package/src/lib/turn-result-validator.js +496 -0
  89. package/src/lib/validation.js +167 -19
  90. package/src/lib/verify-command.js +72 -0
  91. package/src/templates/governed/api-service.json +31 -0
  92. package/src/templates/governed/cli-tool.json +30 -0
  93. package/src/templates/governed/generic.json +10 -0
  94. package/src/templates/governed/web-app.json +30 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Dashboard bridge server — read-only HTTP + WebSocket server.
3
+ *
4
+ * Serves dashboard static assets, exposes read-only API endpoints for
5
+ * .agentxchain/ state files, and pushes WebSocket invalidation events
6
+ * when watched files change.
7
+ *
8
+ * Security: binds to 127.0.0.1 only. No write RPC. No mutation endpoints.
9
+ * See: DEC-DASH-002, DEC-DASH-003, AT-DASH-007, AT-DASH-008.
10
+ */
11
+
12
+ import { createServer } from 'http';
13
+ import { createHash } from 'crypto';
14
+ import { readFileSync, existsSync } from 'fs';
15
+ import { join, extname, resolve, sep } from 'path';
16
+ import { readResource } from './state-reader.js';
17
+ import { FileWatcher } from './file-watcher.js';
18
+
19
+ const MIME_TYPES = {
20
+ '.html': 'text/html; charset=utf-8',
21
+ '.js': 'application/javascript; charset=utf-8',
22
+ '.css': 'text/css; charset=utf-8',
23
+ '.json': 'application/json; charset=utf-8',
24
+ '.svg': 'image/svg+xml',
25
+ '.png': 'image/png',
26
+ };
27
+
28
+ // ── Minimal WebSocket server (RFC 6455, text frames only) ───────────────────
29
+
30
+ const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
31
+
32
+ function acceptWebSocket(req, socket) {
33
+ const key = req.headers['sec-websocket-key'];
34
+ if (!key || req.headers.upgrade?.toLowerCase() !== 'websocket' || req.headers['sec-websocket-version'] !== '13') {
35
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
36
+ socket.destroy();
37
+ return null;
38
+ }
39
+
40
+ const accept = createHash('sha1')
41
+ .update(key + WS_GUID)
42
+ .digest('base64');
43
+
44
+ socket.write(
45
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
46
+ 'Upgrade: websocket\r\n' +
47
+ 'Connection: Upgrade\r\n' +
48
+ `Sec-WebSocket-Accept: ${accept}\r\n` +
49
+ '\r\n'
50
+ );
51
+ return socket;
52
+ }
53
+
54
+ function createWsFrame(opcode, payload = Buffer.alloc(0)) {
55
+ const len = payload.length;
56
+ let header;
57
+ if (len < 126) {
58
+ header = Buffer.alloc(2);
59
+ header[0] = 0x80 | opcode;
60
+ header[1] = len;
61
+ } else if (len < 65536) {
62
+ header = Buffer.alloc(4);
63
+ header[0] = 0x80 | opcode;
64
+ header[1] = 126;
65
+ header.writeUInt16BE(len, 2);
66
+ } else {
67
+ header = Buffer.alloc(10);
68
+ header[0] = 0x80 | opcode;
69
+ header[1] = 127;
70
+ header.writeBigUInt64BE(BigInt(len), 2);
71
+ }
72
+ return Buffer.concat([header, payload]);
73
+ }
74
+
75
+ function sendWsFrame(socket, text) {
76
+ const payload = Buffer.from(text, 'utf8');
77
+ try {
78
+ socket.write(createWsFrame(0x01, payload));
79
+ } catch {
80
+ // Socket already closed
81
+ }
82
+ }
83
+
84
+ function sendWsControlFrame(socket, opcode, payload = Buffer.alloc(0)) {
85
+ try {
86
+ socket.write(createWsFrame(opcode, payload));
87
+ } catch {
88
+ // Socket already closed
89
+ }
90
+ }
91
+
92
+ function parseClientFrame(data) {
93
+ if (!Buffer.isBuffer(data) || data.length < 2) return null;
94
+
95
+ const opcode = data[0] & 0x0f;
96
+ const masked = (data[1] & 0x80) !== 0;
97
+ let payloadLen = data[1] & 0x7f;
98
+ let offset = 2;
99
+
100
+ if (payloadLen === 126) {
101
+ if (data.length < 4) return null;
102
+ payloadLen = data.readUInt16BE(2);
103
+ offset = 4;
104
+ } else if (payloadLen === 127) {
105
+ if (data.length < 10) return null;
106
+ payloadLen = Number(data.readBigUInt64BE(2));
107
+ offset = 10;
108
+ }
109
+
110
+ const maskOffset = offset;
111
+ if (masked) {
112
+ if (data.length < maskOffset + 4) return null;
113
+ offset += 4;
114
+ }
115
+
116
+ if (data.length < offset + payloadLen) return null;
117
+ const payload = Buffer.from(data.slice(offset, offset + payloadLen));
118
+
119
+ if (masked) {
120
+ const mask = data.slice(maskOffset, maskOffset + 4);
121
+ for (let i = 0; i < payload.length; i += 1) {
122
+ payload[i] ^= mask[i % 4];
123
+ }
124
+ }
125
+
126
+ return { opcode, payload };
127
+ }
128
+
129
+ function sendWsError(socket, error) {
130
+ sendWsFrame(socket, JSON.stringify({
131
+ type: 'error',
132
+ error,
133
+ }));
134
+ }
135
+
136
+ function resolveDashboardAssetPath(dashboardDir, pathname) {
137
+ let decodedPath;
138
+ try {
139
+ decodedPath = decodeURIComponent(pathname);
140
+ } catch {
141
+ return { blocked: true, filePath: null };
142
+ }
143
+
144
+ const relativePath = decodedPath === '/' || decodedPath === '/index.html'
145
+ ? 'index.html'
146
+ : decodedPath.replace(/^\/+/, '');
147
+ const dashboardRoot = resolve(dashboardDir);
148
+ const filePath = resolve(dashboardRoot, relativePath);
149
+
150
+ if (filePath !== dashboardRoot && !filePath.startsWith(dashboardRoot + sep)) {
151
+ return { blocked: true, filePath: null };
152
+ }
153
+
154
+ return { blocked: false, filePath };
155
+ }
156
+
157
+ // ── Bridge Server ───────────────────────────────────────────────────────────
158
+
159
+ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847 }) {
160
+ const wsClients = new Set();
161
+ const watcher = new FileWatcher(agentxchainDir);
162
+
163
+ // Broadcast invalidation events to all connected WebSocket clients
164
+ watcher.on('invalidate', ({ resource }) => {
165
+ const msg = JSON.stringify({ type: 'invalidate', resource });
166
+ for (const socket of wsClients) {
167
+ sendWsFrame(socket, msg);
168
+ }
169
+ });
170
+
171
+ const server = createServer((req, res) => {
172
+ // Block all mutation methods (AT-DASH-008)
173
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
174
+ res.writeHead(405, { 'Content-Type': 'application/json', 'Allow': 'GET, HEAD' });
175
+ res.end(JSON.stringify({ error: 'Method not allowed. Dashboard is read-only in v2.0.' }));
176
+ return;
177
+ }
178
+
179
+ const url = new URL(req.url, `http://${req.headers.host}`);
180
+ const pathname = url.pathname;
181
+
182
+ // API routes
183
+ if (pathname.startsWith('/api/')) {
184
+ const result = readResource(agentxchainDir, pathname);
185
+ if (!result) {
186
+ res.writeHead(404, { 'Content-Type': 'application/json' });
187
+ res.end(JSON.stringify({ error: 'Resource not found' }));
188
+ return;
189
+ }
190
+ res.writeHead(200, {
191
+ 'Content-Type': 'application/json; charset=utf-8',
192
+ 'Cache-Control': 'no-cache',
193
+ });
194
+ res.end(JSON.stringify(result.data));
195
+ return;
196
+ }
197
+
198
+ // Static asset serving
199
+ const { blocked, filePath: resolvedPath } = resolveDashboardAssetPath(dashboardDir, pathname);
200
+ if (blocked) {
201
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
202
+ res.end('Forbidden');
203
+ return;
204
+ }
205
+ let filePath = resolvedPath;
206
+
207
+ if (!existsSync(filePath)) {
208
+ // SPA fallback: serve index.html for unknown routes
209
+ filePath = join(dashboardDir, 'index.html');
210
+ }
211
+
212
+ try {
213
+ const content = readFileSync(filePath);
214
+ const ext = extname(filePath);
215
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
216
+ res.writeHead(200, { 'Content-Type': contentType });
217
+ res.end(content);
218
+ } catch {
219
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
220
+ res.end('Not found');
221
+ }
222
+ });
223
+
224
+ // WebSocket upgrade handler
225
+ server.on('upgrade', (req, socket, head) => {
226
+ const url = new URL(req.url, `http://${req.headers.host}`);
227
+ if (url.pathname !== '/ws') {
228
+ socket.destroy();
229
+ return;
230
+ }
231
+
232
+ const ws = acceptWebSocket(req, socket);
233
+ if (!ws) return;
234
+
235
+ wsClients.add(ws);
236
+
237
+ ws.on('close', () => wsClients.delete(ws));
238
+ ws.on('error', () => wsClients.delete(ws));
239
+
240
+ // Handle incoming frames (for ping/pong and close detection)
241
+ ws.on('data', (data) => {
242
+ const frame = parseClientFrame(data);
243
+ if (!frame) return;
244
+
245
+ if (frame.opcode === 0x08) {
246
+ // Close frame
247
+ wsClients.delete(ws);
248
+ sendWsControlFrame(ws, 0x08, frame.payload);
249
+ try { ws.end(); } catch {}
250
+ } else if (frame.opcode === 0x09) {
251
+ // Ping → Pong
252
+ sendWsControlFrame(ws, 0x0a, frame.payload);
253
+ } else if (frame.opcode === 0x01) {
254
+ sendWsError(
255
+ ws,
256
+ 'Dashboard is read-only in v2.0. WebSocket commands and mutations are not supported.'
257
+ );
258
+ }
259
+ });
260
+ });
261
+
262
+ function start() {
263
+ return new Promise((resolve, reject) => {
264
+ server.listen(port, '127.0.0.1', () => {
265
+ watcher.start();
266
+ resolve({ port: server.address().port });
267
+ });
268
+ server.on('error', reject);
269
+ });
270
+ }
271
+
272
+ function stop() {
273
+ return new Promise((resolve) => {
274
+ watcher.stop();
275
+ for (const socket of wsClients) {
276
+ try { socket.destroy(); } catch {}
277
+ }
278
+ wsClients.clear();
279
+ server.close(() => resolve());
280
+ });
281
+ }
282
+
283
+ return { start, stop, server, watcher };
284
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * File watcher for the dashboard bridge server.
3
+ *
4
+ * Watches .agentxchain/ for changes and emits invalidation events
5
+ * mapped to API resource paths. Debounces rapid successive changes.
6
+ */
7
+
8
+ import { watch, existsSync } from 'fs';
9
+ import { basename, join } from 'path';
10
+ import { EventEmitter } from 'events';
11
+ import { WATCH_DIRECTORIES, resourceForRelativePath } from './state-reader.js';
12
+
13
+ const DEBOUNCE_MS = 100;
14
+
15
+ export class FileWatcher extends EventEmitter {
16
+ #watchers = new Map();
17
+ #debounceTimers = new Map();
18
+ #agentxchainDir;
19
+ #closed = false;
20
+
21
+ constructor(agentxchainDir) {
22
+ super();
23
+ this.#agentxchainDir = agentxchainDir;
24
+ }
25
+
26
+ #watchPath(relativeDir) {
27
+ if (this.#watchers.has(relativeDir)) {
28
+ return;
29
+ }
30
+
31
+ const watchPath = relativeDir
32
+ ? join(this.#agentxchainDir, relativeDir)
33
+ : this.#agentxchainDir;
34
+ if (!existsSync(watchPath)) {
35
+ return;
36
+ }
37
+
38
+ try {
39
+ const watcher = watch(watchPath, { recursive: false }, (eventType, filename) => {
40
+ if (!filename || this.#closed) return;
41
+ const base = basename(filename);
42
+ const relativePath = relativeDir ? `${relativeDir}/${base}` : base;
43
+ const resource = resourceForRelativePath(relativePath);
44
+
45
+ if (!resource) {
46
+ if (!relativeDir && base === 'multirepo') {
47
+ this.#watchPath('multirepo');
48
+ }
49
+ return;
50
+ }
51
+
52
+ if (this.#debounceTimers.has(resource)) {
53
+ clearTimeout(this.#debounceTimers.get(resource));
54
+ }
55
+ this.#debounceTimers.set(resource, setTimeout(() => {
56
+ this.#debounceTimers.delete(resource);
57
+ if (!this.#closed) {
58
+ this.emit('invalidate', { resource });
59
+ }
60
+ }, DEBOUNCE_MS));
61
+ });
62
+
63
+ watcher.on('error', (err) => {
64
+ if (!this.#closed) {
65
+ this.emit('error', err);
66
+ }
67
+ });
68
+
69
+ this.#watchers.set(relativeDir, watcher);
70
+ } catch (err) {
71
+ this.emit('error', err);
72
+ }
73
+ }
74
+
75
+ start() {
76
+ if (this.#watchers.size > 0) return;
77
+ for (const relativeDir of WATCH_DIRECTORIES) {
78
+ this.#watchPath(relativeDir);
79
+ }
80
+ }
81
+
82
+ stop() {
83
+ this.#closed = true;
84
+ for (const watcher of this.#watchers.values()) {
85
+ watcher.close();
86
+ }
87
+ this.#watchers.clear();
88
+ for (const timer of this.#debounceTimers.values()) {
89
+ clearTimeout(timer);
90
+ }
91
+ this.#debounceTimers.clear();
92
+ }
93
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * State reader — reads .agentxchain/ state files for the dashboard bridge.
3
+ *
4
+ * All reads are synchronous and return parsed data or null for missing files.
5
+ * The bridge server calls these on each API request (no caching — files are
6
+ * small and change infrequently relative to HTTP request rate).
7
+ */
8
+
9
+ import { readFileSync, existsSync } from 'fs';
10
+ import { join, normalize } from 'path';
11
+
12
+ const STATE_FILE = 'state.json';
13
+ const HISTORY_FILE = 'history.jsonl';
14
+ const LEDGER_FILE = 'decision-ledger.jsonl';
15
+ const HOOK_AUDIT_FILE = 'hook-audit.jsonl';
16
+ const HOOK_ANNOTATIONS_FILE = 'hook-annotations.jsonl';
17
+ const MULTIREPO_DIR = 'multirepo';
18
+ const BARRIERS_FILE = 'barriers.json';
19
+ const BARRIER_LEDGER_FILE = 'barrier-ledger.jsonl';
20
+
21
+ /**
22
+ * Map of API resource paths to their .agentxchain/ file names.
23
+ */
24
+ export const RESOURCE_MAP = {
25
+ '/api/state': STATE_FILE,
26
+ '/api/history': HISTORY_FILE,
27
+ '/api/ledger': LEDGER_FILE,
28
+ '/api/hooks/audit': HOOK_AUDIT_FILE,
29
+ '/api/hooks/annotations': HOOK_ANNOTATIONS_FILE,
30
+ '/api/coordinator/state': join(MULTIREPO_DIR, STATE_FILE),
31
+ '/api/coordinator/history': join(MULTIREPO_DIR, HISTORY_FILE),
32
+ '/api/coordinator/ledger': join(MULTIREPO_DIR, LEDGER_FILE),
33
+ '/api/coordinator/barriers': join(MULTIREPO_DIR, BARRIERS_FILE),
34
+ '/api/coordinator/barrier-ledger': join(MULTIREPO_DIR, BARRIER_LEDGER_FILE),
35
+ '/api/coordinator/hooks/audit': join(MULTIREPO_DIR, HOOK_AUDIT_FILE),
36
+ '/api/coordinator/hooks/annotations': join(MULTIREPO_DIR, HOOK_ANNOTATIONS_FILE),
37
+ };
38
+
39
+ /**
40
+ * Reverse map: relative file path under .agentxchain/ → API resource path.
41
+ */
42
+ export const FILE_TO_RESOURCE = Object.fromEntries(
43
+ Object.entries(RESOURCE_MAP).map(([resource, file]) => [normalizeRelativePath(file), resource])
44
+ );
45
+
46
+ export const WATCH_DIRECTORIES = [
47
+ '',
48
+ MULTIREPO_DIR,
49
+ ];
50
+
51
+ export function normalizeRelativePath(filePath) {
52
+ return normalize(filePath).replace(/\\/g, '/').replace(/^\.\/+/, '');
53
+ }
54
+
55
+ export function resourceForRelativePath(filePath) {
56
+ return FILE_TO_RESOURCE[normalizeRelativePath(filePath)] || null;
57
+ }
58
+
59
+ /**
60
+ * Read a JSON file. Returns parsed object or null if file doesn't exist.
61
+ * Throws on malformed JSON.
62
+ */
63
+ export function readJsonFile(agentxchainDir, filename) {
64
+ const filePath = join(agentxchainDir, filename);
65
+ if (!existsSync(filePath)) return null;
66
+ const content = readFileSync(filePath, 'utf8').trim();
67
+ if (!content) return null;
68
+ return JSON.parse(content);
69
+ }
70
+
71
+ /**
72
+ * Read a JSONL file. Returns array of parsed objects or null if file doesn't exist.
73
+ * Throws on malformed JSON in any line.
74
+ */
75
+ export function readJsonlFile(agentxchainDir, filename) {
76
+ const filePath = join(agentxchainDir, filename);
77
+ if (!existsSync(filePath)) return null;
78
+ const content = readFileSync(filePath, 'utf8').trim();
79
+ if (!content) return [];
80
+ return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
81
+ }
82
+
83
+ /**
84
+ * Read a resource by its API path. Returns { data, format } or null.
85
+ */
86
+ export function readResource(agentxchainDir, resourcePath) {
87
+ const filename = RESOURCE_MAP[resourcePath];
88
+ if (!filename) return null;
89
+
90
+ if (filename.endsWith('.jsonl')) {
91
+ const data = readJsonlFile(agentxchainDir, filename);
92
+ return data !== null ? { data, format: 'jsonl' } : null;
93
+ }
94
+ const data = readJsonFile(agentxchainDir, filename);
95
+ return data !== null ? { data, format: 'json' } : null;
96
+ }