great-cto 3.70.0 → 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.
Files changed (37) hide show
  1. package/README.md +9 -1
  2. package/board/.claude-plugin/plugin.json +93 -1
  3. package/board/packages/board/lib/routes.mjs +14 -1
  4. package/board/packages/board/lib/work.mjs +34 -3
  5. package/board/packages/board/public/assets/admin-shell.css +63 -0
  6. package/board/packages/board/public/assets/admin-shell.js +39 -5
  7. package/board/packages/board/public/assets/work-view.js +320 -37
  8. package/board/packages/board/public/index.html +43 -1
  9. package/board/scripts/codex-pipeline.mjs +7 -1
  10. package/board/scripts/lib/claude-exec.mjs +49 -6
  11. package/board/scripts/lib/codex-exec.mjs +42 -18
  12. package/board/scripts/lib/codex-host-state.mjs +18 -2
  13. package/board/scripts/lib/codex-pipeline.mjs +44 -7
  14. package/board/scripts/lib/host-graph-bindings.mjs +135 -0
  15. package/board/scripts/lib/host-graph-events.mjs +157 -0
  16. package/board/scripts/lib/host-graph-journal.mjs +210 -0
  17. package/board/scripts/lib/host-graph-observer.mjs +61 -0
  18. package/board/scripts/lib/host-graph-runtime.mjs +38 -0
  19. package/board/scripts/work-task.mjs +17 -4
  20. package/dist/adapt.js +10 -21
  21. package/dist/codex.js +4 -15
  22. package/dist/companion.js +6 -122
  23. package/dist/installer.js +4 -72
  24. package/dist/main.js +51 -118
  25. package/dist/release-activation.mjs +561 -0
  26. package/dist/release-archive.mjs +165 -0
  27. package/dist/release-trust.mjs +514 -0
  28. package/dist/release-update.mjs +303 -0
  29. package/dist/self-upgrade.js +10 -39
  30. package/dist/update-check.js +1 -0
  31. package/dist/upgrade.js +5 -120
  32. package/package.json +1 -1
  33. package/board/packages/board/fixtures/beads-read-owner.mjs +0 -3
  34. package/board/packages/board/fixtures/fake-bd-controlled.mjs +0 -55
  35. package/board/packages/board/fixtures/fake-bd-fail.sh +0 -9
  36. package/board/packages/board/fixtures/fake-bd-slow.sh +0 -12
  37. package/board/packages/board/fixtures/held-receipt-git.mjs +0 -21
