great-cto 2.19.0 → 2.21.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.
@@ -1068,13 +1068,52 @@ export function pickArchetype(d) {
1068
1068
  break;
1069
1069
  }
1070
1070
  }
1071
+ // ── Pack hints for low/medium confidence or niche stacks ────────────────
1072
+ // Surfaces domain-specific packs that the archetype alone doesn't capture.
1073
+ const suggestedPacks = confidence !== "high"
1074
+ ? inferPackHints(d)
1075
+ : inferPackHints(d).filter((p) => isNichePack(p)); // always surface niche packs
1071
1076
  return {
1072
1077
  primary: top.archetype,
1073
1078
  confidence,
1074
1079
  rationale: top.reason,
1075
1080
  alternatives,
1081
+ ...(suggestedPacks.length > 0 ? { suggestedPacks } : {}),
1076
1082
  };
1077
1083
  }
1084
+ /** Niche packs that should surface even at high confidence */
1085
+ function isNichePack(pack) {
1086
+ return ["robotics-pack", "climate-pack", "drug-discovery-pack",
1087
+ "clinical-trials-pack", "em-fintech-pack"].includes(pack);
1088
+ }
1089
+ /**
1090
+ * Infer likely domain packs from README/infra keywords when the archetype
1091
+ * scorer doesn't have a dedicated high-confidence rule for the domain.
1092
+ */
1093
+ function inferPackHints(d) {
1094
+ const hints = [];
1095
+ const kws = new Set([...d.readmeKeywords, ...(d.infraKeywords ?? [])]);
1096
+ const has = (...terms) => terms.some((t) => kws.has(t));
1097
+ if (has("robot", "ros2", "ros 2", "cobot", "drone", "uav"))
1098
+ hints.push("robotics-pack");
1099
+ if (has("carbon", "ghg", "mrv", "emission", "verra", "sbti"))
1100
+ hints.push("climate-pack");
1101
+ if (has("clinical", "ctms", "edc", "cdisc", "randomization", "irb"))
1102
+ hints.push("clinical-trials-pack");
1103
+ if (has("drug discovery", "binding affinity", "admet", "chembl", "alphafold"))
1104
+ hints.push("drug-discovery-pack");
1105
+ if (has("recruit", "hiring", "candidate", "ats", "aedt"))
1106
+ hints.push("hr-ai-pack");
1107
+ if (has("loan", "lending", "bnpl", "underwrit", "fcra"))
1108
+ hints.push("lending-pack");
1109
+ if (has("voice", "telephony", "ivr", "stt", "tts", "outbound call"))
1110
+ hints.push("voice-pack");
1111
+ if (has("india", "upi", "rbi", "mpesa", "gcash", "pix", "cross-border", "remittance"))
1112
+ hints.push("em-fintech-pack");
1113
+ if (has("public api", "api key", "developer portal", "openapi"))
1114
+ hints.push("api-platform-pack");
1115
+ return hints;
1116
+ }
1078
1117
  // Compliance hints — auto-suggested based on stack and README.
