great-cto 1.0.167 → 1.0.169
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 +12 -0
- package/dist/llm-fallback.js +178 -0
- package/dist/main.js +71 -3
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -59,6 +59,18 @@ compliance: [${complianceLine}]
|
|
|
59
59
|
> \`compliance:\` list drives which checklists security-officer runs.
|
|
60
60
|
> See ARCHETYPES.md "Parameter Values" for supported keys.
|
|
61
61
|
|
|
62
|
+
## Memory & Query Rule
|
|
63
|
+
|
|
64
|
+
> Before reading source files, agents should query memory layers in this order:
|
|
65
|
+
> 1. **\`.great_cto/lessons.md\`** — project-specific lessons learned
|
|
66
|
+
> 2. **\`~/.great_cto/decisions.md\`** — global decisions log (ADR-style, all projects)
|
|
67
|
+
> 3. **\`~/.great_cto/verdicts/\`** — past agent verdicts (APPROVED/BLOCKED with rationale)
|
|
68
|
+
> 4. Only then read source files for the actual implementation
|
|
69
|
+
>
|
|
70
|
+
> This prevents re-deriving solved problems and surfaces "we decided this last
|
|
71
|
+
> sprint" insights. Agents update these layers via gate approvals (auto) and
|
|
72
|
+
> by appending to \`lessons.md\` after retrospectives.
|
|
73
|
+
|
|
62
74
|
## Goals
|
|
63
75
|
|
|
64
76
|
- <add your primary goal here>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// LLM fallback for low-confidence archetype detection.
|
|
2
|
+
//
|
|
3
|
+
// When pickArchetype() returns confidence: "low", we optionally call
|
|
4
|
+
// Anthropic Haiku with the README (first ~2KB) + dependency list and
|
|
5
|
+
// ask for a structured archetype suggestion. Cost: ~$0.001 per call.
|
|
6
|
+
//
|
|
7
|
+
// Privacy:
|
|
8
|
+
// - Only sends: README first 2KB + dep names (no versions) + stack list.
|
|
9
|
+
// - Never sends: source code, paths, file names, env vars, repo name.
|
|
10
|
+
// - User opts in via:
|
|
11
|
+
// 1. ANTHROPIC_API_KEY env var present (implicit), OR
|
|
12
|
+
// 2. --use-llm CLI flag (explicit override even on high confidence)
|
|
13
|
+
// - Skipped when --no-llm flag, GREATCTO_NO_LLM=1, or no API key.
|
|
14
|
+
//
|
|
15
|
+
// Zero deps: uses native fetch (Node 18+). No @anthropic-ai/sdk import.
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
const ANTHROPIC_API = "https://api.anthropic.com/v1/messages";
|
|
19
|
+
const MODEL = "claude-haiku-4-5"; // cheap + fast; ~$1/M in, $5/M out
|
|
20
|
+
const MAX_README_BYTES = 2048;
|
|
21
|
+
const TIMEOUT_MS = 8000;
|
|
22
|
+
const ALLOWED_ARCHETYPES = [
|
|
23
|
+
"web-service", "mobile-app", "ai-system", "agent-product",
|
|
24
|
+
"data-platform", "infra", "library", "cli-tool",
|
|
25
|
+
"commerce", "fintech", "healthcare", "web3",
|
|
26
|
+
"iot-embedded", "regulated", "devtools", "browser-extension", "game",
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Whether LLM fallback is available and should be tried for this run.
|
|
30
|
+
* Returns false silently if no API key, opted out, or running offline.
|
|
31
|
+
*/
|
|
32
|
+
export function shouldUseLlmFallback(opts) {
|
|
33
|
+
if (opts.forceSkip)
|
|
34
|
+
return { use: false, reason: "--no-llm flag set" };
|
|
35
|
+
if (process.env.GREATCTO_NO_LLM === "1")
|
|
36
|
+
return { use: false, reason: "GREATCTO_NO_LLM=1" };
|
|
37
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
38
|
+
if (!apiKey)
|
|
39
|
+
return { use: false, reason: "no ANTHROPIC_API_KEY" };
|
|
40
|
+
if (opts.forceUse)
|
|
41
|
+
return { use: true, reason: "--use-llm flag" };
|
|
42
|
+
if (opts.heuristicConfidence === "low")
|
|
43
|
+
return { use: true, reason: "low heuristic confidence" };
|
|
44
|
+
return { use: false, reason: "heuristic confidence is high/medium" };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the prompt sent to the LLM. Kept as a pure function for testing.
|
|
48
|
+
*/
|
|
49
|
+
export function buildPrompt(opts) {
|
|
50
|
+
const archList = ALLOWED_ARCHETYPES.join(" | ");
|
|
51
|
+
const readme = opts.readme.slice(0, MAX_README_BYTES).trim() || "(no README)";
|
|
52
|
+
const stack = opts.stack.length ? opts.stack.join(", ") : "(no detected stack)";
|
|
53
|
+
const hints = opts.readmeKeywords.length ? opts.readmeKeywords.join(", ") : "(none)";
|
|
54
|
+
return `You are classifying a software project into one of these archetypes:
|
|
55
|
+
${archList}
|
|
56
|
+
|
|
57
|
+
DETECTED STACK: ${stack}
|
|
58
|
+
README KEYWORDS: ${hints}
|
|
59
|
+
|
|
60
|
+
README EXCERPT (first 2KB):
|
|
61
|
+
"""
|
|
62
|
+
${readme}
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
Respond with ONLY a JSON object matching this schema (no prose, no markdown):
|
|
66
|
+
{
|
|
67
|
+
"archetype": "<one value from the list above>",
|
|
68
|
+
"confidence": "<high|medium|low>",
|
|
69
|
+
"rationale": "<one sentence, ≤120 chars, explaining the choice>"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
Rules:
|
|
73
|
+
- Choose the most specific archetype. fintech beats commerce when banking/ACH is present.
|
|
74
|
+
- agent-product = autonomous LLM agents (LangGraph/CrewAI/MCP), not just a wrapper around an LLM API.
|
|
75
|
+
- ai-system = an app that calls an LLM but is not itself agentic.
|
|
76
|
+
- cli-tool = primary distribution is a command-line binary.
|
|
77
|
+
- library = published as a reusable package, no app shell.
|
|
78
|
+
- If unsure between two, pick the more domain-specific one and use confidence: medium.`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validate the model response and coerce to LlmSuggestion shape.
|
|
82
|
+
* Returns null if the response is malformed or the archetype is invalid.
|
|
83
|
+
*/
|
|
84
|
+
export function parseLlmResponse(raw) {
|
|
85
|
+
// Strip code fences if model added them despite instructions
|
|
86
|
+
const cleaned = raw.trim()
|
|
87
|
+
.replace(/^```(?:json)?\s*/i, "")
|
|
88
|
+
.replace(/```\s*$/i, "")
|
|
89
|
+
.trim();
|
|
90
|
+
let obj;
|
|
91
|
+
try {
|
|
92
|
+
obj = JSON.parse(cleaned);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (typeof obj !== "object" || obj === null)
|
|
98
|
+
return null;
|
|
99
|
+
const o = obj;
|
|
100
|
+
const arch = o.archetype;
|
|
101
|
+
const conf = o.confidence;
|
|
102
|
+
const rat = o.rationale;
|
|
103
|
+
if (typeof arch !== "string" || typeof conf !== "string" || typeof rat !== "string")
|
|
104
|
+
return null;
|
|
105
|
+
if (!ALLOWED_ARCHETYPES.includes(arch))
|
|
106
|
+
return null;
|
|
107
|
+
if (!["high", "medium", "low"].includes(conf))
|
|
108
|
+
return null;
|
|
109
|
+
return {
|
|
110
|
+
archetype: arch,
|
|
111
|
+
confidence: conf,
|
|
112
|
+
rationale: rat.slice(0, 200),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function readReadme(dir) {
|
|
116
|
+
const candidates = ["README.md", "readme.md", "README", "README.rst"];
|
|
117
|
+
for (const f of candidates) {
|
|
118
|
+
const p = join(dir, f);
|
|
119
|
+
if (existsSync(p)) {
|
|
120
|
+
try {
|
|
121
|
+
return readFileSync(p, "utf-8");
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Best-effort LLM call. Returns null if anything fails (network, parse,
|
|
132
|
+
* timeout, rate limit). Never throws — caller is expected to fall back
|
|
133
|
+
* to the heuristic result silently.
|
|
134
|
+
*/
|
|
135
|
+
export async function suggestArchetypeFromLlm(opts) {
|
|
136
|
+
const readme = readReadme(opts.dir);
|
|
137
|
+
const prompt = buildPrompt({
|
|
138
|
+
readme,
|
|
139
|
+
stack: opts.detection.stack,
|
|
140
|
+
readmeKeywords: opts.detection.readmeKeywords,
|
|
141
|
+
});
|
|
142
|
+
const ctrl = new AbortController();
|
|
143
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetch(ANTHROPIC_API, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: {
|
|
148
|
+
"x-api-key": opts.apiKey,
|
|
149
|
+
"anthropic-version": "2023-06-01",
|
|
150
|
+
"content-type": "application/json",
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
model: MODEL,
|
|
154
|
+
max_tokens: 256,
|
|
155
|
+
temperature: 0,
|
|
156
|
+
messages: [{ role: "user", content: prompt }],
|
|
157
|
+
}),
|
|
158
|
+
signal: ctrl.signal,
|
|
159
|
+
});
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
if (!res.ok)
|
|
162
|
+
return null;
|
|
163
|
+
const body = (await res.json());
|
|
164
|
+
const text = body.content?.find((c) => c.type === "text")?.text;
|
|
165
|
+
if (!text)
|
|
166
|
+
return null;
|
|
167
|
+
const parsed = parseLlmResponse(text);
|
|
168
|
+
if (!parsed)
|
|
169
|
+
return null;
|
|
170
|
+
return {
|
|
171
|
+
...parsed,
|
|
172
|
+
conflictsWithHeuristic: parsed.archetype !== opts.heuristicArchetype,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -17,6 +17,7 @@ import { install, findInstalledVersions } from "./installer.js";
|
|
|
17
17
|
import { enableGreatCto } from "./settings.js";
|
|
18
18
|
import { bootstrap } from "./bootstrap.js";
|
|
19
19
|
import { resolveTelemetryConsent, sendInstallPing } from "./telemetry.js";
|
|
20
|
+
import { shouldUseLlmFallback, suggestArchetypeFromLlm } from "./llm-fallback.js";
|
|
20
21
|
import { readFileSync } from "node:fs";
|
|
21
22
|
import { dirname, join } from "node:path";
|
|
22
23
|
import { fileURLToPath } from "node:url";
|
|
@@ -44,6 +45,8 @@ function parseArgs(argv) {
|
|
|
44
45
|
archetype: null,
|
|
45
46
|
version: null,
|
|
46
47
|
noTelemetry: false,
|
|
48
|
+
useLlm: false,
|
|
49
|
+
noLlm: false,
|
|
47
50
|
};
|
|
48
51
|
const rest = [];
|
|
49
52
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -68,6 +71,10 @@ function parseArgs(argv) {
|
|
|
68
71
|
args.boardNoOpen = true;
|
|
69
72
|
else if (a === "--no-telemetry")
|
|
70
73
|
args.noTelemetry = true;
|
|
74
|
+
else if (a === "--use-llm")
|
|
75
|
+
args.useLlm = true;
|
|
76
|
+
else if (a === "--no-llm")
|
|
77
|
+
args.noLlm = true;
|
|
71
78
|
else if (a === "board")
|
|
72
79
|
args.command = "board";
|
|
73
80
|
else if (a === "register")
|
|
@@ -191,11 +198,17 @@ ${bold("Options:")}
|
|
|
191
198
|
--dry-run Show what would be done without doing it
|
|
192
199
|
--force Reinstall even if already present
|
|
193
200
|
--archetype NAME Override detected archetype
|
|
194
|
-
(${cyan("web-service|mobile-app|ai-system|agent-product|commerce|
|
|
195
|
-
${cyan("data-platform|infra|library|
|
|
196
|
-
${cyan("devtools|browser-extension|game")})
|
|
201
|
+
(${cyan("web-service|mobile-app|ai-system|agent-product|commerce|fintech|")}
|
|
202
|
+
${cyan("healthcare|web3|data-platform|infra|library|cli-tool|")}
|
|
203
|
+
${cyan("iot-embedded|regulated|devtools|browser-extension|game")})
|
|
197
204
|
--version-tag VER Pin to specific great_cto version (default: latest)
|
|
198
205
|
--dir PATH Run against a different directory (default: cwd)
|
|
206
|
+
--use-llm Force LLM (Anthropic Haiku) archetype suggestion
|
|
207
|
+
even when heuristic confidence is high
|
|
208
|
+
--no-llm Skip LLM suggestion (run heuristic only)
|
|
209
|
+
Or set ${cyan("GREATCTO_NO_LLM=1")}
|
|
210
|
+
--no-telemetry Skip anonymous install ping
|
|
211
|
+
Or set ${cyan("GREATCTO_NO_TELEMETRY=1")}
|
|
199
212
|
-h, --help Show this help
|
|
200
213
|
-v, --version Show great-cto CLI version
|
|
201
214
|
|
|
@@ -254,6 +267,61 @@ async function runInit(args) {
|
|
|
254
267
|
alternatives = pick.alternatives;
|
|
255
268
|
confidence = pick.confidence;
|
|
256
269
|
}
|
|
270
|
+
// ── 2b. LLM fallback for low-confidence detections (Wave 4) ──────
|
|
271
|
+
if (!args.archetype) {
|
|
272
|
+
const llmDecision = shouldUseLlmFallback({
|
|
273
|
+
heuristicConfidence: confidence,
|
|
274
|
+
forceUse: args.useLlm,
|
|
275
|
+
forceSkip: args.noLlm,
|
|
276
|
+
});
|
|
277
|
+
if (llmDecision.use) {
|
|
278
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
279
|
+
if (apiKey) {
|
|
280
|
+
log(` ${dim("→ low confidence — asking Anthropic Haiku for second opinion...")}`);
|
|
281
|
+
const llm = await suggestArchetypeFromLlm({
|
|
282
|
+
dir: args.dir,
|
|
283
|
+
detection,
|
|
284
|
+
heuristicArchetype: archetype,
|
|
285
|
+
apiKey,
|
|
286
|
+
});
|
|
287
|
+
if (llm) {
|
|
288
|
+
if (llm.conflictsWithHeuristic) {
|
|
289
|
+
log("");
|
|
290
|
+
log(` ${bold("AI suggests:")} ${cyan(llm.archetype)} ${dim(`(${llm.confidence})`)}`);
|
|
291
|
+
log(` ${dim("AI rationale:")} ${llm.rationale}`);
|
|
292
|
+
log(` ${bold("Heuristic says:")} ${cyan(archetype)} ${dim(`(${confidence})`)}`);
|
|
293
|
+
if (!args.yes) {
|
|
294
|
+
const accept = await confirm(`Use AI suggestion ${cyan(llm.archetype)} instead of ${cyan(archetype)}?`, true);
|
|
295
|
+
if (accept) {
|
|
296
|
+
archetype = llm.archetype;
|
|
297
|
+
rationale = `(AI) ${llm.rationale}`;
|
|
298
|
+
confidence = llm.confidence;
|
|
299
|
+
if (!alternatives.includes(archetype)) {
|
|
300
|
+
alternatives = [archetype, ...alternatives.filter((a) => a !== archetype)].slice(0, 3);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
// --yes: silently take AI suggestion only if it bumps confidence
|
|
306
|
+
if (llm.confidence !== "low") {
|
|
307
|
+
archetype = llm.archetype;
|
|
308
|
+
rationale = `(AI) ${llm.rationale}`;
|
|
309
|
+
confidence = llm.confidence;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else if (llm.confidence !== "low") {
|
|
314
|
+
// AI agrees → bump confidence, refine rationale
|
|
315
|
+
confidence = llm.confidence;
|
|
316
|
+
rationale = `${rationale} (AI confirmed: ${llm.rationale})`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
log(` ${dim("(LLM call failed, keeping heuristic)")}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
257
325
|
const compliance = suggestCompliance(detection, archetype);
|
|
258
326
|
log(` ${dim("archetype:")} ${cyan(archetype)} ${dim(`(confidence: ${confidence})`)}`);
|
|
259
327
|
log(` ${dim("rationale:")} ${rationale}`);
|
package/package.json
CHANGED