@@ -0,0 +1,165 @@
1
+ /** Canonical regular-file USTAR; never delegate admitted extraction to a second parser. */
2
+ import { gzipSync, gunzipSync } from 'node:zlib';
3
+ export const RELEASE_ARCHIVE_LIMITS = Object.freeze({ compressed: 128 * 1024 * 1024, expanded: 512 * 1024 * 1024, entries: 10_000, file: 128 * 1024 * 1024 });
4
+ const privatePart = /^(?:\.git|\.beads|\.great_cto|\.codex|\.claude|node_modules|\.env(?:\..*)?|\.npmrc|\.yarnrc(?:\..*)?|\.pnpmfile\.cjs|AGENTS\.md|CLAUDE\.md)$/i;
5
+ const reserved = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
6
+ export function releaseArchivePath(value) {
7
+ if (typeof value !== 'string' || !value || value.length > 255 || !/^[\x21-\x7e]+$/.test(value) || /[\\:]/.test(value))
8
+ throw Error('invalid archive path');
9
+ const parts = value.split('/');
10
+ if (parts.some(p => !p || p === '.' || p === '..' || /[. ]$/.test(p)))
11
+ throw Error('invalid archive path segment');
12
+ if (parts.some(p => privatePart.test(p)))
13
+ throw Error('private archive path refused');
14
+ if (parts.some(p => reserved.test(p)))
15
+ throw Error('reserved archive name refused');
16
+ return value;
17
+ }
18
+ function memberNames(files, prefix) {
19
+ releaseArchivePath(prefix);
20
+ if (prefix.includes('/'))
21
+ throw Error('archive prefix must be one segment');
22
+ if (!Array.isArray(files) || files.length === 0 || files.length > RELEASE_ARCHIVE_LIMITS.entries)
23
+ throw Error('archive entry count exceeded');
24
+ const names = new Set(), paths = new Set();
25
+ for (const f of files) {
26
+ const name = releaseArchivePath(f.path), folded = name.toLowerCase();
27
+ if (names.has(folded))
28
+ throw Error('duplicate or case-colliding archive membership');
29
+ names.add(folded);
30
+ paths.add(name);
31
+ if (![0o644, 0o755].includes(f.mode))
32
+ throw Error('unsupported archive mode');
33
+ if (!Buffer.isBuffer(f.bytes) || f.bytes.length > RELEASE_ARCHIVE_LIMITS.file)
34
+ throw Error('invalid archive file bytes');
35
+ }
36
+ for (const name of paths) {
37
+ const parts = name.toLowerCase().split('/');
38
+ for (let i = 1; i < parts.length; i++)
39
+ if (names.has(parts.slice(0, i).join('/')))
40
+ throw Error('archive file-parent collision');
41
+ }
42
+ }
43
+ function putString(header, start, length, text) {
44
+ if (Buffer.byteLength(text, 'ascii') > length)
45
+ throw Error('archive path exceeds USTAR field');
46
+ header.write(text, start, length, 'ascii');
47
+ }
48
+ function octal(value, length) {
49
+ const encoded = value.toString(8);
50
+ if (encoded.length > length - 1)
51
+ throw Error('archive numeric field exceeded');
52
+ return encoded.padStart(length - 1, '0') + '\0';
53
+ }
54
+ function splitName(full) {
55
+ if (full.length <= 100)
56
+ return [full, ''];
57
+ for (let at = full.lastIndexOf('/'); at > 0; at = full.lastIndexOf('/', at - 1)) {
58
+ if (at <= 155 && full.length - at - 1 <= 100)
59
+ return [full.slice(at + 1), full.slice(0, at)];
60
+ }
61
+ throw Error('archive path exceeds USTAR fields');
62
+ }
63
+ export function encodeReleaseArchive(files, { prefix = 'package' } = {}) {
64
+ memberNames(files, prefix);
65
+ const parts = [], sorted = [...files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
66
+ let expanded = 1024;
67
+ for (const f of sorted) {
68
+ const [name, pre] = splitName(`${prefix}/${f.path}`), header = Buffer.alloc(512);
69
+ putString(header, 0, 100, name);
70
+ putString(header, 345, 155, pre);
71
+ putString(header, 100, 8, octal(f.mode, 8));
72
+ putString(header, 108, 8, octal(0, 8));
73
+ putString(header, 116, 8, octal(0, 8));
74
+ putString(header, 124, 12, octal(f.bytes.length, 12));
75
+ putString(header, 136, 12, octal(0, 12));
76
+ header.fill(32, 148, 156);
77
+ header[156] = 48;
78
+ putString(header, 257, 6, 'ustar\0');
79
+ putString(header, 263, 2, '00');
80
+ const sum = header.reduce((a, b) => a + b, 0);
81
+ putString(header, 148, 8, sum.toString(8).padStart(6, '0') + '\0 ');
82
+ const padding = (512 - f.bytes.length % 512) % 512;
83
+ expanded += 512 + f.bytes.length + padding;
84
+ if (expanded > RELEASE_ARCHIVE_LIMITS.expanded)
85
+ throw Error('expanded archive limit exceeded');
86
+ parts.push(header, f.bytes, Buffer.alloc(padding));
87
+ }
88
+ parts.push(Buffer.alloc(1024));
89
+ const compressed = gzipSync(Buffer.concat(parts), { level: 9 });
90
+ if (compressed.length > RELEASE_ARCHIVE_LIMITS.compressed)
91
+ throw Error('compressed archive limit exceeded');
92
+ return compressed;
93
+ }
94
+ function stringField(header, start, length) {
95
+ const field = header.subarray(start, start + length), zero = field.indexOf(0);
96
+ const end = zero === -1 ? field.length : zero;
97
+ if (zero !== -1 && field.subarray(zero).some(b => b !== 0))
98
+ throw Error('ambiguous archive string field');
99
+ const bytes = field.subarray(0, end);
100
+ if (bytes.some(b => b < 32 || b > 126))
101
+ throw Error('invalid archive string bytes');
102
+ return bytes.toString('ascii');
103
+ }
104
+ function numberField(header, start, length) {
105
+ const text = header.subarray(start, start + length).toString('ascii');
106
+ if (!/^[0-7]+(?:\0| )*$/.test(text))
107
+ throw Error('invalid archive numeric field');
108
+ const n = Number.parseInt(text.replace(/[\0 ]+$/, ''), 8);
109
+ if (!Number.isSafeInteger(n))
110
+ throw Error('archive numeric field exceeded');
111
+ return n;
112
+ }
113
+ export function inspectReleaseArchive(bytes, { prefix = 'package' } = {}) {
114
+ releaseArchivePath(prefix);
115
+ if (prefix.includes('/') || !Buffer.isBuffer(bytes) || !bytes.length || bytes.length > RELEASE_ARCHIVE_LIMITS.compressed)
116
+ throw Error('invalid compressed archive');
117
+ let raw;
118
+ try {
119
+ raw = gunzipSync(bytes, { maxOutputLength: RELEASE_ARCHIVE_LIMITS.expanded });
120
+ }
121
+ catch (error) {
122
+ throw Error(`invalid gzip archive: ${error.code || 'decompression refused'}`);
123
+ }
124
+ if (raw.length % 512 !== 0)
125
+ throw Error('truncated archive block');
126
+ const files = [];
127
+ let offset = 0, ended = false;
128
+ while (offset + 512 <= raw.length) {
129
+ const header = raw.subarray(offset, offset + 512);
130
+ if (header.every(b => b === 0)) {
131
+ if (raw.length - offset < 1024 || raw.subarray(offset).some(b => b !== 0))
132
+ throw Error('invalid archive termination padding');
133
+ ended = true;
134
+ break;
135
+ }
136
+ const expected = numberField(header, 148, 8);
137
+ const checksum = header.reduce((sum, b, at) => sum + (at >= 148 && at < 156 ? 32 : b), 0);
138
+ if (checksum !== expected)
139
+ throw Error('archive checksum mismatch');
140
+ if (header.subarray(257, 263).toString('ascii') !== 'ustar\0' || header.subarray(263, 265).toString('ascii') !== '00')
141
+ throw Error('unsupported archive header');
142
+ if (![0, 48].includes(header[156]))
143
+ throw Error('archive entry type must be regular');
144
+ if (header.subarray(157, 257).some(b => b !== 0))
145
+ throw Error('regular archive entry has a link target');
146
+ const name = stringField(header, 0, 100), pre = stringField(header, 345, 155), full = pre ? `${pre}/${name}` : name;
147
+ if (!full.startsWith(`${prefix}/`))
148
+ throw Error('archive path root prefix mismatch');
149
+ const filePath = releaseArchivePath(full.slice(prefix.length + 1));
150
+ const size = numberField(header, 124, 12), mode = numberField(header, 100, 8);
151
+ if (size > RELEASE_ARCHIVE_LIMITS.file || ![0o644, 0o755].includes(mode))
152
+ throw Error('archive size or mode refused');
153
+ const dataStart = offset + 512, next = dataStart + Math.ceil(size / 512) * 512;
154
+ if (next > raw.length || raw.subarray(dataStart + size, next).some(b => b !== 0))
155
+ throw Error('truncated archive or nonzero file padding');
156
+ files.push({ path: filePath, bytes: Buffer.from(raw.subarray(dataStart, dataStart + size)), mode });
157
+ if (files.length > RELEASE_ARCHIVE_LIMITS.entries)
158
+ throw Error('archive entry count exceeded');
159
+ offset = next;
160
+ }
161
+ if (!ended)
162
+ throw Error('archive termination missing');
163
+ memberNames(files, prefix);
164
+ return files;
165
+ }