1079
1118
  export function suggestCompliance(d, archetype) {
1080
1119
  const c = new Set();
package/dist/bootstrap.js CHANGED
@@ -4,12 +4,13 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { dim, success, warn } from "./ui.js";
6
6
  import { suggestJurisdictions } from "./jurisdictions.js";
7
+ import { compileFlow, renderFlowMd } from "./flow.js";
7
8
  export function bootstrap(dir, detection, archetype, compliance, detectionMeta) {
8
9
  const greatCtoDir = join(dir, ".great_cto");
9
10
  const projectMd = join(greatCtoDir, "PROJECT.md");
10
11
  if (existsSync(projectMd)) {
11
12
  warn(`.great_cto/PROJECT.md already exists — not overwriting.`);
12
- return { projectMdPath: projectMd, created: false, skippedReason: "already exists" };
13
+ return { projectMdPath: projectMd, created: false, skippedReason: "already exists", flow: null };
13
14
  }
14
15
  mkdirSync(greatCtoDir, { recursive: true });
15
16
  const title = inferProjectTitle(dir);
@@ -120,7 +121,14 @@ when you actually start work:
120
121
  `;
121
122
  writeFileSync(projectMd, content, "utf-8");
122
123
  success(`created .great_cto/PROJECT.md ${dim(`(archetype: ${archetype})`)}`);
123
- return { projectMdPath: projectMd, created: true, skippedReason: null };
124
+ // Write FLOW.md compiled delivery flow for agents and user
125
+ const confidence = detectionMeta?.confidence ?? "medium";
126
+ const size = (detection.projectSize ?? "medium");
127
+ const flow = compileFlow(archetype, size, detection, compliance, confidence);
128
+ const flowMdPath = join(greatCtoDir, "FLOW.md");
129
+ const generatedAt = new Date().toISOString().slice(0, 10);
130
+ writeFileSync(flowMdPath, renderFlowMd(flow, generatedAt), "utf-8");
131
+ return { projectMdPath: projectMd, created: true, skippedReason: null, flow };
124
132
  }
125
133
  /**
126
134
  * Slugify a project title into a tenant-id safe for HTTP headers and audit
package/dist/detect.js CHANGED
@@ -1004,6 +1004,10 @@ export function detect(dir) {
1004
1004
  const readmeKeywords = mineReadmeKeywords(dir);
1005
1005
  for (const kw of readmeKeywords)
1006
1006
  sig(`readme:${kw}`, "README");
1007
+ // ── Infra signals (Wave 2b) — terraform/env/docker/homepage ──
1008
+ const infraKeywords = mineInfraKeywords(dir, pkg);
1009
+ for (const kw of infraKeywords)
1010
+ sig(`infra:${kw}`, "infra");
1007
1011
  return {
1008
1012
  stack: Array.from(stack).sort(),
1009
1013
  languages: Array.from(languages).sort(),
@@ -1022,6 +1026,7 @@ export function detect(dir) {
1022
1026
  scripts: scriptHints,
1023
1027
  projectSize,
1024
1028
  readmeKeywords,
1029
+ infraKeywords,
1025
1030
  };
1026
1031
  }
1027
1032
  // ── helpers ──────────────────────────────────────────────────
@@ -1208,6 +1213,25 @@ function mineReadmeKeywords(dir) {
1208
1213
  "pdpa", "pdpc", "singapore users", "singaporean users",
1209
1214
  "mas guidelines", "mas tpm", "singpass", "myinfo",
1210
1215
  "singapore data residency",
1216
+ // CA
1217
+ "pipeda", "quebec law 25", "bill 64", "opc canada", "casl",
1218
+ "canadian users", "canada users", "canadian customers", "canadian residents",
1219
+ "osfi", "fintrac", "ca-central", "ca-west", "canada-central",
1220
+ // JP
1221
+ "appi", "personal information protection commission", "ppc japan",
1222
+ "japan users", "japanese users", "japan customers",
1223
+ "fsa japan", "jfsa", "fisc",
1224
+ "ap-northeast-1", "ap-northeast-3", "japan east", "japan west",
1225
+ // CN
1226
+ "pipl", "personal information protection law", "data security law",
1227
+ "mlps", "classified protection", "cyberspace administration",
1228
+ "china users", "chinese users", "mainland china",
1229
+ "pboc", "cn-north", "cn-east", "cn-south", "china-east", "china-north",
1230
+ // KR
1231
+ "pipa korea", "pipa", "personal information protection act korea",
1232
+ "pipc", "isms-p", "kisa", "k-isms",
1233
+ "korea users", "korean users", "south korea users",
1234
+ "fsc korea", "ap-northeast-2", "korea central", "korea south",
1211
1235
  ];
1212
1236
  for (const term of jurisdictionTerms) {
1213
1237
  if (text.includes(term))
@@ -1215,6 +1239,148 @@ function mineReadmeKeywords(dir) {
1215
1239
  }
1216
1240
  return Array.from(kws).sort();
1217
1241
  }
1242
+ /**
1243
+ * Mine infra-level jurisdiction signals that README often omits:
1244
+ * - Terraform/Pulumi/CDK region strings (eu-west-1, ap-northeast-2, …)
1245
+ * - .env.example / .env.sample / .env.test AWS_REGION / AZURE_LOCATION / GCP_REGION
1246
+ * - docker-compose.yml TZ= environment variables
1247
+ * - package.json homepage TLD (.de, .fr, .jp, .cn, .kr, .ca, …)
1248
+ *
1249
+ * Returns a flat list of canonical keyword strings that jurisdiction.ts can match.
1250
+ */
1251
+ function mineInfraKeywords(dir, pkg) {
1252
+ const kws = new Set();
1253
+ // ── 1. package.json homepage TLD → jurisdiction keyword ──────────────────
1254
+ const homepageTldMap = {
1255
+ ".de": "german users", ".at": "austrian", ".fr": "french users",
1256
+ ".nl": "dutch users", ".es": "spanish users", ".it": "italian users",
1257
+ ".pl": "polish users", ".eu": "eu users",
1258
+ ".co.uk": "uk users", ".uk": "uk users",
1259
+ ".ca": "canadian users",
1260
+ ".jp": "japan users",
1261
+ ".cn": "china users",
1262
+ ".kr": "korea users",
1263
+ ".com.br": "brazil users", ".br": "brazil users",
1264
+ ".com.au": "australia users", ".au": "australia users",
1265
+ ".sg": "singapore users",
1266
+ ".in": "india users",
1267
+ };
1268
+ const homepage = (pkg?.homepage ?? "").toLowerCase();
1269
+ if (homepage) {
1270
+ for (const [tld, kw] of Object.entries(homepageTldMap)) {
1271
+ if (homepage.includes(tld))
1272
+ kws.add(kw);
1273
+ }
1274
+ }
1275
+ // ── 2. .env / docker-compose TZ= → jurisdiction ──────────────────────────
1276
+ const tzRegionMap = [
1277
+ [/tz=europe\//i, "eu users"],
1278
+ [/tz=america\/toronto|tz=canada/i, "canadian users"],
1279
+ [/tz=asia\/tokyo/i, "japan users"],
1280
+ [/tz=asia\/shanghai|tz=asia\/beijing|tz=prc|tz=asia\/hong_kong/i, "china users"],
1281
+ [/tz=asia\/seoul/i, "korea users"],
1282
+ [/tz=asia\/kolkata|tz=asia\/calcutta/i, "india users"],
1283
+ [/tz=america\/sao_paulo/i, "brazil users"],
1284
+ [/tz=australia\//i, "australia users"],
1285
+ [/tz=asia\/singapore/i, "singapore users"],
1286
+ [/tz=europe\/london/i, "uk users"],
1287
+ ];
1288
+ const envFiles = [".env.example", ".env.sample", ".env.test", ".env.local.example",
1289
+ "docker-compose.yml", "docker-compose.yaml",
1290
+ "docker-compose.dev.yml", "docker-compose.prod.yml"];
1291
+ for (const f of envFiles) {
1292
+ const p = join(dir, f);
1293
+ if (!existsSync(p))
1294
+ continue;
1295
+ try {
1296
+ const txt = readFileSync(p, "utf-8").slice(0, 8000).toLowerCase();
1297
+ // AWS_REGION / AZURE_LOCATION / GCP_REGION / REGION
1298
+ const awsRegion = txt.match(/(?:aws_region|region)\s*=\s*["']?([a-z0-9-]+)/g) ?? [];
1299
+ for (const m of awsRegion) {
1300
+ const val = m.split(/=\s*["']?/)[1] ?? "";
1301
+ if (/^eu-/.test(val) || /^europe/.test(val))
1302
+ kws.add("eu users");
1303
+ if (/^ca-/.test(val) || val.includes("canada"))
1304
+ kws.add("canadian users");
1305
+ if (/^ap-northeast-1$|^ap-northeast-3$/.test(val))
1306
+ kws.add("japan users");
1307
+ if (/^ap-northeast-2$/.test(val))
1308
+ kws.add("korea users");
1309
+ if (/^cn-/.test(val) || val.includes("china"))
1310
+ kws.add("china users");
1311
+ if (/^ap-south-1$/.test(val) || val.includes("india"))
1312
+ kws.add("india users");
1313
+ if (/^ap-southeast-1$/.test(val))
1314
+ kws.add("singapore users");
1315
+ if (/^ap-southeast-2$/.test(val))
1316
+ kws.add("australia users");
1317
+ if (/^sa-east/.test(val))
1318
+ kws.add("brazil users");
1319
+ if (/^us-/.test(val) || /^us_/.test(val))
1320
+ kws.add("us users");
1321
+ if (val.includes("uk") || val.includes("europe") && val.includes("west"))
1322
+ kws.add("uk users");
1323
+ }
1324
+ for (const [re, kw] of tzRegionMap) {
1325
+ if (re.test(txt))
1326
+ kws.add(kw);
1327
+ }
1328
+ }
1329
+ catch { /* unreadable */ }
1330
+ }
1331
+ // ── 3. Terraform / Pulumi / CloudFormation region strings ─────────────────
1332
+ const tfFiles = [];
1333
+ function collectTf(d, depth) {
1334
+ if (depth > 4)
1335
+ return;
1336
+ const SKIP = new Set(["node_modules", ".git", "dist", ".terraform"]);
1337
+ try {
1338
+ for (const e of readdirSync(d)) {
1339
+ if (SKIP.has(e))
1340
+ continue;
1341
+ const p = join(d, e);
1342
+ try {
1343
+ const st = statSync(p);
1344
+ if (st.isDirectory()) {
1345
+ collectTf(p, depth + 1);
1346
+ continue;
1347
+ }
1348
+ if (/\.(tf|yaml|yml|json)$/.test(e) && st.size < 200_000)
1349
+ tfFiles.push(p);
1350
+ }
1351
+ catch { /* skip */ }
1352
+ if (tfFiles.length > 40)
1353
+ return;
1354
+ }
1355
+ }
1356
+ catch { /* skip */ }
1357
+ }
1358
+ collectTf(dir, 0);
1359
+ const regionPatterns = [
1360
+ [/\beu-west-\d|eu-central-\d|eu-north-\d|eu-south-\d|europe-west\d|europe-north\d|westeurope|northeurope|germanywestcentral|francecentral\b/i, "eu users"],
1361
+ [/\bca-central-\d|canadacentral|canadaeast\b/i, "canadian users"],
1362
+ [/\bap-northeast-1\b|\bjapan-east\b|\bjapaneast\b|\bjapanwest\b/i, "japan users"],
1363
+ [/\bap-northeast-2\b|\bkoreacentral\b|\bkoreasouth\b/i, "korea users"],
1364
+ [/\bcn-north-\d|\bcn-east-\d|\bcn-northwest-\d|\bchinanorth\b|\bchinaeast\b/i, "china users"],
1365
+ [/\bap-south-\d|\bcentralindia\b|\bsouthindia\b|\bwestindia\b/i, "india users"],
1366
+ [/\bap-southeast-1\b|\bsoutheastasia\b/i, "singapore users"],
1367
+ [/\bap-southeast-2\b|\baustralia\b|\baustraliasoutheast\b|\baustraliaeast\b/i, "australia users"],
1368
+ [/\bsa-east-\d|\bbrazilsouth\b|\bbrazilsoutheast\b/i, "brazil users"],
1369
+ [/\buksouth\b|\bukwest\b|\buk-south\b/i, "uk users"],
1370
+ [/\bus-east-\d|\bus-west-\d|\beastus\b|\bwestus\b|\bcentralus\b/i, "us users"],
1371
+ ];
1372
+ for (const p of tfFiles) {
1373
+ try {
1374
+ const txt = readFileSync(p, "utf-8").slice(0, 50_000);
1375
+ for (const [re, kw] of regionPatterns) {
1376
+ if (re.test(txt))
1377
+ kws.add(kw);
1378
+ }
1379
+ }
1380
+ catch { /* skip */ }
1381
+ }
1382
+ return Array.from(kws).sort();
1383
+ }
1218
1384
  function safeGlob(dir, pattern, kind = "file") {
1219
1385
  try {
1220
1386
  const entries = readdirSync(dir);
package/dist/flow.js ADDED
@@ -0,0 +1,188 @@
1
+ // flow.ts — compiles all detection outputs into a single user-facing FlowResult.
2
+ // Pure function: no I/O, no side effects.
3
+ // Called by bootstrap.ts (writes FLOW.md) and main.ts (prints summary).
4
+ import { reviewersFor, gatesFor } from "./archetypes.js";
5
+ import { suggestPackReviewers, suggestPackGates, suggestPacks } from "./packs.js";
6
+ import { suggestJurisdictions, suggestJurisdictionReviewers, suggestJurisdictionGates } from "./jurisdictions.js";
7
+ // ── Human-readable titles ─────────────────────────────────────────────────
8
+ const ARCHETYPE_TITLE = {
9
+ "fintech": "Fintech",
10
+ "healthcare": "Healthcare",
11
+ "enterprise-saas": "Enterprise SaaS",
12
+ "agent-product": "AI agent",
13
+ "ai-system": "AI system",
14
+ "mlops": "MLOps pipeline",
15
+ "commerce": "E-commerce",
16
+ "marketplace": "Marketplace",
17
+ "mobile-app": "Mobile app",
18
+ "web-service": "Web service",
19
+ "library": "Library / SDK",
20
+ "cli-tool": "CLI tool",
21
+ "data-platform": "Data platform",
22
+ "streaming": "Streaming system",
23
+ "infra": "Infrastructure",
24
+ "devtools": "Developer tool",
25
+ "browser-extension": "Browser extension",
26
+ "game": "Game",
27
+ "web3": "Web3 / DeFi",
28
+ "iot-embedded": "IoT / embedded",
29
+ "cms": "CMS",
30
+ "edtech": "EdTech",
31
+ "gov-public": "Government",
32
+ "insurance": "Insurance",
33
+ "regulated": "Regulated system",
34
+ "greenfield": "New project",
35
+ };
36
+ // Gate id (StandardGate) → user label
37
+ const GATE_LABEL = {
38
+ "plan": "gate:plan",
39
+ "arch": "gate:arch",
40
+ "code": "gate:code",
41
+ "qa": "gate:qa",
42
+ "security": "gate:security",
43
+ "compliance": "gate:compliance",
44
+ "ship": "gate:ship",
45
+ "cost": "gate:cost-forecast",
46
+ "oracle-review": "gate:oracle-review",
47
+ "edtech-review": "gate:edtech-review",
48
+ "gov-review": "gate:gov-review",
49
+ "insurance-review": "gate:insurance-review",
50
+ };
51
+ // Cost (low, high) per feature cycle by archetype tier
52
+ const ARCHETYPE_COST = {
53
+ "fintech": [8, 18],
54
+ "healthcare": [8, 18],
55
+ "agent-product": [8, 18],
56
+ "mlops": [8, 18],
57
+ "marketplace": [8, 18],
58
+ "enterprise-saas": [8, 18],
59
+ "regulated": [8, 18],
60
+ "edtech": [8, 18],
61
+ "gov-public": [8, 18],
62
+ "insurance": [8, 18],
63
+ "web3": [8, 18],
64
+ "commerce": [3, 8],
65
+ "mobile-app": [3, 8],
66
+ "web-service": [3, 8],
67
+ "data-platform": [3, 8],
68
+ "streaming": [3, 8],
69
+ "devtools": [3, 8],
70
+ "browser-extension": [3, 8],
71
+ "game": [3, 8],
72
+ "cms": [3, 8],
73
+ "ai-system": [3, 8],
74
+ "iot-embedded": [3, 8],
75
+ "infra": [3, 8],
76
+ "library": [0.5, 3],
77
+ "cli-tool": [0.5, 3],
78
+ "greenfield": [0.5, 3],
79
+ };
80
+ // ── Main export ───────────────────────────────────────────────────────────
81
+ /**
82
+ * Compile all detection outputs into a single FlowResult.
83
+ *
84
+ * Pure function — no file I/O. Called by bootstrap.ts (FLOW.md) and
85
+ * main.ts (summary output).
86
+ */
87
+ export function compileFlow(archetype, size, detection, compliance, confidence) {
88
+ // ── Agents ──────────────────────────────────────────────────────────────
89
+ const agentSet = new Set(reviewersFor(archetype));
90
+ for (const r of suggestPackReviewers(detection))
91
+ agentSet.add(r);
92
+ for (const r of suggestJurisdictionReviewers(detection))
93
+ agentSet.add(r);
94
+ // Always include base orchestration agents
95
+ agentSet.add("architect");
96
+ agentSet.add("senior-dev");
97
+ agentSet.add("qa-engineer");
98
+ // ── Gates ────────────────────────────────────────────────────────────────
99
+ const gateSet = new Set(gatesFor(archetype, size).map((g) => GATE_LABEL[g] ?? `gate:${g}`));
100
+ for (const g of suggestPackGates(detection))
101
+ gateSet.add(g);
102
+ for (const g of suggestJurisdictionGates(detection))
103
+ gateSet.add(g);
104
+ // ── Packs + jurisdictions for routing block ──────────────────────────────
105
+ const packs = suggestPacks(detection);
106
+ const jurisdictions = suggestJurisdictions(detection);
107
+ // ── Title ────────────────────────────────────────────────────────────────
108
+ const productLabel = ARCHETYPE_TITLE[archetype] ?? archetype;
109
+ const jCodes = jurisdictions
110
+ .slice(0, 3)
111
+ .map((j) => j.jurisdiction.toUpperCase())
112
+ .join(" + ");
113
+ const title = jCodes ? `${productLabel} · ${jCodes}` : productLabel;
114
+ // ── ID ───────────────────────────────────────────────────────────────────
115
+ const id = [archetype, ...jurisdictions.map((j) => j.jurisdiction)]
116
+ .join("-")
117
+ .toLowerCase();
118
+ // ── Cost range ────────────────────────────────────────────────────────────
119
+ const costEntry = ARCHETYPE_COST[archetype] ?? [3, 8];
120
+ const [low, high] = costEntry;
121
+ return {
122
+ id,
123
+ title,
124
+ agents: Array.from(agentSet).sort(),
125
+ gates: Array.from(gateSet).sort(),
126
+ compliance: [...new Set(compliance)].sort(),
127
+ costRange: { low, high },
128
+ routing: {
129
+ archetype,
130
+ packs: packs.map((p) => p.pack),
131
+ jurisdictions: jurisdictions.map((j) => j.jurisdiction),
132
+ confidence,
133
+ },
134
+ };
135
+ }
136
+ /**
137
+ * Render FLOW.md content from a FlowResult.
138
+ * Exported separately so bootstrap.ts can call it without depending on main.ts.
139
+ */
140
+ export function renderFlowMd(flow, generatedAt) {
141
+ const agentLines = flow.agents.map((a) => `- ${a}`).join("\n");
142
+ const gateLines = flow.gates.map((g) => `- ${g}`).join("\n");
143
+ const complianceLines = flow.compliance.length > 0
144
+ ? flow.compliance.map((c) => `- ${c}`).join("\n")
145
+ : "- none";
146
+ const packLines = flow.routing.packs.length > 0
147
+ ? flow.routing.packs.join(", ")
148
+ : "none";
149
+ const jLines = flow.routing.jurisdictions.length > 0
150
+ ? flow.routing.jurisdictions.join(", ")
151
+ : "none";
152
+ return `# Delivery Flow
153
+
154
+ > Auto-generated by \`great-cto init\` on ${generatedAt}.
155
+ > This file tells agents how to orchestrate your SDLC.
156
+ > Regenerates on \`npx great-cto init --force\`. Edit \`_routing:\` to tune.
157
+
158
+ ## Detected
159
+
160
+ ${flow.title}
161
+
162
+ ## Agents
163
+
164
+ ${agentLines}
165
+
166
+ ## Human gates
167
+
168
+ ${gateLines}
169
+
170
+ ## Compliance
171
+
172
+ ${complianceLines}
173
+
174
+ ## Cost estimate
175
+
176
+ $${flow.costRange.low}–$${flow.costRange.high} per feature cycle
177
+
178
+ ---
179
+
180
+ <!-- Internal routing — view with: great-cto flow explain -->
181
+ _routing:
182
+ id: ${flow.id}
183
+ archetype: ${flow.routing.archetype}
184
+ packs: [${packLines}]
185
+ jurisdictions: [${jLines}]
186
+ confidence: ${flow.routing.confidence}
187
+ `;
188
+ }
@@ -18,6 +18,10 @@ const JURISDICTION_REVIEWERS = {
18
18
  "br": ["gdpr-reviewer"], // LGPD mirrors GDPR — same reviewer covers
19
19
  "au": ["us-privacy-reviewer"], // Privacy Act 1988 — covered by privacy reviewer
20
20
  "sg": ["us-privacy-reviewer"], // PDPA — covered by privacy reviewer
21
+ "ca": ["us-privacy-reviewer"], // PIPEDA + Quebec Law 25
22
+ "jp": ["us-privacy-reviewer"], // APPI — covered by privacy reviewer
23
+ "cn": ["gdpr-reviewer"], // PIPL structure mirrors GDPR concepts
24
+ "kr": ["us-privacy-reviewer"], // PIPA — covered by privacy reviewer
21
25
  };
22
26
  const JURISDICTION_GATES = {
23
27
  "eu": ["gate:gdpr-dpia", "gate:eu-ai-act-classification"],
@@ -28,6 +32,10 @@ const JURISDICTION_GATES = {
28
32
  "br": ["gate:lgpd-dpia"],
29
33
  "au": ["gate:au-privacy-act-assessment"],
30
34
  "sg": ["gate:pdpa-dpo"],
35
+ "ca": ["gate:pipeda-pia", "gate:quebec-law25-consent"],
36
+ "jp": ["gate:appi-third-party-transfer", "gate:appi-ppc-registration"],
37
+ "cn": ["gate:pipl-consent-framework", "gate:mlps-classification", "gate:pipl-data-localisation"],
38
+ "kr": ["gate:pipa-isms-p", "gate:pipa-consent-framework"],
31
39
  };
32
40
  const JURISDICTION_LAWS = {
33
41
  "eu": ["GDPR (EU) 2016/679", "EU AI Act 2024/1689", "NIS2 Directive 2022/2555", "ePrivacy Directive"],
@@ -38,6 +46,10 @@ const JURISDICTION_LAWS = {
38
46
  "br": ["LGPD (Lei 13.709/2018)", "ANPD resolutions", "Marco Civil da Internet"],
39
47
  "au": ["Privacy Act 1988 (Cth)", "Australian Privacy Principles (APPs)", "CDR (if fintech)", "OAIC enforcement"],
40
48
  "sg": ["PDPA 2012 (amended 2021)", "MAS TRM Guidelines (if fintech)", "PDPC Advisory Guidelines"],
49
+ "ca": ["PIPEDA (Personal Information Protection and Electronic Documents Act)", "Quebec Law 25 / Bill 64", "CASL (if email marketing)", "OSFI B-10 (if fintech)"],
50
+ "jp": ["APPI 2022 (Act on Protection of Personal Information)", "PPC Guidelines", "My Number Act (if govt-adjacent)", "FISC (if fintech)"],
51
+ "cn": ["PIPL 2021 (Personal Information Protection Law)", "DSL 2021 (Data Security Law)", "MLPS 2.0 (Cybersecurity Classified Protection)", "CBDT (cross-border data transfer rules)", "CAC regulations"],
52
+ "kr": ["PIPA (Personal Information Protection Act)", "ISMS-P certification (mandatory for large platforms)", "Network Act", "FSC regulations (if fintech)"],
41
53
  };
42
54
  // ── Signal dictionary ─────────────────────────────────────────────────────
43
55
  export const JURISDICTION_SIGNALS = {
@@ -112,15 +124,95 @@ export const JURISDICTION_SIGNALS = {
112
124
  "singapore data residency",
113
125
  ],
114
126
  },
127
+ "ca": {
128
+ keywords: [
129
+ // Privacy law
130
+ "pipeda", "quebec law 25", "bill 64", "privacy commissioner",
131
+ "opc canada", "casl", "canadian users", "canada users",
132
+ "canadian customers", "canadian residents",
133
+ // Fintech
134
+ "osfi", "fintrac", "aml canada",
135
+ // Infra
136
+ "ca-central", "ca-west", "canada-central",
137
+ ],
138
+ },
139
+ "jp": {
140
+ keywords: [
141
+ // Privacy law
142
+ "appi", "personal information protection commission", "ppc japan",
143
+ "japan users", "japanese users", "japan customers",
144
+ "my number", "individual number act",
145
+ // Fintech
146
+ "fsa japan", "jfsa", "fisc",
147
+ // Infra
148
+ "ap-northeast-1", "ap-northeast-3", "japan east", "japan west",
149
+ "japaneast", "japanwest",
150
+ ],
151
+ },
152
+ "cn": {
153
+ keywords: [
154
+ // Privacy / data laws
155
+ "pipl", "personal information protection law", "data security law",
156
+ "dsl 2021", "mlps", "classified protection", "cybersecurity law",
157
+ "cac", "cyberspace administration", "cbdt",
158
+ "china users", "chinese users", "mainland china",
159
+ // Cross-border
160
+ "cross-border data transfer china", "security assessment cac",
161
+ "standard contract cac", "personal information export",
162
+ // Fintech
163
+ "pboc", "nfra", "cbirc", "csrc", "alipay", "wechatpay",
164
+ // Infra
165
+ "cn-north", "cn-northwest", "cn-east", "cn-south",
166
+ "china-east", "china-north", "chinaeast", "chinanorth",
167
+ ],
168
+ },
169
+ "kr": {
170
+ keywords: [
171
+ // Privacy / data laws
172
+ "pipa korea", "pipa", "personal information protection act korea",
173
+ "pipc", "privacy commissioner korea",
174
+ "korea users", "korean users", "south korea users",
175
+ "isms-p", "kisa", "k-isms",
176
+ // Fintech
177
+ "fsc korea", "fss korea", "kftc",
178
+ // Infra
179
+ "ap-northeast-2", "korea central", "korea south",
180
+ "koreacentral", "koreasouth",
181
+ ],
182
+ },
115
183
  };
116
184
  // ── Public API ─────────────────────────────────────────────────────────────
185
+ /**
186
+ * Word-boundary aware keyword match.
187
+ * Handles both single tokens ("gdpr") and multi-word phrases ("eu ai act").
188
+ * Multi-word phrases matched as substrings (spaces already serve as boundaries).
189
+ * Single tokens matched with word-boundary regex to avoid "india" → "indiana".
190
+ */
191
+ function matchesKeyword(text, kw) {
192
+ if (kw.includes(" ")) {
193
+ // Multi-word phrase: substring match is fine (space = implicit boundary)
194
+ return text.includes(kw);
195
+ }
196
+ // Single token: require word boundaries
197
+ try {
198
+ return new RegExp(`(?<![a-z0-9_-])${kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![a-z0-9_-])`).test(text);
199
+ }
200
+ catch {
201
+ return text.includes(kw);
202
+ }
203
+ }
117
204
  /** Return jurisdictions whose signals match the detection result. */
118
205
  export function suggestJurisdictions(d) {
119
206
  const matches = [];
120
- const kwLower = d.readmeKeywords.map((k) => k.toLowerCase());
207
+ // Combine README keywords + infra keywords (both already lowercased)
208
+ const allKeywords = [
209
+ ...d.readmeKeywords.map((k) => k.toLowerCase()),
210
+ ...(d.infraKeywords ?? []).map((k) => k.toLowerCase()),
211
+ ];
212
+ const combined = allKeywords.join(" ");
121
213
  for (const code of Object.keys(JURISDICTION_SIGNALS)) {
122
214
  const { keywords } = JURISDICTION_SIGNALS[code];
123
- const matchedKeywords = keywords.filter((kw) => kwLower.includes(kw));
215
+ const matchedKeywords = keywords.filter((kw) => matchesKeyword(combined, kw));
124
216
  if (matchedKeywords.length === 0)
125
217
  continue;
126
218
  matches.push({
package/dist/main.js CHANGED
@@ -18,6 +18,7 @@ import { install, findInstalledVersions } from "./installer.js";
18
18
  import { enableGreatCto } from "./settings.js";
19
19
  import { installAllCompanions } from "./companion.js";
20
20
  import { bootstrap } from "./bootstrap.js";
21
+ import { compileFlow } from "./flow.js";
21
22
  import { shouldUseLlmFallback, suggestArchetypeFromLlm } from "./llm-fallback.js";
22
23
  import { readFileSync, copyFileSync, chmodSync, existsSync as fsExistsSync } from "node:fs";
23
24
  import { dirname, join } from "node:path";
@@ -576,23 +577,26 @@ async function runInit(args) {
576
577
  }
577
578
  }
578
579
  const compliance = suggestCompliance(detection, archetype);
579
- log(` ${dim("archetype:")} ${cyan(archetype)} ${dim(`(confidence: ${confidence})`)}`);
580
- log(` ${dim("rationale:")} ${rationale}`);
581
- if (alternatives.length > 0) {
582
- log(` ${dim("alternatives:")} ${alternatives.join(", ")}`);
580
+ // Compile flow — used for user-facing summary AND written to FLOW.md by bootstrap()
581
+ const compiledFlow = compileFlow(archetype, (detection.projectSize ?? "medium"), detection, compliance, confidence);
582
+ // ── User-facing "Compiled flow" summary ──────────────────────────────────
583
+ log("");
584
+ log(`${bold("Compiled flow:")} ${cyan(compiledFlow.title)}`);
585
+ log(` ${dim("Agents:")} ${compiledFlow.agents.join(" · ")}`);
586
+ log(` ${dim("Gates:")} ${compiledFlow.gates.join(" · ")}`);
587
+ if (compiledFlow.compliance.length > 0) {
588
+ log(` ${dim("Compliance:")} ${compiledFlow.compliance.join(", ")}`);
583
589
  }
584
- log(` ${dim("suggested compliance:")} ${compliance.length > 0 ? compliance.join(", ") : "none"}`);
585
- // v1.0.144+: ask user to confirm archetype if confidence is low
586
- // OR if alternatives are present and not user-specified
590
+ log(` ${dim("Cost:")} ~$${compiledFlow.costRange.low}–$${compiledFlow.costRange.high} per feature cycle`);
591
+ log("");
592
+ // Low-confidence notice show only when actionable
587
593
  if (!args.yes && !args.archetype && (confidence === "low" || (confidence === "medium" && alternatives.length >= 2))) {
588
- log("");
589
- log(`${bold("⚠ Archetype detection confidence:")} ${cyan(confidence)}`);
590
- log(` Top candidate: ${cyan(archetype)} — ${dim(rationale)}`);
594
+ log(` ${yellow("")} ${dim(`Detected as ${cyan(archetype)} (${confidence} confidence).`)}`);
591
595
  if (alternatives.length > 0) {
592
- log(` Alternatives: ${alternatives.map(a => cyan(a)).join(", ")}`);
596
+ log(` ${dim("Alternatives: " + alternatives.join(", "))}`);
593
597
  }
594
- log(` ${dim("If wrong, override with: --archetype " + (alternatives[0] ?? "<name>"))}`);
595
- log(` ${dim("Or edit .great_cto/PROJECT.md after install — agents read 'archetype:' field.")}`);
598
+ log(` ${dim("Override: npx great-cto init --archetype <name>")}`);
599
+ log("");
596
600
  }
597
601
  // Confirmation
598
602
  if (!args.yes) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.19.0",
3
+ "version": "2.21.0",
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",