great-cto 2.73.1 → 2.75.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/adapt.js CHANGED
@@ -160,9 +160,6 @@ using it for pattern-matched work silently skips specialist review.
160
160
  | Play Store, App Store, iOS/Android release | \`mobile-store-reviewer\` |
161
161
  | API contract: OpenAPI, GraphQL schema, webhook signatures | \`api-platform-reviewer\` |
162
162
  | Voice/IVR/telephony, Twilio, recording-consent, TCPA | \`voice-ai-reviewer\` |
163
- | Clinical / SaMD / FDA / FHIR / PHI | \`ai-clinical-reviewer\`, \`fda-reviewer\` |
164
- | Wearable telemetry, HealthKit, Health Connect, Garmin, Samsung Health, fitness AI, mental health AI, nutrition AI, supplement AI, physician HITL | \`digital-health-reviewer\` |
165
- | Lending, ECOA, FCRA, NMLS, adverse action | \`lending-credit-reviewer\` |
166
163
  | HR-AI, hiring, AEDT, resume screening | \`hr-ai-reviewer\` |
167
164
  | Infra-as-code: Terraform / Helm / CDK / Pulumi | \`infra-reviewer\` |
168
165
  | Performance regression, hot path, p99 budgets | \`performance-engineer\` |
@@ -1100,9 +1100,8 @@ export function pickArchetype(d) {
1100
1100
  };
1101
1101
  }
1102
1102
  /** Niche packs that should surface even at high confidence */
1103
- function isNichePack(pack) {
1104
- return ["robotics-pack", "climate-pack", "drug-discovery-pack",
1105
- "clinical-trials-pack", "em-fintech-pack"].includes(pack);
1103
+ function isNichePack(_pack) {
1104
+ return false;
1106
1105
  }
1107
1106
  /**
1108
1107
  * Infer likely domain packs from README/infra keywords when the archetype
@@ -1112,22 +1111,10 @@ function inferPackHints(d) {
1112
1111
  const hints = [];
1113
1112
  const kws = new Set([...d.readmeKeywords, ...(d.infraKeywords ?? [])]);
1114
1113
  const has = (...terms) => terms.some((t) => kws.has(t));
1115
- if (has("robot", "ros2", "ros 2", "cobot", "drone", "uav"))
1116
- hints.push("robotics-pack");
1117
- if (has("carbon", "ghg", "mrv", "emission", "verra", "sbti"))
1118
- hints.push("climate-pack");
1119
- if (has("clinical", "ctms", "edc", "cdisc", "randomization", "irb"))
1120
- hints.push("clinical-trials-pack");
1121
- if (has("drug discovery", "binding affinity", "admet", "chembl", "alphafold"))
1122
- hints.push("drug-discovery-pack");
1123
1114
  if (has("recruit", "hiring", "candidate", "ats", "aedt"))
1124
1115
  hints.push("hr-ai-pack");
1125
- if (has("loan", "lending", "bnpl", "underwrit", "fcra"))
1126
- hints.push("lending-pack");
1127
1116
  if (has("voice", "telephony", "ivr", "stt", "tts", "outbound call"))
1128
1117
  hints.push("voice-pack");
1129
- if (has("india", "upi", "rbi", "mpesa", "gcash", "pix", "cross-border", "remittance"))
1130
- hints.push("em-fintech-pack");
1131
1118
  if (has("public api", "api key", "developer portal", "openapi"))
1132
1119
  hints.push("api-platform-pack");
1133
1120
  return hints;
@@ -1331,6 +1318,13 @@ export const REVIEWERS_BY_ARCHETYPE = {
1331
1318
  "gov-public": ["gov-reviewer", "security-officer"],
1332
1319
  "insurance": ["insurance-reviewer", "regulated-reviewer"],
1333
1320
  "defense-govcon": ["cmmc-reviewer", "gov-reviewer", "security-officer"],
1321
+ // Product Builder archetypes (A1–A6) — lean reviewer sets; payments → pci.
1322
+ "vertical-saas": ["security-officer"],
1323
+ "booking": ["pci-reviewer", "security-officer"],
1324
+ "crm": ["security-officer"],
1325
+ "dashboard": ["security-officer"],
1326
+ "content-platform": ["pci-reviewer", "security-officer"],
1327
+ "marketplace-lite": ["pci-reviewer", "security-officer"],
1334
1328
  "greenfield": [],
1335
1329
  };
1336
1330
  /**
@@ -1367,6 +1361,14 @@ export const GATES_BY_ARCHETYPE = {
1367
1361
  "gov-public": ["plan", "qa", "gov-review", "security", "ship", "compliance"],
1368
1362
  "insurance": ["plan", "qa", "insurance-review", "security", "ship", "compliance"],
1369
1363
  "defense-govcon": ["plan", "qa", "cmmc-assessment", "security", "ship", "compliance"],
1364
+ // Product Builder archetypes (A1–A6) — non-regulated baseline; the single CTO gate
1365
+ // emerges at change_tier T1 (effectiveGates → [plan]).
1366
+ "vertical-saas": ["plan", "qa", "ship"],
1367
+ "booking": ["plan", "qa", "ship"],
1368
+ "crm": ["plan", "qa", "ship"],
1369
+ "dashboard": ["plan", "qa", "ship"],
1370
+ "content-platform": ["plan", "qa", "ship"],
1371
+ "marketplace-lite": ["plan", "qa", "ship"],
1370
1372
  "greenfield": ["plan"],
1371
1373
  };
1372
1374
  /**
@@ -1386,6 +1388,46 @@ export function gatesFor(archetype, size) {
1386
1388
  // large + enterprise → ensure compliance is included
1387
1389
  return all.includes("compliance") ? all : [...all, "compliance"];
1388
1390
  }
1391
+ /** Gates that form the non-negotiable floor for a *regulated* archetype. */
1392
+ const FLOOR_GATES = ["security", "compliance", "ship"];
1393
+ /**
1394
+ * The human gates the pipeline opens for a change, given its archetype, the
1395
+ * project size, AND the per-change risk tier. Composes with `gatesFor()`:
1396
+ *
1397
+ * - T2 (default): the full size baseline, with `ship` guaranteed even if a
1398
+ * small project_size had stripped it. An irreversible change always gets a
1399
+ * final go/no-go. Nothing is ever removed → fail-safe for an unknown tier.
1400
+ * - T1: only `plan` (you review intent) PLUS the regulated floor. The standalone
1401
+ * qa/security-as-ceremony is dropped because CI + tests cover a reversible change.
1402
+ * - T0: the regulated floor only — for a plain archetype that is the empty set
1403
+ * (CI is the gate); for a regulated one, security+compliance+ship still fire.
1404
+ *
1405
+ * An archetype is "regulated" iff its baseline contains `security` or `compliance`.
1406
+ * That is what keeps the downgrade sound: a fintech repo cannot ship an
1407
+ * irreversible change unreviewed even when the change is labelled a fix.
1408
+ *
1409
+ * NOTE: change-specific T2 triggers (a migration, a new write-connector, a prod
1410
+ * deploy) are resolved upstream by the classifier, which escalates such a change
1411
+ * to T2 before this function sees it. Here we only map an already-decided tier.
1412
+ */
1413
+ export function effectiveGates(archetype, size, tier = "T2") {
1414
+ const base = gatesFor(archetype, size);
1415
+ // T2 (and any unrecognized tier) → full baseline, never downgraded, ship forced.
1416
+ if (tier !== "T0" && tier !== "T1") {
1417
+ return base.includes("ship") ? base : [...base, "ship"];
1418
+ }
1419
+ // Regulated archetypes carry a floor that survives every downgrade.
1420
+ const isRegulated = base.includes("security") || base.includes("compliance");
1421
+ const floor = isRegulated ? base.filter((g) => FLOOR_GATES.includes(g)) : [];
1422
+ if (tier === "T1") {
1423
+ const keep = new Set(floor);
1424
+ if (base.includes("plan"))
1425
+ keep.add("plan");
1426
+ return base.filter((g) => keep.has(g)); // preserve base order
1427
+ }
1428
+ // T0 — maintenance.
1429
+ return floor;
1430
+ }
1389
1431
  /**
1390
1432
  * Returns the ordered list of reviewers for an archetype. Empty for
1391
1433
  * `greenfield`. Used by the orchestrator to spawn the right
@@ -1394,3 +1436,26 @@ export function gatesFor(archetype, size) {
1394
1436
  export function reviewersFor(archetype) {
1395
1437
  return REVIEWERS_BY_ARCHETYPE[archetype] ?? [];
1396
1438
  }
1439
+ /**
1440
+ * Horizontal builder agents (the "obвязка") wired into the build pipeline per
1441
+ * Product-Builder archetype: integrations + migration + billing are universal (every
1442
+ * SMB product integrates, imports from an incumbent, and is a paid product —
1443
+ * subscription-billing-engineer self-scopes to one-off when there's no subscription).
1444
+ * connector-builder adds to dashboards (ingest), media-pipeline-engineer to content
1445
+ * platforms. geo-routing-engineer and mobile-app-builder are signal-gated in
1446
+ * compileFlow (routing/maps signals; mobile/field signals), not archetype-static.
1447
+ */
1448
+ const SMB_BUILD_LAYER = ["app-scaffolder", "auth-engineer"];
1449
+ const SMB_OBVYAZKA = ["integrations-engineer", "migration-import-engineer", "subscription-billing-engineer"];
1450
+ export const BUILDERS_BY_ARCHETYPE = {
1451
+ "vertical-saas": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA],
1452
+ "booking": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA],
1453
+ "crm": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA],
1454
+ "dashboard": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA, "connector-builder"],
1455
+ "content-platform": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA, "media-pipeline-engineer"],
1456
+ "marketplace-lite": [...SMB_BUILD_LAYER, ...SMB_OBVYAZKA],
1457
+ };
1458
+ /** Builder agents for an archetype (the obвязка layer). Empty for non-SMB archetypes. */
1459
+ export function buildersFor(archetype) {
1460
+ return BUILDERS_BY_ARCHETYPE[archetype] ?? [];
1461
+ }
package/dist/detect.js CHANGED
@@ -1129,63 +1129,13 @@ function mineReadmeKeywords(dir) {
1129
1129
  // voice-pack
1130
1130
  "voice", "telephony", "ivr", "tts", "stt", "speech-to-text", "text-to-speech",
1131
1131
  "outbound call", "inbound call", "voice agent",
1132
- // clinical-pack + clinical-trials-pack
1133
- "clinical", "patient", "ehr", "emr", "phi", "diagnosis", "diagnos", "triage",
1134
- "radiolog", "patholog", "samd", "scribe", "telehealth-ai", "medical record",
1135
- "cds", "clinical decision support",
1136
- "clinical trial", "ctms", "edc", "epro", "econsent", "esource",
1137
- "randomization", "rtsm", "irt", "decentralized trial", "ind submission",
1138
- "21 cfr 11", "cdisc", "sdtm", "adam", "irb",
1139
1132
  // hr-ai-pack
1140
1133
  "recruit", "hiring", "candidate", "resume", "interview", "ats", "talent acquisition",
1141
1134
  "performance review", "workforce scheduling", "employee evaluation", "aedt",
1142
1135
  // api-platform-pack
1143
1136
  "public api", "partner api", "developer portal", "api key", "webhook", "sdk",
1144
1137
  "rest api", "graphql api", "openapi",
1145
- // lending-pack
1146
- "loan", "lending", "credit decision", "underwrit", "bnpl", "buy now pay later",
1147
- "buy-now-pay-later", "payroll advance", "ewa", "line of credit", "fico",
1148
- "credit score", "fcra", "nmls", "financing", "adverse action",
1149
- // robotics-pack
1150
- "robot", "cobot", "manipulator", "end-effector", "amr", "agv", "autonomous mobile",
1151
- "surgical robot", "ros 2", "ros2", "drone", "uav",
1152
- // em-fintech-pack
1153
- "india", "nigeria", "brazil", "indonesia", "philippines", "mexico", "kenya",
1154
- "m-pesa", "mpesa", "upi", "pix", "gcash", "ovo", "dana", "rbi", "cbn", "bsp",
1155
- "ojk", "mas", "bcb", "condusef", "cross-border", "remittance", "local rails",
1156
- // climate-pack
1157
- "carbon", "emission", "ghg", "mrv", "scope 1", "scope 2", "scope 3", "verra",
1158
- "gold standard", "puro", "sbti", "cdp", "csrd", "cbam", "ghgrp", "offset",
1159
- "credit retir", "removal", "biogenic",
1160
- "dna synthesis", "gene synthesis", "oligonucleotide", "protein design",
1161
- "esm", "alphafold", "rfdiffusion", "pathogen", "select agent", "gain-of-function",
1162
- "dual-use", "bsl-3", "bsl-4", "biocontainment", "bwc", "p3co", "igsc", "cloud lab",
1163
- // drug-discovery-pack
1164
- "drug discovery", "binding affinity", "admet", "toxicity prediction",
1165
- "generative chem", "generative protein", "antibody design", "mrna design",
1166
- "virtual screening", "docking", "fep", "chembl", "bindingdb", "pdbbind",
1167
- "glp", "gmp", "gxp", "preclinical", "lims", "eln", "annex 11", "alcoa",
1168
- "lab automation", "robotic biology", "liquid handler", "hamilton", "tecan",
1169
- "beckman", "opentrons", "plate reader", "sequencer", "hplc", "mass spec", "sila",
1170
- // digital-health-pack (Wave 4) — keep in sync with packs.ts SIGNALS.keywords
1171
- "wearable", "apple watch", "apple health", "healthkit", "health connect",
1172
- "garmin", "samsung health", "google fit", "fitbit", "heart rate", "hrv",
1173
- "heart rate variability", "spo2", "sleep tracking", "sleep stages",
1174
- "biometric sensor", "stress score", "activity tracking", "ecg wearable",
1175
- "mental health", "mental wellness", "wellbeing", "mindfulness ai",
1176
- "stress detection", "burnout detection", "mood tracking", "anxiety ai",
1177
- "depression ai", "phq-9", "gad-7", "digital therapeutics", "dtx",
1178
- "cbt app", "dbt app", "therapy ai", "personalised training",
1179
- "personalized training", "fitness ai", "nutrition ai",
1180
- "supplement recommendation", "supplement ai", "diet ai", "meal plan ai",
1181
- "macro ai", "physician review", "physician hitl", "doctor in the loop",
1182
- "clinical review workflow", "remote patient monitoring", "rpm", "teleconsultation",
1183
1138
  // US-market packs — keep in sync with packs.ts SIGNALS.keywords
1184
- // sec-cyber-pack
1185
- "public company", "publicly traded", "10-k", "10k", "8-k", "8k", "s-1", "ipo",
1186
- "sec filing", "investor relations", "material incident", "materiality",
1187
- "incident response", "incident disclosure", "breach notification", "siem",
1188
- "security operations", "circia", "cisa reporting",
1189
1139
  // adtech-privacy-pack
1190
1140
  "meta pixel", "facebook pixel", "fbevents", "conversions api", "capi",
1191
1141
  "google analytics", "ga4", "google tag manager", "tiktok pixel", "ad pixel",
package/dist/flow.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // flow.ts — compiles all detection outputs into a single user-facing FlowResult.
2
2
  // Pure function: no I/O, no side effects.
3
3
  // Called by bootstrap.ts (writes FLOW.md) and main.ts (prints summary).
4
- import { reviewersFor, gatesFor } from "./archetypes.js";
4
+ import { reviewersFor, gatesFor, buildersFor } from "./archetypes.js";
5
5
  import { suggestPackReviewers, suggestPackGates, suggestPacks } from "./packs.js";
6
6
  import { suggestJurisdictions, suggestJurisdictionReviewers, suggestJurisdictionGates } from "./jurisdictions.js";
7
7
  // ── Human-readable titles ─────────────────────────────────────────────────
@@ -31,6 +31,12 @@ const ARCHETYPE_TITLE = {
31
31
  "gov-public": "Government",
32
32
  "insurance": "Insurance",
33
33
  "regulated": "Regulated system",
34
+ "vertical-saas": "Vertical SaaS",
35
+ "booking": "Booking / scheduling",
36
+ "crm": "CRM",
37
+ "dashboard": "Dashboard / analytics",
38
+ "content-platform": "Content / media platform",
39
+ "marketplace-lite": "Marketplace",
34
40
  "greenfield": "New project",
35
41
  };
36
42
  // Gate id (StandardGate) → user label
@@ -76,8 +82,29 @@ const ARCHETYPE_COST = {
76
82
  "infra": [3, 8],
77
83
  "library": [0.5, 3],
78
84
  "cli-tool": [0.5, 3],
85
+ // Product Builder archetypes — cheap, high-automation builds.
86
+ "vertical-saas": [3, 8],
87
+ "booking": [3, 8],
88
+ "crm": [3, 8],
89
+ "dashboard": [3, 8],
90
+ "content-platform": [3, 8],
91
+ "marketplace-lite": [3, 8],
79
92
  "greenfield": [0.5, 3],
80
93
  };
94
+ /**
95
+ * Archetypes that ship a user-facing interface and therefore get a
96
+ * `design-advisor` stage (plan-altitude, before pm). Backend/infra/library/CLI
97
+ * archetypes have no UI surface and skip it. design-advisor itself skips a
98
+ * change classified T0 at runtime (see scripts/lib/change-tier.mjs).
99
+ */
100
+ export const UI_BEARING_ARCHETYPES = new Set([
101
+ "web-service", "mobile-app", "commerce", "marketplace", "cms",
102
+ "enterprise-saas", "edtech", "game", "browser-extension",
103
+ "healthcare", "fintech", "insurance", "gov-public", "web3",
104
+ // Product Builder archetypes (A1–A6) — all ship a UI.
105
+ "vertical-saas", "booking", "crm", "dashboard", "content-platform",
106
+ "marketplace-lite",
107
+ ]);
81
108
  // ── Main export ───────────────────────────────────────────────────────────
82
109
  /**
83
110
  * Compile all detection outputs into a single FlowResult.
@@ -96,6 +123,23 @@ export function compileFlow(archetype, size, detection, compliance, confidence)
96
123
  agentSet.add("architect");
97
124
  agentSet.add("senior-dev");
98
125
  agentSet.add("qa-engineer");
126
+ // Design-advisor for UI-bearing archetypes (plan-altitude, before pm).
127
+ if (UI_BEARING_ARCHETYPES.has(archetype))
128
+ agentSet.add("design-advisor");
129
+ // Horizontal builder agents (the obвязка) for Product-Builder archetypes.
130
+ for (const b of buildersFor(archetype))
131
+ agentSet.add(b);
132
+ // Signal-gated specialist builders — only when the product actually needs them.
133
+ const kw = new Set(detection.readmeKeywords.map((k) => k.toLowerCase()));
134
+ const ROUTING_SIGNALS = ["routing", "route", "dispatch", "delivery", "fleet", "logistics", "maps", "geocode"];
135
+ if ((archetype === "booking" || archetype === "vertical-saas") &&
136
+ ROUTING_SIGNALS.some((s) => kw.has(s))) {
137
+ agentSet.add("geo-routing-engineer");
138
+ }
139
+ const MOBILE_SIGNALS = ["mobile", "ios", "android", "react-native", "field", "offline", "technician", "crew"];
140
+ if (UI_BEARING_ARCHETYPES.has(archetype) && MOBILE_SIGNALS.some((s) => kw.has(s))) {
141
+ agentSet.add("mobile-app-builder");
142
+ }
99
143
  // ── Gates ────────────────────────────────────────────────────────────────
100
144
  const gateSet = new Set(gatesFor(archetype, size).map((g) => GATE_LABEL[g] ?? `gate:${g}`));
101
145
  for (const g of suggestPackGates(detection))
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Three-axis detection model:
5
5
  // archetype → what you build (ai-system, healthcare, fintech…)
6
- // pack → what you use (digital-health-pack, lending-pack…)
6
+ // pack → what you use (…)
7
7
  // jurisdiction → where you operate (eu, us-ca, in, br, uk, au, sg)
8
8
  //
9
9
  // Jurisdiction overlays add specialist reviewer agents and human gates
package/dist/land.js ADDED
@@ -0,0 +1,60 @@
1
+ // ZRS land-step — the ctx merge-queue borrow: "agent finished" != "change landed".
2
+ // Two gates: (1) apply-clean rebase onto the LIVE target, (2) combined-verify on the rebased
3
+ // result. Advance the target with ff-only ONLY when both pass. Runners are injected so the gates
4
+ // are unit-testable without a real repo (see tests/task-queue.test.mjs).
5
+ import { spawnSync } from "node:child_process";
6
+ export const realRunners = {
7
+ git(cwd, args) {
8
+ const r = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8" });
9
+ return { code: r.status ?? 1, out: (r.stdout || "") + (r.stderr || "") };
10
+ },
11
+ verify(cmd, cwd) {
12
+ if (!cmd)
13
+ return 0; // no verify configured → Gate 2 is a no-op (still green)
14
+ const r = spawnSync("sh", ["-c", cmd], { cwd, stdio: "inherit" });
15
+ return r.status ?? 1;
16
+ },
17
+ };
18
+ /**
19
+ * Land `branch` (built in `worktree`) onto `task.target`. Returns the terminal status.
20
+ * needs-revision = Gate 1 conflict (rebase failed) — bounce back for human re-add / agent rebase
21
+ * failed = Gate 2 red (verify non-zero)
22
+ * landed = clean ∧ green, fast-forwarded onto target
23
+ */
24
+ export function land(task, branch, worktree, runners = realRunners) {
25
+ const { git, verify } = runners;
26
+ const target = task.target;
27
+ // ── Gate 1: rebase the work onto the live target (it may have moved under us) ──
28
+ const reb = git(worktree, ["rebase", target]);
29
+ if (reb.code !== 0) {
30
+ git(worktree, ["rebase", "--abort"]);
31
+ return { status: "needs-revision", gate1: "conflict", reason: `rebase conflict onto ${target}` };
32
+ }
33
+ // ── Gate 2: verify the REBASED (combined) result — A green + B green can still be A+B red ──
34
+ if (verify(task.verify, worktree) !== 0) {
35
+ return { status: "failed", gate1: "clean", gate2: "red", reason: "verify failed on rebased result" };
36
+ }
37
+ // ── Land: advance target with ff-only (never force, never clobber a concurrent commit) ──
38
+ if (git(task.repo, ["checkout", target]).code !== 0) {
39
+ return { status: "failed", gate1: "clean", gate2: "green", reason: `checkout ${target} failed` };
40
+ }
41
+ let merged = git(task.repo, ["merge", "--ff-only", branch]);
42
+ if (merged.code !== 0) {
43
+ // target moved during verify → re-rebase once, re-verify, retry ff
44
+ const re = git(worktree, ["rebase", target]);
45
+ if (re.code !== 0) {
46
+ git(worktree, ["rebase", "--abort"]);
47
+ return { status: "needs-revision", gate1: "conflict", gate2: "green", reason: "target moved + conflict" };
48
+ }
49
+ if (verify(task.verify, worktree) !== 0) {
50
+ return { status: "failed", gate1: "clean", gate2: "red", reason: "verify red after re-rebase" };
51
+ }
52
+ git(task.repo, ["checkout", target]);
53
+ merged = git(task.repo, ["merge", "--ff-only", branch]);
54
+ if (merged.code !== 0) {
55
+ return { status: "needs-revision", gate1: "clean", gate2: "green", reason: "still not fast-forward after re-rebase" };
56
+ }
57
+ }
58
+ const sha = git(task.repo, ["rev-parse", "--short", "HEAD"]).out.trim();
59
+ return { status: "landed", gate1: "clean", gate2: "green", commit: sha };
60
+ }
package/dist/main.js CHANGED
@@ -118,6 +118,16 @@ function parseArgs(argv) {
118
118
  args.command = "report";
119
119
  else if (a === "upgrade")
120
120
  args.command = "upgrade";
121
+ else if (a === "task") {
122
+ args.command = "task";
123
+ args.taskArgs = argv.slice(i + 1);
124
+ break;
125
+ }
126
+ else if (a === "worker") {
127
+ args.command = "worker";
128
+ args.taskArgs = argv.slice(i + 1);
129
+ break;
130
+ }
121
131
  // Slash-commands surfaced as CLI subcommands so users get a clear hint
122
132
  // instead of a confusing usage error. These work only in the chat plugin.
123
133
  else if (a === "start" || a === "audit" || a === "inbox" || a === "digest" ||
@@ -1154,6 +1164,14 @@ async function main() {
1154
1164
  process.exit(2);
1155
1165
  }
1156
1166
  }
1167
+ if (args.command === "task") {
1168
+ const { runTask } = await import("./worker.js");
1169
+ process.exit(await runTask(args.taskArgs ?? []));
1170
+ }
1171
+ if (args.command === "worker") {
1172
+ const { runWorker } = await import("./worker.js");
1173
+ process.exit(await runWorker(args.taskArgs ?? []));
1174
+ }
1157
1175
  if (args.command === "serve") {
1158
1176
  try {
1159
1177
  const { runServe } = await import("./serve.js");
package/dist/packs.js CHANGED
@@ -11,33 +11,15 @@
11
11
  // skills/great_cto/packs/*-pack.md.
12
12
  const PACK_REVIEWERS = {
13
13
  "voice-pack": ["voice-ai-reviewer"],
14
- "clinical-pack": ["ai-clinical-reviewer", "fda-reviewer"],
15
14
  "hr-ai-pack": ["hr-ai-reviewer"],
16
15
  "api-platform-pack": ["api-platform-reviewer"],
17
- "lending-pack": ["lending-credit-reviewer"],
18
- "clinical-trials-pack": ["clinical-trials-reviewer", "bio-data-reviewer"],
19
- "robotics-pack": ["robotics-safety-reviewer"],
20
- "em-fintech-pack": ["emerging-markets-fintech-reviewer"],
21
- "climate-pack": ["climate-mrv-reviewer", "biosecurity-reviewer"],
22
- "drug-discovery-pack": ["drug-discovery-ml-reviewer", "glp-glab-reviewer", "lab-automation-reviewer"],
23
- "digital-health-pack": ["digital-health-reviewer", "ai-clinical-reviewer", "healthcare-reviewer"],
24
- "sec-cyber-pack": ["sec-cyber-disclosure-reviewer"],
25
16
  "adtech-privacy-pack": ["adtech-privacy-reviewer", "us-privacy-reviewer"],
26
17
  "us-ai-pack": ["us-ai-reviewer"],
27
18
  };
28
19
  const PACK_GATES = {
29
20
  "voice-pack": ["gate:voice-compliance"],
30
- "clinical-pack": ["gate:samd-class", "gate:clinical-validation", "gate:ide-approval"],
31
21
  "hr-ai-pack": ["gate:aedt-audit"],
32
22
  "api-platform-pack": ["gate:api-contract"],
33
- "lending-pack": ["gate:fair-lending"],
34
- "clinical-trials-pack": ["gate:irb-ready", "gate:part11-validation", "gate:deidentification"],
35
- "robotics-pack": ["gate:hara-signoff", "gate:functional-safety-test"],
36
- "em-fintech-pack": ["gate:license-strategy"],
37
- "climate-pack": ["gate:mrv-methodology", "gate:durc-signoff", "gate:open-weights-release"],
38
- "drug-discovery-pack": ["gate:model-card-signoff", "gate:csv-validation", "gate:iq-oq-pq"],
39
- "digital-health-pack": ["gate:wellness-vs-samd", "gate:hitl-design", "gate:wearable-api-access", "gate:supplement-safety", "gate:mental-health-protocol"],
40
- "sec-cyber-pack": ["gate:cyber-disclosure-readiness"],
41
23
  "adtech-privacy-pack": ["gate:tracking-consent"],
42
24
  "us-ai-pack": ["gate:ai-governance"],
43
25
  };
@@ -47,10 +29,6 @@ const SIGNALS = {
47
29
  stack: ["twilio", "vonage", "livekit", "deepgram", "elevenlabs", "whisper", "hume"],
48
30
  keywords: ["voice", "telephony", "ivr", "call-center", "tts", "stt", "speech-to-text", "text-to-speech", "phone", "outbound call", "inbound call", "voice agent"],
49
31
  },
50
- "clinical-pack": {
51
- stack: ["fhir", "hl7"],
52
- keywords: ["clinical", "patient", "ehr", "emr", "phi", "hipaa", "diagnos", "triage", "radiolog", "patholog", "samd", "scribe", "telehealth-ai", "medical record", "cds", "clinical decision support"],
53
- },
54
32
  "hr-ai-pack": {
55
33
  stack: ["greenhouse", "lever", "ashby", "workday"],
56
34
  // "candidate" alone is too generic (SaMD/predicate-candidate clash); use compound terms.
@@ -60,67 +38,6 @@ const SIGNALS = {
60
38
  stack: ["openapi", "graphql", "grpc", "fastify", "trpc"],
61
39
  keywords: ["public api", "partner api", "developer portal", "api key", "webhook", "sdk", "rest api", "graphql api", "openapi"],
62
40
  },
63
- "lending-pack": {
64
- stack: ["plaid"],
65
- // "ecoa" removed: collides with clinical "eCOA" (Electronic Clinical Outcome Assessment).
66
- // Lending-specific signals (loan, fcra, nmls, underwrit, fico, ...) are unambiguous.
67
- keywords: ["loan", "lending", "credit decision", "underwrit", "bnpl", "buy now pay later", "buy-now-pay-later", "payroll advance", "ewa", "line of credit", "fico", "credit score", "fcra", "nmls", "financing", "adverse action"],
68
- },
69
- "clinical-trials-pack": {
70
- stack: ["fhir", "hl7", "dicom", "redcap"],
71
- // "ecoa" removed: ambiguous with lending ECOA (Reg B). Other clinical-trial
72
- // signals (ctms, edc, epro, econsent, cdisc, irb, ...) are unambiguous.
73
- keywords: ["clinical trial", "ctms", "edc", "epro", "econsent", "esource", "randomization", "rtsm", "irt", "decentralized trial", "ind submission", "21 cfr 11", "cdisc", "sdtm", "adam", "irb"],
74
- },
75
- "robotics-pack": {
76
- stack: ["ros", "ros2", "moveit", "gazebo", "px4"],
77
- keywords: ["robot", "cobot", "manipulator", "end-effector", "amr", "agv", "autonomous mobile", "surgical robot", "ros 2", "drone", "uav"],
78
- },
79
- "em-fintech-pack": {
80
- stack: [],
81
- keywords: ["india", "nigeria", "brazil", "indonesia", "philippines", "mexico", "kenya", "m-pesa", "mpesa", "upi", "pix", "gcash", "ovo", "dana", "rbi", "cbn", "bsp", "ojk", "mas", "bcb", "condusef", "cross-border", "remittance", "local rails"],
82
- },
83
- "climate-pack": {
84
- stack: [],
85
- keywords: ["carbon", "emission", "ghg", "mrv", "scope 1", "scope 2", "scope 3", "verra", "gold standard", "puro", "sbti", "cdp", "csrd", "cbam", "ghgrp", "offset", "credit retir", "removal", "biogenic",
86
- // biosec triggers
87
- "dna synthesis", "gene synthesis", "oligonucleotide", "protein design", "esm", "alphafold", "rfdiffusion", "pathogen", "select agent", "gain-of-function", "dual-use", "bsl-3", "bsl-4", "biocontainment", "bwc", "p3co", "igsc", "cloud lab"],
88
- },
89
- "drug-discovery-pack": {
90
- stack: ["rdkit"],
91
- keywords: ["drug discovery", "binding affinity", "admet", "toxicity prediction", "generative chem", "generative protein", "antibody design", "mrna design", "virtual screening", "docking", "fep", "alphafold", "rfdiffusion", "chembl", "bindingdb", "pdbbind",
92
- // GLP / lab-automation triggers
93
- "glp", "gmp", "gxp", "preclinical", "lims", "eln", "annex 11", "alcoa",
94
- "lab automation", "cloud lab", "robotic biology", "liquid handler", "hamilton", "tecan", "beckman", "opentrons", "plate reader", "sequencer", "hplc", "mass spec", "sila"],
95
- },
96
- "digital-health-pack": {
97
- stack: ["healthkit", "health-connect", "garmin-connect-iq", "samsung-health", "fitbit", "polar", "withings", "oura", "whoop"],
98
- keywords: [
99
- // wearable / biometric
100
- "wearable", "apple watch", "apple health", "healthkit", "health connect", "garmin", "samsung health",
101
- "google fit", "fitbit", "heart rate", "hrv", "heart rate variability", "spo2", "sleep tracking",
102
- "sleep stages", "biometric sensor", "stress score", "activity tracking", "ecg wearable",
103
- // mental health / wellness AI
104
- "mental health", "mental wellness", "wellbeing", "mindfulness ai", "stress detection",
105
- "burnout detection", "mood tracking", "anxiety ai", "depression ai", "phq-9", "gad-7",
106
- "digital therapeutics", "dtx", "cbt app", "dbt app", "therapy ai",
107
- // fitness / nutrition AI
108
- "personalised training", "personalized training", "fitness ai", "nutrition ai",
109
- "supplement recommendation", "supplement ai", "diet ai", "meal plan ai", "macro ai",
110
- // HITL clinical
111
- "physician review", "physician hitl", "doctor in the loop", "clinical review workflow",
112
- "remote patient monitoring", "rpm", "teleconsultation",
113
- ],
114
- },
115
- "sec-cyber-pack": {
116
- stack: ["pagerduty", "opsgenie", "statuspage", "splunk", "datadog-siem", "sentinel", "wazuh"],
117
- keywords: [
118
- "public company", "publicly traded", "10-k", "10k", "8-k", "8k", "s-1", "ipo",
119
- "sec filing", "investor relations", "material incident", "materiality",
120
- "incident response", "incident disclosure", "breach notification", "siem",
121
- "security operations", "soc 2 incident", "circia", "cisa reporting",
122
- ],
123
- },
124
41
  "adtech-privacy-pack": {
125
42
  stack: ["fbevents", "facebook-pixel", "meta-pixel", "gtag", "ga4", "google-tag-manager", "gtm", "tiktok-pixel", "fullstory", "hotjar", "logrocket", "mouseflow", "smartlook"],
126
43
  keywords: [
@@ -0,0 +1,83 @@
1
+ // ZRS bounded task-runner — queue + worker-owned state (see docs/strategy/ZRS-TASK-RUNNER-DESIGN.md).
2
+ // Producer appends to task-queue.jsonl; the single worker owns task-state.json (atomic rewrite).
3
+ // State lives under ~/.great_cto/ (override with GREAT_CTO_HOME) so a VPS worker is self-contained.
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, renameSync } from "node:fs";
7
+ const ROOT = process.env.GREAT_CTO_HOME || join(homedir(), ".great_cto");
8
+ export const QUEUE = join(ROOT, "task-queue.jsonl");
9
+ export const STATE = join(ROOT, "task-state.json");
10
+ const TERMINAL = new Set(["landed", "failed", "needs-revision", "cancelled"]);
11
+ export function isTerminal(s) { return TERMINAL.has(s); }
12
+ function ensureDir() { mkdirSync(ROOT, { recursive: true }); }
13
+ export function newId(now = Date.now()) {
14
+ return `t_${Math.floor(now / 1000)}_${Math.random().toString(36).slice(2, 6)}`;
15
+ }
16
+ export function appendTask(t) {
17
+ ensureDir();
18
+ const task = {
19
+ id: t.id ?? newId(),
20
+ created_at: t.created_at ?? new Date().toISOString(),
21
+ repo: t.repo,
22
+ target: t.target || "main",
23
+ prompt: t.prompt,
24
+ model: t.model || "sonnet",
25
+ verify: t.verify ?? "",
26
+ timeout_s: t.timeout_s || 3600,
27
+ priority: t.priority || 0,
28
+ };
29
+ appendFileSync(QUEUE, JSON.stringify(task) + "\n");
30
+ return task;
31
+ }
32
+ export function readQueue() {
33
+ if (!existsSync(QUEUE))
34
+ return [];
35
+ return readFileSync(QUEUE, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
36
+ }
37
+ export function readState() {
38
+ if (!existsSync(STATE))
39
+ return {};
40
+ try {
41
+ return JSON.parse(readFileSync(STATE, "utf8"));
42
+ }
43
+ catch {
44
+ return {};
45
+ }
46
+ }
47
+ export function writeState(s) {
48
+ ensureDir();
49
+ const tmp = STATE + ".tmp";
50
+ writeFileSync(tmp, JSON.stringify(s, null, 2));
51
+ renameSync(tmp, STATE); // atomic on the same fs
52
+ }
53
+ export function setState(id, patch) {
54
+ const s = readState();
55
+ const prev = s[id] ?? { status: "queued", attempts: 0 };
56
+ s[id] = { ...prev, ...patch };
57
+ writeState(s);
58
+ return s[id];
59
+ }
60
+ /** Highest-priority queued task with no (or queued) state. One worker → no claim race beyond this. */
61
+ export function nextRunnable() {
62
+ const st = readState();
63
+ const cand = readQueue().filter((t) => {
64
+ const s = st[t.id];
65
+ return !s || s.status === "queued";
66
+ });
67
+ cand.sort((a, b) => (b.priority - a.priority) || a.created_at.localeCompare(b.created_at));
68
+ return cand[0] ?? null;
69
+ }
70
+ /** On worker boot, any non-terminal task is a crash remnant → fail it so a restart never double-runs. */
71
+ export function recoverCrashed() {
72
+ const s = readState();
73
+ let n = 0;
74
+ for (const id of Object.keys(s)) {
75
+ if (!isTerminal(s[id].status)) {
76
+ s[id] = { ...s[id], status: "failed", reason: "interrupted", ended_at: new Date().toISOString() };
77
+ n++;
78
+ }
79
+ }
80
+ if (n)
81
+ writeState(s);
82
+ return n;
83
+ }
package/dist/telemetry.js CHANGED
@@ -141,13 +141,15 @@ async function send(evt) {
141
141
  catch { /* best-effort */ }
142
142
  }
143
143
  // --- Public API ------------------------------------------------------------
144
- /** First-run/install ping. Sent only when enabled. Idempotent across runs. */
144
+ /** First-run/install ping. Sent only when enabled. Idempotent across runs.
145
+ * Distinct `command: "install"` (was "init", which conflated installs with the
146
+ * init subcommand) so lifetime installs are counted separately from init runs. */
145
147
  export async function sendInstallPing(opts) {
146
148
  if (!opts.consent)
147
149
  return;
148
150
  if (!isTelemetryEnabled())
149
151
  return;
150
- const evt = sanitize({ cliVersion: opts.cliVersion, command: "init", archetype: opts.archetype });
152
+ const evt = sanitize({ cliVersion: opts.cliVersion, command: "install", archetype: opts.archetype });
151
153
  if (!evt)
152
154
  return;
153
155
  await send(evt);
package/dist/worker.js ADDED
@@ -0,0 +1,172 @@
1
+ // ZRS worker — single persistent worker: poll queue → one agent in an isolated worktree → land-step.
2
+ // CLI: `great-cto task add|ls|status|cancel` and `great-cto worker`. See docs/strategy/ZRS-TASK-RUNNER-DESIGN.md.
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import { createServer } from "node:http";
5
+ import { existsSync, mkdirSync, openSync, writeFileSync, unlinkSync } from "node:fs";
6
+ import { homedir, hostname } from "node:os";
7
+ import { join, resolve } from "node:path";
8
+ import { appendTask, readQueue, readState, setState, nextRunnable, recoverCrashed, isTerminal, } from "./task-queue.js";
9
+ import { land } from "./land.js";
10
+ const ROOT = process.env.GREAT_CTO_HOME || join(homedir(), ".great_cto");
11
+ const LOGDIR = join(ROOT, "worker-logs");
12
+ const LOCK = join(ROOT, "worker.lock");
13
+ const flagOf = (rest) => (name, def = "") => {
14
+ const i = rest.indexOf("--" + name);
15
+ return i >= 0 ? (rest[i + 1] ?? def) : def;
16
+ };
17
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
18
+ // ── CLI: great-cto task <add|ls|status|cancel> ──────────────────────────────
19
+ export async function runTask(rest) {
20
+ const action = rest[0];
21
+ const flag = flagOf(rest);
22
+ if (action === "add") {
23
+ const prompt = rest.slice(1).find((a) => !a.startsWith("--"));
24
+ if (!prompt) {
25
+ console.error('usage: great-cto task add "<prompt>" [--repo .] [--target main] [--model sonnet] [--verify "npm test"] [--timeout 3600]');
26
+ return 1;
27
+ }
28
+ const t = appendTask({
29
+ repo: resolve(flag("repo", process.cwd())),
30
+ target: flag("target", "main"),
31
+ prompt,
32
+ model: flag("model", "sonnet"),
33
+ verify: flag("verify", ""),
34
+ timeout_s: parseInt(flag("timeout", "3600"), 10),
35
+ priority: parseInt(flag("priority", "0"), 10),
36
+ });
37
+ console.log("queued " + t.id);
38
+ return 0;
39
+ }
40
+ if (action === "ls") {
41
+ const st = readState();
42
+ for (const t of readQueue()) {
43
+ const s = st[t.id];
44
+ console.log(`${t.id} ${(s?.status ?? "queued").padEnd(14)} ${t.prompt.slice(0, 60)}`);
45
+ }
46
+ return 0;
47
+ }
48
+ if (action === "status") {
49
+ const s = readState()[rest[1] ?? ""];
50
+ console.log(s ? JSON.stringify(s, null, 2) : "unknown task");
51
+ return s ? 0 : 1;
52
+ }
53
+ if (action === "cancel") {
54
+ const id = rest[1] ?? "";
55
+ const s = readState()[id];
56
+ if (s && !isTerminal(s.status)) {
57
+ setState(id, { status: "cancelled", ended_at: new Date().toISOString() });
58
+ console.log("cancelled " + id);
59
+ return 0;
60
+ }
61
+ console.log("not cancellable");
62
+ return 1;
63
+ }
64
+ console.error("usage: great-cto task <add|ls|status|cancel>");
65
+ return 1;
66
+ }
67
+ // ── CLI: great-cto worker ───────────────────────────────────────────────────
68
+ export async function runWorker(rest) {
69
+ const flag = flagOf(rest);
70
+ const poll = parseInt(flag("poll", "10"), 10) * 1000;
71
+ const port = parseInt(flag("status-port", "4319"), 10);
72
+ const once = rest.includes("--once");
73
+ mkdirSync(LOGDIR, { recursive: true });
74
+ if (existsSync(LOCK)) {
75
+ console.error(`worker.lock present — another worker is running (or stale: rm ${LOCK})`);
76
+ return 1;
77
+ }
78
+ writeFileSync(LOCK, String(process.pid));
79
+ const cleanup = () => { try {
80
+ unlinkSync(LOCK);
81
+ }
82
+ catch { /* ignore */ } };
83
+ process.on("exit", cleanup);
84
+ process.on("SIGINT", () => { cleanup(); process.exit(0); });
85
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
86
+ recoverCrashed();
87
+ let current = null;
88
+ const srv = createServer((req, res) => {
89
+ if (req.url === "/healthz") {
90
+ res.end("ok");
91
+ return;
92
+ }
93
+ const st = readState();
94
+ const counts = {};
95
+ for (const v of Object.values(st))
96
+ counts[v.status] = (counts[v.status] ?? 0) + 1;
97
+ res.setHeader("content-type", "application/json");
98
+ res.end(JSON.stringify({ host: hostname(), worker_pid: process.pid, current, counts, tasks: st }, null, 2));
99
+ });
100
+ srv.listen(port, "127.0.0.1");
101
+ console.log(`great-cto worker up · status http://127.0.0.1:${port}/status · poll ${poll / 1000}s${once ? " · once" : ""}`);
102
+ for (;;) {
103
+ const t = nextRunnable();
104
+ if (!t) {
105
+ if (once)
106
+ break;
107
+ await sleep(poll);
108
+ continue;
109
+ }
110
+ current = t.id;
111
+ const branch = `zrs/${t.id}`;
112
+ const wt = join(t.repo, ".worktrees", t.id);
113
+ setState(t.id, { status: "claimed", branch, worktree: wt, attempts: (readState()[t.id]?.attempts ?? 0) + 1 });
114
+ const wtAdd = spawnSync("git", ["-C", t.repo, "worktree", "add", wt, "-b", branch, t.target], { encoding: "utf8" });
115
+ if (wtAdd.status !== 0) {
116
+ setState(t.id, { status: "failed", reason: "worktree add failed: " + (wtAdd.stderr || "").trim(), ended_at: new Date().toISOString() });
117
+ current = null;
118
+ if (once)
119
+ break;
120
+ continue;
121
+ }
122
+ setState(t.id, { status: "running", started_at: new Date().toISOString() });
123
+ const log = join(LOGDIR, `${t.id}.log`);
124
+ const code = await runAgent(t, wt, log);
125
+ if (code !== 0) {
126
+ setState(t.id, { status: "failed", reason: "agent exit " + code, ended_at: new Date().toISOString(), log });
127
+ cleanupWorktree(t.repo, wt);
128
+ current = null;
129
+ if (once)
130
+ break;
131
+ continue;
132
+ }
133
+ setState(t.id, { status: "landing", log });
134
+ const result = land(t, branch, wt);
135
+ setState(t.id, { ...result, ended_at: new Date().toISOString(), log });
136
+ cleanupWorktree(t.repo, wt);
137
+ current = null;
138
+ if (once)
139
+ break;
140
+ }
141
+ srv.close();
142
+ cleanup();
143
+ return 0;
144
+ }
145
+ function cleanupWorktree(repo, wt) {
146
+ spawnSync("git", ["-C", repo, "worktree", "remove", wt, "--force"]);
147
+ }
148
+ function runAgent(t, wt, log) {
149
+ return new Promise((res) => {
150
+ const out = openSync(log, "a");
151
+ // GREAT_CTO_AGENT_CMD overrides the spawn (CI / dry-run with a fake agent). PROMPT is exported.
152
+ const override = process.env.GREAT_CTO_AGENT_CMD;
153
+ const child = override
154
+ ? spawn("sh", ["-c", override], { cwd: wt, stdio: ["ignore", out, out], env: { ...process.env, PROMPT: t.prompt } })
155
+ : spawn("claude", ["-p", t.prompt, "--permission-mode", "acceptEdits", "--model", t.model], {
156
+ cwd: wt,
157
+ stdio: ["ignore", out, out],
158
+ });
159
+ const timer = setTimeout(() => {
160
+ try {
161
+ child.kill("SIGTERM");
162
+ }
163
+ catch { /* ignore */ }
164
+ setTimeout(() => { try {
165
+ child.kill("SIGKILL");
166
+ }
167
+ catch { /* ignore */ } }, 5000);
168
+ }, t.timeout_s * 1000);
169
+ child.on("exit", (c) => { clearTimeout(timer); res(c ?? 1); });
170
+ child.on("error", () => { clearTimeout(timer); res(127); });
171
+ });
172
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.73.1",
3
+ "version": "2.75.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",