great-cto 3.75.4 → 3.75.6

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/assets/statusline.mjs +7 -4
  2. package/board/.claude-plugin/plugin.json +1 -1
  3. package/board/agents/architect.md +19 -1
  4. package/board/agents/code-reviewer.md +50 -42
  5. package/board/agents/design-advisor.md +13 -2
  6. package/board/agents/devops.md +40 -1
  7. package/board/agents/qa-engineer.md +33 -0
  8. package/board/agents/security-officer.md +15 -7
  9. package/board/agents/senior-dev.md +16 -0
  10. package/board/packages/board/lib/bd-read-worker.mjs +2 -1
  11. package/board/packages/board/lib/receipt-read-worker.mjs +2 -1
  12. package/board/scripts/hooks/auto-attach-reviewers.mjs +2 -2
  13. package/board/scripts/lib/affected-tests.mjs +2 -2
  14. package/board/scripts/lib/agent-events.mjs +2 -1
  15. package/board/scripts/lib/agent-usage.mjs +2 -1
  16. package/board/scripts/lib/approval-level.mjs +3 -1
  17. package/board/scripts/lib/board-watch.mjs +2 -2
  18. package/board/scripts/lib/change-tier.mjs +2 -1
  19. package/board/scripts/lib/cost-meter.mjs +2 -2
  20. package/board/scripts/lib/cross-model-review.mjs +2 -2
  21. package/board/scripts/lib/dag-metric.mjs +2 -1
  22. package/board/scripts/lib/gate-plan.mjs +2 -2
  23. package/board/scripts/lib/gate-tier.mjs +2 -1
  24. package/board/scripts/lib/is-main.mjs +24 -0
  25. package/board/scripts/lib/judge-model.mjs +2 -1
  26. package/board/scripts/lib/pipeline-health.mjs +2 -1
  27. package/board/scripts/lib/receipt.mjs +138 -19
  28. package/board/scripts/lib/second-opinion.mjs +2 -1
  29. package/board/scripts/lib/session-usage.mjs +2 -1
  30. package/board/scripts/lib/stack-capabilities.mjs +3 -1
  31. package/board/scripts/lib/stand-down.mjs +2 -1
  32. package/board/scripts/lib/system-map.mjs +2 -1
  33. package/board/scripts/lib/verdict-record.mjs +15 -3
  34. package/board/scripts/lib/verdict-seal.mjs +95 -0
  35. package/board/scripts/skill-lint.mjs +2 -1
  36. package/package.json +1 -1
  37. package/postinstall.mjs +7 -2
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Seals on verdict lines and receipt acceptances — tamper-evidence, not a lock.
3
+ *
4
+ * great_cto-dv3b. A deploy trusts the newest approving verdict in
5
+ * .great_cto/verdicts/<agent>.log and an operator's acceptance in
6
+ * .great_cto/.receipt-accept. Both were plain JSON: a line appended with `echo`,
7
+ * or a field edited by hand, read exactly like one log-verdict.sh wrote.
8
+ *
9
+ * Each now carries an HMAC-SHA256 over the fields that matter, keyed by
10
+ * ~/.great_cto/keys/verdict-seal.key (0600, created on first use). The writers
11
+ * — log-verdict.sh via verdict-record.mjs, and `receipt.mjs --accept` — seal what
12
+ * they write; `receipt.mjs --verify --require` checks before it trusts.
13
+ *
14
+ * What it does NOT do, said rather than implied (ADR-046). Every agent runs as the
15
+ * same OS user as the operator, so no file on this machine is out of an agent's
16
+ * reach: code that imports this module can seal anything. What the seal changes:
17
+ * a hand-written or hand-edited line no longer passes, and getting a valid one
18
+ * means running the writer or reading the key — and a direct read of a `*.key`
19
+ * file is refused by the secret-read guard. A lock would need a human-presence
20
+ * primitive (a hardware key, an OS confirmation) this machine does not have.
21
+ */
22
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
23
+ import fs from 'node:fs';
24
+ import os from 'node:os';
25
+ import path from 'node:path';
26
+
27
+ const VERSION = 'v1';
28
+
29
+ export function sealKeyPath(env = process.env) {
30
+ if (env.GREAT_CTO_SEAL_KEY) return env.GREAT_CTO_SEAL_KEY;
31
+ let home;
32
+ try { home = os.userInfo().homedir; } catch { home = os.homedir(); }
33
+ return path.join(home, '.great_cto', 'keys', 'verdict-seal.key');
34
+ }
35
+
36
+ /** The key, or null. `create` makes one (0600, in a 0700 directory) when there is none. */
37
+ export function loadSealKey({ create = false, env = process.env } = {}) {
38
+ const p = sealKeyPath(env);
39
+ try {
40
+ const hex = fs.readFileSync(p, 'utf8').trim();
41
+ if (/^[0-9a-f]{64}$/.test(hex)) return Buffer.from(hex, 'hex');
42
+ // A damaged key is not "no key": say so, every time, rather than unseal quietly.
43
+ process.stderr.write(`verdict-seal: ${p} is not a valid key — verdicts go unsealed until it is removed and recreated\n`);
44
+ return null;
45
+ } catch (e) {
46
+ if (e.code !== 'ENOENT' || !create) return null;
47
+ }
48
+ fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
49
+ // Written whole to a temporary name and linked into place: two first-time writers,
50
+ // or a crash mid-write, must never leave an empty key that silently unseals
51
+ // every later verdict.
52
+ const tmp = `${p}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
53
+ fs.writeFileSync(tmp, randomBytes(32).toString('hex') + '\n', { flag: 'wx', mode: 0o600 });
54
+ try { fs.linkSync(tmp, p); }
55
+ catch (e) { if (e.code !== 'EEXIST') throw e; } // another writer made it first: use theirs
56
+ finally { fs.rmSync(tmp, { force: true }); }
57
+ return loadSealKey({ create: false, env });
58
+ }
59
+
60
+ /** Every field but the seal, keys sorted at every depth — so editing any of them
61
+ * (meta.need routes the pipeline, cost_usd feeds the spend view) breaks it. */
62
+ function canonical(v) {
63
+ if (Array.isArray(v)) return v.map(canonical);
64
+ if (v && typeof v === 'object') return Object.fromEntries(Object.keys(v).sort().filter((k) => k !== 'seal').map((k) => [k, canonical(v[k])]));
65
+ return v;
66
+ }
67
+ const VERDICT_FIELDS = (r) => ['verdict', canonical(r)];
68
+ const ACCEPT_FIELDS = (a) => ['accept', canonical(a)];
69
+
70
+ function mac(key, fields) {
71
+ return `${VERSION}:${createHmac('sha256', key).update(JSON.stringify(fields)).digest('hex')}`;
72
+ }
73
+
74
+ function check(key, fields, seal) {
75
+ if (typeof seal !== 'string' || !seal) return 'unsealed';
76
+ if (!key) return 'unknown';
77
+ const want = Buffer.from(mac(key, fields));
78
+ const got = Buffer.from(seal);
79
+ return want.length === got.length && timingSafeEqual(want, got) ? 'valid' : 'invalid';
80
+ }
81
+
82
+ export function sealVerdict(rec, { key = loadSealKey({ create: true }) } = {}) {
83
+ return key ? mac(key, VERDICT_FIELDS(rec)) : null;
84
+ }
85
+ /** 'valid' | 'invalid' (written or edited outside the writer) | 'unsealed' (older, or never sealed) | 'unknown' (no key here). */
86
+ export function checkVerdictSeal(rec, { key = loadSealKey() } = {}) {
87
+ return check(key, VERDICT_FIELDS(rec || {}), rec?.seal);
88
+ }
89
+
90
+ export function sealAcceptance(acc, { key = loadSealKey({ create: true }) } = {}) {
91
+ return key ? mac(key, ACCEPT_FIELDS(acc)) : null;
92
+ }
93
+ export function checkAcceptanceSeal(acc, { key = loadSealKey() } = {}) {
94
+ return check(key, ACCEPT_FIELDS(acc || {}), acc?.seal);
95
+ }
@@ -22,6 +22,7 @@
22
22
  import fs from 'node:fs';
23
23
  import path from 'node:path';
24
24
  import { fileURLToPath } from 'node:url';
25
+ import { isMainModule } from './lib/is-main.mjs';
25
26
 
26
27
  export const SIZE_BUDGET = 20 * 1024; // bytes: 20 KiB (20,480), not 20,000
27
28
  export const DESC_MIN = 40;
@@ -162,7 +163,7 @@ export function lintSkills({ repoRoot }) {
162
163
  return { skills: names.length, errors, warnings };
163
164
  }
164
165
 
165
- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
166
+ const isMain = isMainModule(import.meta.url);
166
167
  if (isMain) {
167
168
  const a = process.argv.slice(2);
168
169
  const i = a.indexOf('--root');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.75.4",
3
+ "version": "3.75.6",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",
package/postinstall.mjs CHANGED
@@ -34,7 +34,7 @@
34
34
  * evidence of staleness.
35
35
  */
36
36
 
37
- import { readFileSync, readdirSync, existsSync } from 'node:fs';
37
+ import { readFileSync, readdirSync, existsSync, realpathSync } from 'node:fs';
38
38
  import { spawn } from 'node:child_process';
39
39
  import { join, dirname } from 'node:path';
40
40
  import { homedir } from 'node:os';
@@ -174,6 +174,11 @@ export function ensureBoard({ env = process.env, spawnFn = spawn,
174
174
  }
175
175
 
176
176
  // Only when run as the hook, so the pure parts above stay importable by tests.
177
- if (process.argv[1] && process.argv[1].endsWith('postinstall.mjs')) {
177
+ // Real paths on both sides, the rule of scripts/lib/is-main.mjs — inlined
178
+ // because the npm package does not ship scripts/.
179
+ const isMain = () => {
180
+ try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); } catch { return false; }
181
+ };
182
+ if (process.argv[1] && isMain()) {
178
183
  try { main(); } catch { /* an installer must never fail the install */ }
179
184
  }