great-cto 2.31.0 → 2.33.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/dist/bootstrap.js CHANGED
@@ -2,7 +2,6 @@
2
2
  // Safe: will NOT overwrite an existing PROJECT.md.
3
3
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { homedir } from "node:os";
6
5
  import { dim, success, warn } from "./ui.js";
7
6
  import { suggestJurisdictions } from "./jurisdictions.js";
8
7
  import { compileFlow, renderFlowMd } from "./flow.js";
@@ -146,55 +145,6 @@ when you actually start work:
146
145
  `;
147
146
  writeFileSync(projectMd, content, "utf-8");
148
147
  success(`created .great_cto/PROJECT.md ${dim(`(archetype: ${archetype})`)}`);
149
- // Write ~/.great_cto/guardrails.yml example if it doesn't exist yet.
150
- // This is the user-level agentshield config — applies across all projects.
151
- const globalGreatCtoDir = join(homedir(), ".great_cto");
152
- const guardrailsPath = join(globalGreatCtoDir, "guardrails.yml");
153
- if (!existsSync(guardrailsPath)) {
154
- mkdirSync(globalGreatCtoDir, { recursive: true });
155
- const guardrailsContent = `# ~/.great_cto/guardrails.yml
156
- # User-defined agentshield rules. Loaded and merged with built-in rules on every scan.
157
- # Rules use the same YAML format as agentshield-rules/*.yaml.
158
- #
159
- # action field (user rules only):
160
- # block — scan fails (treated as critical finding; blocks gate:ship)
161
- # audit — finding reported but scan continues (warning only)
162
- # redact — same as audit; marks pattern for future content redaction
163
- #
164
- # Example rule — customize with your org's patterns:
165
- #
166
- # - id: UG-001
167
- # scanner: secrets-in-prompts
168
- # title: Internal API token pattern
169
- # severity: critical
170
- # description: |
171
- # Detects hardcoded internal API tokens matching the org pattern mytoken_*.
172
- # remediation: |
173
- # Move to environment variable. Reference via process.env.MY_TOKEN.
174
- # patterns:
175
- # - 'mytoken_[a-z0-9]{32}'
176
- # action: block
177
- #
178
- # - id: UG-002
179
- # scanner: prompt-injection
180
- # title: Audit use of customer data in prompts
181
- # severity: high
182
- # description: |
183
- # Flags when customer PII fields (email, phone, address) are embedded in prompts.
184
- # remediation: |
185
- # Replace PII with anonymized tokens before embedding in prompts.
186
- # patterns:
187
- # - 'customer\.(email|phone|address|ssn)'
188
- # action: audit
189
- # file_globs:
190
- # - "**/*.ts"
191
- # - "**/*.py"
192
- #
193
- # Add your own rules below:
194
- `;
195
- writeFileSync(guardrailsPath, guardrailsContent, "utf-8");
196
- success(`created ~/.great_cto/guardrails.yml ${dim("(user-level agentshield rules — edit to add org-specific patterns)")}`);
197
- }
198
148
  // Write FLOW.md — compiled delivery flow for agents and user
199
149
  const confidence = detectionMeta?.confidence ?? "medium";
200
150
  const size = (detection.projectSize ?? "medium");
package/dist/ci.js CHANGED
@@ -1,95 +1,22 @@
1
1
  // great-cto ci — single-command CI gate.
2
2
  //
3
- // Runs scan + budget-check + archetype-validate with output formats matching
4
- // CI conventions. Designed to be the only great_cto invocation in a CI step.
3
+ // Runs archetype-validate + budget-check. Designed to be the only great_cto
4
+ // invocation in a CI step.
5
5
  //
6
6
  // Outputs:
7
7
  // - human-readable to stderr (always)
8
- // - GitHub Actions annotations (auto when $GITHUB_ACTIONS is set, or --annotations)
9
- // - SARIF 2.1.0 JSON (--sarif <path>) — uploadable to GitHub Security tab
10
- // - JUnit XML (--junit <path>) — for test reporters / pipeline UIs
11
8
  //
12
9
  // Exit codes:
13
10
  // 0 = clean, all gates pass
14
- // 1 = findings at/above --fail-on threshold (CI should fail)
15
- // 2 = scan/setup error (not a finding — infrastructure problem)
11
+ // 1 = archetype drift (CI should fail)
12
+ // 2 = setup error (not a finding — infrastructure problem)
16
13
  //
17
14
  // Example workflow:
18
15
  // - run: npx great-cto@latest ci ./
19
16
  // env:
20
17
  // GREAT_CTO_NO_TELEMETRY: "1"
21
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { existsSync, readFileSync } from "node:fs";
22
19
  import { resolve } from "node:path";
23
- const SEVERITY_ORDER = {
24
- info: 0,
25
- low: 1,
26
- medium: 2,
27
- high: 3,
28
- critical: 4,
29
- };
30
- function severityAtLeast(sev, threshold) {
31
- return (SEVERITY_ORDER[sev] ?? 0) >= (SEVERITY_ORDER[threshold] ?? 0);
32
- }
33
- /**
34
- * Emit GitHub Actions annotation lines. These appear inline on PR diffs.
35
- * Format: ::error file=path,line=N,col=M,title=T::message
36
- * ::warning file=...
37
- * ::notice file=...
38
- */
39
- function emitGitHubAnnotation(finding) {
40
- const sev = finding.rule.severity;
41
- const level = sev === "critical" || sev === "high" ? "error"
42
- : sev === "medium" ? "warning" : "notice";
43
- const file = finding.location.file;
44
- const line = finding.location.line;
45
- const title = `${finding.rule.id}: ${finding.rule.title}`;
46
- const message = finding.rule.owasp
47
- ? `${finding.rule.title} (${finding.rule.owasp})`
48
- : finding.rule.title;
49
- // Escape GHA-special characters
50
- const escape = (s) => s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
51
- console.log(`::${level} file=${escape(file)},line=${line},title=${escape(title)}::${escape(message)}`);
52
- }
53
- /**
54
- * Emit JUnit XML report. One <testcase> per scanned file, fails recorded as
55
- * <failure> elements. Format compatible with most CI test reporters.
56
- */
57
- function buildJunitXml(report) {
58
- const findingsByFile = new Map();
59
- for (const f of report.findings) {
60
- const arr = findingsByFile.get(f.location.file) || [];
61
- arr.push(f);
62
- findingsByFile.set(f.location.file, arr);
63
- }
64
- const totalTests = Math.max(report.filesScanned, findingsByFile.size);
65
- const failures = report.findings.length;
66
- const escape = (s) => String(s)
67
- .replace(/&/g, "&amp;")
68
- .replace(/</g, "&lt;")
69
- .replace(/>/g, "&gt;")
70
- .replace(/"/g, "&quot;")
71
- .replace(/'/g, "&apos;");
72
- const cases = Array.from(findingsByFile.entries())
73
- .map(([file, findings]) => {
74
- const failureBody = findings
75
- .map(f => ` <failure type="${escape(f.rule.severity)}" message="${escape(f.rule.id + ': ' + f.rule.title)}">
76
- ${escape(f.location.snippet || '')}
77
- ${escape(f.rule.owasp || '')}
78
- </failure>`)
79
- .join("\n");
80
- return ` <testcase classname="agentshield" name="${escape(file)}" time="0">
81
- ${failureBody}
82
- </testcase>`;
83
- })
84
- .join("\n");
85
- return `<?xml version="1.0" encoding="UTF-8"?>
86
- <testsuites>
87
- <testsuite name="great-cto ci" tests="${totalTests}" failures="${failures}" errors="0" time="${(report.durationMs / 1000).toFixed(3)}">
88
- ${cases}
89
- </testsuite>
90
- </testsuites>
91
- `;
92
- }
93
20
  /**
94
21
  * Quick archetype-detection sanity check. Fails CI if the archetype changed
95
22
  * from what's pinned in .great_cto/PROJECT.md (signals undeclared
@@ -147,66 +74,26 @@ function budgetCheck(cwd, quiet) {
147
74
  }
148
75
  export async function runCi(args) {
149
76
  const startTs = Date.now();
150
- const inGitHubActions = process.env.GITHUB_ACTIONS === "true";
151
- const wantAnnotations = args.annotations || inGitHubActions;
152
77
  if (!args.quiet) {
153
- console.error(`\ngreat-cto ci — gate threshold: ${args.failOn}, scan: ${args.severity}+`);
78
+ console.error(`\ngreat-cto ci — archetype + budget gate`);
154
79
  console.error(` path: ${resolve(args.path)}`);
155
- if (inGitHubActions)
156
- console.error(` env: GitHub Actions detected — emitting annotations`);
157
80
  console.error("");
158
81
  }
159
- // 1. Scan
160
- let scan;
161
- let toSarif;
162
- try {
163
- ({ scan } = await import("./agentshield/scanner.js"));
164
- ({ toSarif } = await import("./agentshield/sarif.js"));
165
- }
166
- catch (e) {
167
- console.error(`ci: failed to load scanner: ${e.message}`);
168
- return 2;
169
- }
170
- const report = scan(resolve(args.path), {
171
- minSeverity: args.severity,
172
- });
173
- // 2. Archetype check
82
+ // 1. Archetype check
174
83
  let archResult = { ok: true, msg: "skipped" };
175
84
  if (!args.noArchetype) {
176
85
  archResult = await archetypeCheck(args.path, args.quiet);
177
86
  }
178
- // 3. Budget check (warn-only)
87
+ // 2. Budget check (warn-only)
179
88
  let budgetResult = { ok: true, msg: "skipped" };
180
89
  if (!args.noBudget) {
181
90
  budgetResult = budgetCheck(args.path, args.quiet);
182
91
  }
183
- // 4. Emit annotations
184
- if (wantAnnotations) {
185
- for (const f of report.findings) {
186
- if (severityAtLeast(f.rule.severity, args.failOn)) {
187
- emitGitHubAnnotation(f);
188
- }
189
- }
190
- }
191
- // 5. Emit SARIF
192
- if (args.sarifPath) {
193
- writeFileSync(args.sarifPath, JSON.stringify(toSarif(report), null, 2));
194
- if (!args.quiet)
195
- console.error(` ✓ SARIF → ${args.sarifPath}`);
196
- }
197
- // 6. Emit JUnit XML
198
- if (args.junitPath) {
199
- writeFileSync(args.junitPath, buildJunitXml(report));
200
- if (!args.quiet)
201
- console.error(` ✓ JUnit XML → ${args.junitPath}`);
202
- }
203
- // 7. Summary
204
- const blockingFindings = report.findings.filter((f) => severityAtLeast(f.rule.severity, args.failOn));
205
- const passed = blockingFindings.length === 0 && archResult.ok;
92
+ // 3. Summary
93
+ const passed = archResult.ok;
206
94
  if (!args.quiet) {
207
95
  const dur = ((Date.now() - startTs) / 1000).toFixed(1);
208
96
  console.error("");
209
- console.error(` scan: ${report.findings.length} finding(s) (${blockingFindings.length} at/above ${args.failOn})`);
210
97
  console.error(` archetype: ${archResult.ok ? "✓" : "✗"} ${archResult.msg}`);
211
98
  console.error(` budget: ${budgetResult.msg}`);
212
99
  console.error(` duration: ${dur}s`);
@@ -216,15 +103,6 @@ export async function runCi(args) {
216
103
  }
217
104
  else {
218
105
  console.error("\x1b[31m✗ great-cto ci: failed\x1b[0m");
219
- if (blockingFindings.length) {
220
- console.error(` ${blockingFindings.length} finding(s) at/above ${args.failOn}:`);
221
- for (const f of blockingFindings.slice(0, 10)) {
222
- console.error(` [${f.rule.severity}] ${f.rule.id} — ${f.location.file}:${f.location.line}`);
223
- }
224
- if (blockingFindings.length > 10) {
225
- console.error(` ... +${blockingFindings.length - 10} more`);
226
- }
227
- }
228
106
  if (!archResult.ok)
229
107
  console.error(` ${archResult.msg}`);
230
108
  }
@@ -236,10 +114,6 @@ export async function runCi(args) {
236
114
  */
237
115
  export function parseCiArgs(rawArgv) {
238
116
  const flag = (n) => rawArgv.includes(`--${n}`);
239
- const value = (n, def) => {
240
- const i = rawArgv.indexOf(`--${n}`);
241
- return i >= 0 && i < rawArgv.length - 1 ? rawArgv[i + 1] : def;
242
- };
243
117
  const ciIdx = rawArgv.indexOf("ci");
244
118
  let path = ".";
245
119
  for (let i = ciIdx + 1; i < rawArgv.length; i++) {
@@ -250,11 +124,6 @@ export function parseCiArgs(rawArgv) {
250
124
  }
251
125
  return {
252
126
  path,
253
- severity: value("severity", "high"),
254
- failOn: value("fail-on", "critical"),
255
- sarifPath: value("sarif") ?? null,
256
- junitPath: value("junit") ?? null,
257
- annotations: flag("annotations"),
258
127
  noBudget: flag("no-budget"),
259
128
  noArchetype: flag("no-archetype"),
260
129
  quiet: flag("quiet"),
package/dist/companion.js CHANGED
@@ -42,7 +42,11 @@ function isAlreadyInstalled(name) {
42
42
  return null;
43
43
  try {
44
44
  const versions = readdirSync(base).filter((v) => /\S/.test(v));
45
- return versions.length > 0 ? versions[0] : null;
45
+ if (versions.length === 0)
46
+ return null;
47
+ // Return the highest version, not filesystem order — matches
48
+ // upgrade.ts/installer.ts which both sort before picking.
49
+ return versions.sort(semverDescending)[0];
46
50
  }
47
51
  catch {
48
52
  return null;
package/dist/detect.js CHANGED
@@ -1118,7 +1118,7 @@ function mineReadmeKeywords(dir) {
1118
1118
  if (terms.some((t) => text.includes(t)))
1119
1119
  kws.add(bucket);
1120
1120
  }
1121
- // Wave 1-3 pack-trigger raw terms — emitted verbatim so packs.ts
1121
+ // Wave 1-4 pack-trigger raw terms — emitted verbatim so packs.ts
1122
1122
  // can substring-match them. Keep in sync with packs.ts SIGNALS.keywords.
1123
1123
  // Single tokens + multi-word phrases supported.
1124
1124
  const packTerms = [
@@ -1163,6 +1163,19 @@ function mineReadmeKeywords(dir) {
1163
1163
  "glp", "gmp", "gxp", "preclinical", "lims", "eln", "annex 11", "alcoa",
1164
1164
  "lab automation", "robotic biology", "liquid handler", "hamilton", "tecan",
1165
1165
  "beckman", "opentrons", "plate reader", "sequencer", "hplc", "mass spec", "sila",
1166
+ // digital-health-pack (Wave 4) — keep in sync with packs.ts SIGNALS.keywords
1167
+ "wearable", "apple watch", "apple health", "healthkit", "health connect",
1168
+ "garmin", "samsung health", "google fit", "fitbit", "heart rate", "hrv",
1169
+ "heart rate variability", "spo2", "sleep tracking", "sleep stages",
1170
+ "biometric sensor", "stress score", "activity tracking", "ecg wearable",
1171
+ "mental health", "mental wellness", "wellbeing", "mindfulness ai",
1172
+ "stress detection", "burnout detection", "mood tracking", "anxiety ai",
1173
+ "depression ai", "phq-9", "gad-7", "digital therapeutics", "dtx",
1174
+ "cbt app", "dbt app", "therapy ai", "personalised training",
1175
+ "personalized training", "fitness ai", "nutrition ai",
1176
+ "supplement recommendation", "supplement ai", "diet ai", "meal plan ai",
1177
+ "macro ai", "physician review", "physician hitl", "doctor in the loop",
1178
+ "clinical review workflow", "remote patient monitoring", "rpm", "teleconsultation",
1166
1179
  ];
1167
1180
  for (const term of packTerms) {
1168
1181
  if (text.includes(term))
package/dist/main.js CHANGED
@@ -82,10 +82,6 @@ function parseArgs(argv) {
82
82
  args.command = "board";
83
83
  else if (a === "register")
84
84
  args.command = "register";
85
- else if (a === "scan")
86
- args.command = "scan";
87
- else if (a === "list-rules")
88
- args.command = "list-rules";
89
85
  else if (a === "ci")
90
86
  args.command = "ci";
91
87
  else if (a === "mcp")
@@ -140,136 +136,6 @@ function parseArgs(argv) {
140
136
  args.positional = rest;
141
137
  return args;
142
138
  }
143
- /**
144
- * `great-cto scan [path]` — AI-specific security scanner (formerly @great-cto/agentshield).
145
- *
146
- * Detects OWASP LLM Top 10 patterns: prompt injection vectors, secrets in
147
- * prompts, SSRF in tool definitions, RAG poisoning, cost-runaway loops.
148
- *
149
- * Flags (parsed from raw argv since they're scan-specific):
150
- * --severity <lvl> info|low|medium|high|critical (default: info)
151
- * --scanner <name> prompt-injection | secrets-in-prompts | ssrf-in-tools |
152
- * rag-poisoning | cost-runaway (repeatable)
153
- * --sarif <file> emit SARIF 2.1.0 to file
154
- * --json emit JSON to stdout
155
- * --quiet suppress human-readable output
156
- * --max <n> stop after N findings
157
- * --exclude <regex> add path exclude (repeatable)
158
- *
159
- * Exit codes:
160
- * 0 = no findings (or all below severity threshold)
161
- * 1 = findings at/above threshold (CI-friendly)
162
- * 2 = scan failed
163
- */
164
- async function runScan(args, rawArgv) {
165
- const { writeFileSync } = await import("node:fs");
166
- const { resolve: resolvePath } = await import("node:path");
167
- // Lazy import compiled scanner — keeps cold start fast for `init` flow.
168
- let scan;
169
- let toSarif;
170
- try {
171
- ({ scan } = await import("./agentshield/scanner.js"));
172
- ({ toSarif } = await import("./agentshield/sarif.js"));
173
- }
174
- catch (e) {
175
- error(`scan: failed to load scanner: ${e.message}`);
176
- return 2;
177
- }
178
- // Parse scan-specific flags from raw argv
179
- const flag = (n) => rawArgv.includes(`--${n}`);
180
- const value = (n, def) => {
181
- const i = rawArgv.indexOf(`--${n}`);
182
- return i >= 0 && i < rawArgv.length - 1 ? rawArgv[i + 1] : def;
183
- };
184
- const scanners = rawArgv
185
- .map((a, i) => (a === "--scanner" ? rawArgv[i + 1] : null))
186
- .filter(Boolean);
187
- const exclude = rawArgv
188
- .map((a, i) => (a === "--exclude" ? rawArgv[i + 1] : null))
189
- .filter(Boolean);
190
- // Path: first non-flag arg after `scan`, default cwd
191
- const scanIdx = rawArgv.indexOf("scan");
192
- let root = ".";
193
- for (let i = scanIdx + 1; i < rawArgv.length; i++) {
194
- if (rawArgv[i] && !rawArgv[i].startsWith("--")) {
195
- root = rawArgv[i];
196
- break;
197
- }
198
- }
199
- const opts = {
200
- scanners: scanners.length > 0 ? scanners : undefined,
201
- minSeverity: value("severity", "info"),
202
- exclude: exclude.length > 0 ? exclude : undefined,
203
- maxFindings: value("max") ? parseInt(value("max"), 10) : undefined,
204
- };
205
- const sarifPath = value("sarif");
206
- const wantsJson = flag("json");
207
- const quiet = flag("quiet");
208
- const report = scan(resolvePath(root), opts);
209
- if (sarifPath) {
210
- writeFileSync(sarifPath, JSON.stringify(toSarif(report), null, 2));
211
- if (!quiet)
212
- console.error(`✓ SARIF written → ${sarifPath}`);
213
- }
214
- if (wantsJson) {
215
- console.log(JSON.stringify(report, null, 2));
216
- }
217
- else if (!quiet) {
218
- const COLORS = {
219
- critical: "\x1b[1;31m", high: "\x1b[31m", medium: "\x1b[33m",
220
- low: "\x1b[36m", info: "\x1b[2m", reset: "\x1b[0m",
221
- };
222
- const useColor = process.stdout.isTTY;
223
- const c = (sev, s) => (useColor ? `${COLORS[sev] || ""}${s}${COLORS.reset}` : s);
224
- console.error(`\ngreat-cto scan ${getCliVersion()} — scanned ${report.filesScanned} file(s) in ${report.durationMs}ms\n`);
225
- if (report.errors.length > 0) {
226
- console.error(`\x1b[33m⚠ ${report.errors.length} error(s):\x1b[0m`);
227
- for (const e of report.errors)
228
- console.error(` ${e}`);
229
- console.error("");
230
- }
231
- if (report.findings.length === 0) {
232
- console.error("\x1b[32m✓ No findings.\x1b[0m\n");
233
- }
234
- else {
235
- for (const f of report.findings) {
236
- const tag = c(f.rule.severity, `[${f.rule.severity.toUpperCase()}]`);
237
- console.error(`${tag} ${f.rule.id} ${f.location.file}:${f.location.line}`);
238
- console.error(` ${f.rule.title}`);
239
- console.error(` ${c("info", f.location.snippet)}`);
240
- if (f.rule.owasp)
241
- console.error(` ${c("info", f.rule.owasp)}`);
242
- console.error("");
243
- }
244
- const counts = {};
245
- for (const f of report.findings)
246
- counts[f.rule.severity] = (counts[f.rule.severity] || 0) + 1;
247
- const order = ["critical", "high", "medium", "low", "info"];
248
- const parts = order.filter((s) => counts[s]).map((s) => c(s, `${counts[s]} ${s}`));
249
- console.error(`\x1b[1m${report.findings.length} finding(s)\x1b[0m — ${parts.join(", ")}\n`);
250
- }
251
- }
252
- return report.findings.length > 0 ? 1 : 0;
253
- }
254
- /**
255
- * `great-cto list-rules` — print the rule catalog.
256
- */
257
- async function runListRules() {
258
- let loadRules;
259
- try {
260
- ({ loadRules } = await import("./agentshield/rules-loader.js"));
261
- }
262
- catch (e) {
263
- error(`list-rules: failed: ${e.message}`);
264
- return 2;
265
- }
266
- const rules = loadRules();
267
- for (const r of rules) {
268
- console.log(`${r.id.padEnd(8)} ${r.severity.padEnd(8)} ${r.scanner.padEnd(20)} ${r.title}`);
269
- }
270
- console.log(`\n${rules.length} rule(s) loaded.`);
271
- return 0;
272
- }
273
139
  async function runRegister(args) {
274
140
  const { join } = await import("node:path");
275
141
  const { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } = await import("node:fs");
@@ -464,9 +330,7 @@ ${bold("Usage:")}
464
330
  npx great-cto [init] [options] Detect + bootstrap
465
331
  npx great-cto board [--port 3141] [--no-open]
466
332
  npx great-cto register [--dir PATH]
467
- npx great-cto scan [path] [--severity LVL] [--scanner NAME] [--sarif FILE]
468
- npx great-cto list-rules
469
- npx great-cto ci [path] [--fail-on LVL] [--sarif F] [--junit F]
333
+ npx great-cto ci [path] [--no-archetype] [--no-budget]
470
334
  npx great-cto mcp [--sse --port N]
471
335
  npx great-cto adapt [--dry-run]
472
336
  npx great-cto serve [--port 3142]
@@ -490,27 +354,18 @@ ${bold("Upgrade:")}
490
354
  great-cto upgrade beads Upgrade beads only
491
355
  ${dim("(Safe to run any time — idempotent if already on latest)")}
492
356
 
493
- ${bold("Scan (AI-security):")}
494
- great-cto scan AI-specific scan of cwd (OWASP LLM Top 10)
495
- great-cto scan ./src --severity high Filter by minimum severity
496
- great-cto scan --scanner ssrf-in-tools Run only one scanner
497
- great-cto scan --sarif out.sarif Emit SARIF for GitHub Code Scanning
498
- great-cto scan --json JSON output for CI pipelines
499
- great-cto list-rules Print rule catalog
500
- ${dim("(exits 1 if findings ≥ severity threshold; CI-friendly)")}
501
-
502
357
  ${bold("CI gate:")}
503
- great-cto ci Single-command CI gate (scan + archetype check)
504
- great-cto ci --fail-on critical Exit 1 only on critical findings (default)
505
- great-cto ci --sarif out.sarif Emit SARIF (uploadable to GitHub Security)
506
- great-cto ci --junit out.xml Emit JUnit XML for test reporters
507
- ${dim("(auto-detects \$GITHUB_ACTIONS → emits ::error:: annotations)")}
358
+ great-cto ci Single-command CI gate (archetype + budget check)
359
+ great-cto ci --no-archetype Skip the archetype-drift check
360
+ great-cto ci --no-budget Skip the monthly-budget sanity check
361
+ ${dim("(exits 1 on archetype drift; budget is warn-only)")}
508
362
 
509
363
  ${bold("MCP server (cross-platform):")}
510
364
  great-cto mcp Stdio MCP server — works in Claude Code /
511
365
  Claude Desktop / any MCP host
512
366
  great-cto mcp --sse --port 8765 SSE mode for remote / multi-client (TODO v2.5)
513
- ${dim("Tools exposed: scan, list_rules, detect_archetype, estimate_cost, query_decisions")}
367
+ ${dim("Tools exposed: detect_archetype, estimate_cost, query_decisions,")}
368
+ ${dim(" project_status, cost_summary, pipeline_stages, recent_verdicts")}
514
369
 
515
370
  ${bold("Claude Code adapter:")}
516
371
  great-cto adapt Generate AGENTS.md + CLAUDE.md
@@ -997,26 +852,6 @@ async function main() {
997
852
  log(`Run ${cyan("great-cto --help")} for usage.`);
998
853
  process.exit(2);
999
854
  }
1000
- if (args.command === "scan") {
1001
- try {
1002
- const code = await runScan(args, rawArgv);
1003
- process.exit(code);
1004
- }
1005
- catch (e) {
1006
- error(e.message);
1007
- process.exit(2);
1008
- }
1009
- }
1010
- if (args.command === "list-rules") {
1011
- try {
1012
- const code = await runListRules();
1013
- process.exit(code);
1014
- }
1015
- catch (e) {
1016
- error(e.message);
1017
- process.exit(2);
1018
- }
1019
- }
1020
855
  if (args.command === "board") {
1021
856
  try {
1022
857
  const code = await runBoard(args);
@@ -1080,8 +915,11 @@ async function main() {
1080
915
  if (args.command === "serve") {
1081
916
  try {
1082
917
  const { runServe } = await import("./serve.js");
918
+ const explicitPort = rawArgv.some((a) => a === "--port" || a.startsWith("--port="));
1083
919
  const code = await runServe({
1084
- port: args.boardPort === 3141 ? 3142 : args.boardPort,
920
+ // serve defaults to 3142 (board uses 3141). Honor an explicit --port
921
+ // of any value, including 3141 — only fall back to 3142 when unset.
922
+ port: explicitPort ? args.boardPort : 3142,
1085
923
  noLog: rawArgv.includes("--no-log"),
1086
924
  insecure: rawArgv.includes("--insecure"),
1087
925
  });
@@ -1128,8 +966,8 @@ async function main() {
1128
966
  log(` ${cyan("/" + tried)} ${dim("[args]")}`);
1129
967
  log("");
1130
968
  log(`The CLI surface (this command) only exposes:`);
1131
- log(` ${cyan("init")} · ${cyan("scan")} · ${cyan("list-rules")} · ${cyan("ci")} · ${cyan("mcp")} ·`);
1132
- log(` ${cyan("adapt")} · ${cyan("serve")} · ${cyan("webhook")} · ${cyan("report")} · ${cyan("board")} · ${cyan("register")}`);
969
+ log(` ${cyan("init")} · ${cyan("ci")} · ${cyan("mcp")} · ${cyan("adapt")} ·`);
970
+ log(` ${cyan("serve")} · ${cyan("webhook")} · ${cyan("report")} · ${cyan("board")} · ${cyan("register")} · ${cyan("upgrade")}`);
1133
971
  log("");
1134
972
  log(`Run ${cyan("npx great-cto --help")} for the full CLI reference.`);
1135
973
  process.exit(2);
package/dist/mcp.js CHANGED
@@ -10,11 +10,13 @@
10
10
  // sse — long-running HTTP/SSE for remote / multi-client access
11
11
  //
12
12
  // Tools exposed:
13
- // scan OWASP LLM Top 10 + 24 rules → findings
14
- // list_rules full rule catalogue
15
13
  // detect_archetype archetype + compliance for a path
16
14
  // estimate_cost LLM/human time for a task
17
15
  // query_decisions search ~/.great_cto/decisions.md
16
+ // project_status board project state
17
+ // cost_summary board cost rollup
18
+ // pipeline_stages board pipeline stage breakdown
19
+ // recent_verdicts board recent agent verdicts
18
20
  //
19
21
  // Protocol: minimal MCP 2024-11-05 implementation. We hand-roll because
20
22
  // adding @modelcontextprotocol/sdk would balloon install size for what is
@@ -28,41 +30,6 @@ const SERVER_INFO = {
28
30
  version: "", // populated at startup
29
31
  };
30
32
  // ── Tool implementations ────────────────────────────────────────────────────
31
- async function toolScan(args) {
32
- const { scan } = await import("./agentshield/scanner.js");
33
- const path = args.path ?? ".";
34
- const report = scan(resolve(path), {
35
- minSeverity: (args.severity ?? "info"),
36
- scanners: args.scanner,
37
- });
38
- return {
39
- files_scanned: report.filesScanned,
40
- duration_ms: report.durationMs,
41
- findings: report.findings.map((f) => ({
42
- rule_id: f.rule.id,
43
- severity: f.rule.severity,
44
- title: f.rule.title,
45
- owasp: f.rule.owasp,
46
- file: f.location.file,
47
- line: f.location.line,
48
- snippet: f.location.snippet,
49
- })),
50
- };
51
- }
52
- async function toolListRules() {
53
- const { loadRules } = await import("./agentshield/rules-loader.js");
54
- const rules = loadRules();
55
- return {
56
- count: rules.length,
57
- rules: rules.map((r) => ({
58
- id: r.id,
59
- severity: r.severity,
60
- scanner: r.scanner,
61
- title: r.title,
62
- owasp: r.owasp ?? null,
63
- })),
64
- };
65
- }
66
33
  async function toolDetectArchetype(args) {
67
34
  const { detect } = await import("./detect.js");
68
35
  const { pickArchetype, suggestCompliance } = await import("./archetypes.js");
@@ -234,36 +201,6 @@ async function toolRecentVerdicts(args) {
234
201
  }
235
202
  // ── Tool dispatch table ────────────────────────────────────────────────────
236
203
  const TOOLS = [
237
- {
238
- name: "scan",
239
- description: "Scan code for AI/LLM-specific security issues (OWASP LLM Top 10, 24 rules). Returns findings with severity, file, line, OWASP mapping.",
240
- inputSchema: {
241
- type: "object",
242
- properties: {
243
- path: { type: "string", description: "File or directory to scan (default: cwd)" },
244
- severity: {
245
- type: "string",
246
- enum: ["info", "low", "medium", "high", "critical"],
247
- description: "Minimum severity to report",
248
- },
249
- scanner: {
250
- type: "array",
251
- items: {
252
- type: "string",
253
- enum: ["prompt-injection", "secrets-in-prompts", "ssrf-in-tools", "rag-poisoning", "cost-runaway"],
254
- },
255
- description: "Limit to specific scanner categories",
256
- },
257
- },
258
- },
259
- handler: toolScan,
260
- },
261
- {
262
- name: "list_rules",
263
- description: "List all 24 AgentShield security rules with severity and OWASP LLM Top 10 mapping.",
264
- inputSchema: { type: "object", properties: {} },
265
- handler: toolListRules,
266
- },
267
204
  {
268
205
  name: "detect_archetype",
269
206
  description: "Detect the project archetype (one of 25: fintech, healthcare, commerce, agent-product, mlops, edtech, gov-public, insurance, ...) and the compliance gates that apply.",
package/dist/report.js CHANGED
@@ -45,10 +45,14 @@ function readAllVerdicts() {
45
45
  return out.sort((a, b) => a.ts.localeCompare(b.ts));
46
46
  }
47
47
  function periodToCutoff(period) {
48
- const m = period.match(/^(\d+)d$/);
49
- if (!m)
48
+ // "all" intentionally returns "" → every verdict passes the `ts >= cutoff`
49
+ // filter (no lower bound).
50
+ if (period === "all")
50
51
  return "";
51
- const days = parseInt(m[1], 10);
52
+ // Malformed input (e.g. "30" without the "d", "1w") must NOT silently fall
53
+ // through to all-time — fall back to the documented 30d default instead.
54
+ const m = period.match(/^(\d+)d$/);
55
+ const days = m ? parseInt(m[1], 10) : 30;
52
56
  return new Date(Date.now() - days * 86_400_000).toISOString();
53
57
  }
54
58
  // ── Cost report ────────────────────────────────────────────────────────────