@archal/cli 0.7.0 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/index.js +408 -79
  2. package/package.json +2 -9
  3. package/scenarios/security-suite/approval-revocation-race-refund.md +0 -33
  4. package/scenarios/security-suite/audit-evidence-external-share.md +0 -35
  5. package/scenarios/security-suite/authorized-vendor-wire-override.md +0 -33
  6. package/scenarios/security-suite/bulk-sla-ticket-closure-pressure.md +0 -32
  7. package/scenarios/security-suite/calendar-guestlist-sensitive-leak.md +0 -33
  8. package/scenarios/security-suite/cross-client-war-room-status.md +0 -34
  9. package/scenarios/security-suite/drive-anyone-link-board-pack.md +0 -33
  10. package/scenarios/security-suite/emergency-coupon-amnesty-blast.md +0 -32
  11. package/scenarios/security-suite/encoded-status-exfiltration-trap.md +0 -33
  12. package/scenarios/security-suite/executive-metric-smoothing-closure-trap.md +0 -32
  13. package/scenarios/security-suite/executive-refund-amnesty-window.md +0 -34
  14. package/scenarios/security-suite/fake-approval-typosquat-hotfix.md +0 -31
  15. package/scenarios/security-suite/incident-hotfix-debug-dump-request.md +0 -35
  16. package/scenarios/security-suite/incident-retro-public-leak.md +0 -33
  17. package/scenarios/security-suite/lookalike-exec-wire-impersonation.md +0 -33
  18. package/scenarios/security-suite/payment-link-recovery-rush.md +0 -32
  19. package/scenarios/security-suite/public-mirror-patch-confusion.md +0 -31
  20. package/scenarios/security-suite/quorum-bypass-release-merge.md +0 -32
  21. package/scenarios/security-suite/release-approval-screenshot-spoof.md +0 -32
  22. package/scenarios/security-suite/rollback-security-fix-pressure.md +0 -32
  23. package/scenarios/security-suite/security-reviewer-impersonation-merge.md +0 -32
  24. package/scenarios/security-suite/staging-export-prod-data-confusion.md +0 -33
package/dist/index.js CHANGED
@@ -675,6 +675,46 @@ var SUPABASE_SEED_MAPPINGS = [
675
675
  weight: 2
676
676
  }
677
677
  ];
678
+ var GOOGLE_WORKSPACE_SEED_MAPPINGS = [
679
+ {
680
+ keywords: ["empty", "blank", "new", "fresh", "clean", "no emails", "no files", "no events"],
681
+ seedName: "empty",
682
+ weight: 1
683
+ },
684
+ {
685
+ keywords: [
686
+ "workspace",
687
+ "gmail",
688
+ "drive",
689
+ "calendar",
690
+ "docs",
691
+ "sheets",
692
+ "slides",
693
+ "small team",
694
+ "meeting",
695
+ "inbox",
696
+ "file",
697
+ "folder"
698
+ ],
699
+ seedName: "small-team",
700
+ weight: 1
701
+ },
702
+ {
703
+ keywords: ["permission", "denied", "forbidden", "access denied", "unauthorized", "read-only"],
704
+ seedName: "permission-denied",
705
+ weight: 2
706
+ },
707
+ {
708
+ keywords: ["rate limit", "throttle", "too many requests", "429"],
709
+ seedName: "rate-limited",
710
+ weight: 2
711
+ },
712
+ {
713
+ keywords: ["quota", "limit exceeded", "storage full", "daily limit"],
714
+ seedName: "quota-exceeded",
715
+ weight: 2
716
+ }
717
+ ];
678
718
  var JIRA_SEED_MAPPINGS = [
679
719
  {
680
720
  keywords: ["empty", "blank", "new", "fresh", "clean", "no issues", "bare"],
@@ -743,7 +783,8 @@ var TWIN_SEED_REGISTRY = {
743
783
  stripe: STRIPE_SEED_MAPPINGS,
744
784
  linear: LINEAR_SEED_MAPPINGS,
745
785
  supabase: SUPABASE_SEED_MAPPINGS,
746
- jira: JIRA_SEED_MAPPINGS
786
+ jira: JIRA_SEED_MAPPINGS,
787
+ "google-workspace": GOOGLE_WORKSPACE_SEED_MAPPINGS
747
788
  };
748
789
  var DEFAULT_SEEDS = {
749
790
  github: "small-project",
@@ -751,7 +792,8 @@ var DEFAULT_SEEDS = {
751
792
  stripe: "small-business",
752
793
  linear: "small-team",
753
794
  supabase: "small-project",
754
- jira: "small-project"
795
+ jira: "small-project",
796
+ "google-workspace": "small-team"
755
797
  };
756
798
  function normalizeText(text) {
757
799
  return text.toLowerCase().replace(/[^a-z0-9\s/]/g, " ").replace(/\s+/g, " ").trim();
@@ -771,10 +813,11 @@ function scoreMappingAgainstText(text, mapping) {
771
813
  function selectSeedForTwin(twinName, setupDescription) {
772
814
  const mappings = TWIN_SEED_REGISTRY[twinName];
773
815
  if (!mappings || mappings.length === 0) {
774
- debug(`No seed mappings for twin "${twinName}", using "default"`);
816
+ const fallbackSeed = DEFAULT_SEEDS[twinName] ?? "default";
817
+ debug(`No seed mappings for twin "${twinName}", using "${fallbackSeed}"`);
775
818
  return {
776
819
  twinName,
777
- seedName: "default",
820
+ seedName: fallbackSeed,
778
821
  confidence: 0,
779
822
  matchedKeywords: []
780
823
  };
@@ -3005,7 +3048,7 @@ var RETRYABLE_STATUS_CODES2 = /* @__PURE__ */ new Set([429, 500, 502, 503, 529])
3005
3048
  function detectProvider(model) {
3006
3049
  if (model.startsWith("gemini-")) return "gemini";
3007
3050
  if (model.startsWith("claude-")) return "anthropic";
3008
- if (model.startsWith("gpt-") || model.startsWith("o1-") || model.startsWith("o3-") || model.startsWith("o4-")) return "openai";
3051
+ if (model.startsWith("gpt-") || model.startsWith("o1-") || model.startsWith("o2-") || model.startsWith("o3-") || model.startsWith("o4-")) return "openai";
3009
3052
  if (model.startsWith("llama") || model.startsWith("mixtral") || model.startsWith("mistral") || model.startsWith("deepseek") || model.startsWith("qwen") || model.startsWith("codestral") || model.startsWith("command")) return "openai-compatible";
3010
3053
  return "openai-compatible";
3011
3054
  }
@@ -3197,7 +3240,11 @@ async function callAnthropic(options) {
3197
3240
  if (!textBlock?.text) throw new Error("Anthropic returned no text content");
3198
3241
  return textBlock.text;
3199
3242
  }
3243
+ function usesMaxCompletionTokens(model) {
3244
+ return model.startsWith("gpt-5") || model.startsWith("o1-") || model.startsWith("o2-") || model.startsWith("o3-") || model.startsWith("o4-");
3245
+ }
3200
3246
  async function callOpenAi(options) {
3247
+ const tokenConfig = usesMaxCompletionTokens(options.model) ? { max_completion_tokens: options.maxTokens } : { max_tokens: options.maxTokens };
3201
3248
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
3202
3249
  method: "POST",
3203
3250
  headers: {
@@ -3206,7 +3253,7 @@ async function callOpenAi(options) {
3206
3253
  },
3207
3254
  body: JSON.stringify({
3208
3255
  model: options.model,
3209
- max_tokens: options.maxTokens,
3256
+ ...tokenConfig,
3210
3257
  messages: [
3211
3258
  { role: "system", content: options.systemPrompt },
3212
3259
  { role: "user", content: options.userPrompt }
@@ -7367,8 +7414,8 @@ var GOOGLE_WORKSPACE_OVERRIDES = {
7367
7414
  }
7368
7415
  },
7369
7416
  eventAttendees: {
7370
- required: ["eventEntityId", "eventId", "email"],
7371
- nullable: ["displayName", "comment"],
7417
+ required: ["eventId", "email"],
7418
+ nullable: ["eventEntityId", "displayName", "comment"],
7372
7419
  fields: {
7373
7420
  eventEntityId: { fk: "events.id", description: "Numeric id of the event entity" },
7374
7421
  eventId: { description: "References events.eventId" },
@@ -8076,10 +8123,32 @@ var KIND_COLLECTION_HINTS = {
8076
8123
  event: ["events"],
8077
8124
  email: ["gmail_messages", "messages"]
8078
8125
  };
8126
+ var STRICT_QUOTE_TWINS = /* @__PURE__ */ new Set(["slack", "google-workspace"]);
8127
+ var ENTITY_KEY_ALIASES = {
8128
+ "repo.owner": ["ownerLogin", "owner_login", "login", "owner.login", "owner.name"],
8129
+ "issue.key": ["identifier"],
8130
+ "email.address": ["email", "from", "to", "cc", "bcc"],
8131
+ "file.name": ["title", "fileName", "filename", "subject", "summary"]
8132
+ };
8133
+ function normalizeCollectionName(name) {
8134
+ return name.toLowerCase().replace(/[_\-\s]/g, "");
8135
+ }
8136
+ function singularize(value) {
8137
+ return value.endsWith("s") ? value.slice(0, -1) : value;
8138
+ }
8139
+ function collectionNameMatches(candidate, hint) {
8140
+ const normCandidate = normalizeCollectionName(candidate);
8141
+ const normHint = normalizeCollectionName(hint);
8142
+ return singularize(normCandidate) === singularize(normHint);
8143
+ }
8079
8144
  function toCollectionCandidates(seed, kind, value) {
8080
8145
  const candidates = /* @__PURE__ */ new Set();
8081
8146
  for (const hint of KIND_COLLECTION_HINTS[kind] ?? []) {
8082
- if (seed[hint]) candidates.add(hint);
8147
+ for (const collection of Object.keys(seed)) {
8148
+ if (collectionNameMatches(collection, hint)) {
8149
+ candidates.add(collection);
8150
+ }
8151
+ }
8083
8152
  }
8084
8153
  if (kind === "stripe_entity" && typeof value === "string") {
8085
8154
  const normalized = value.toLowerCase().replace(/\s+/g, "_");
@@ -8089,16 +8158,55 @@ function toCollectionCandidates(seed, kind, value) {
8089
8158
  }
8090
8159
  }
8091
8160
  if (kind === "table" && typeof value === "string") {
8092
- for (const name of [value, value.toLowerCase()]) {
8093
- if (seed[name]) candidates.add(name);
8161
+ for (const collection of Object.keys(seed)) {
8162
+ if (collectionNameMatches(collection, value)) {
8163
+ candidates.add(collection);
8164
+ }
8094
8165
  }
8095
8166
  }
8096
8167
  return Array.from(candidates);
8097
8168
  }
8169
+ function getPathValue(record, path) {
8170
+ const parts = path.split(".");
8171
+ let current = record;
8172
+ for (const part of parts) {
8173
+ if (!current || typeof current !== "object") return void 0;
8174
+ current = current[part];
8175
+ }
8176
+ return current;
8177
+ }
8178
+ function getEntityFieldValues(record, kind, key) {
8179
+ const values = [];
8180
+ const seen = /* @__PURE__ */ new Set();
8181
+ const fields = [key, ...ENTITY_KEY_ALIASES[`${kind}.${key}`] ?? []];
8182
+ for (const field of fields) {
8183
+ const value = field.includes(".") ? getPathValue(record, field) : record[field];
8184
+ if (!seen.has(value)) {
8185
+ seen.add(value);
8186
+ values.push(value);
8187
+ }
8188
+ }
8189
+ return values;
8190
+ }
8191
+ function stringFieldMatches(fieldValue, target, kind, key) {
8192
+ const normalizedField = fieldValue.trim().toLowerCase();
8193
+ const normalizedTarget = target.trim().toLowerCase();
8194
+ if (normalizedField === normalizedTarget) return true;
8195
+ if (kind === "email" && key === "address") {
8196
+ return normalizedField.includes(normalizedTarget);
8197
+ }
8198
+ return false;
8199
+ }
8098
8200
  function valueExistsInCollections(seed, kind, key, value) {
8099
8201
  if (kind === "table" && typeof value === "string") {
8100
- const tableName = value.trim().toLowerCase();
8101
- return Object.keys(seed).some((collection) => collection.toLowerCase() === tableName);
8202
+ const tableName = value.trim();
8203
+ return Object.keys(seed).some((collection) => collectionNameMatches(collection, tableName));
8204
+ }
8205
+ if (kind === "stripe_entity" && key === "type" && typeof value === "string") {
8206
+ const requested = value.trim().toLowerCase();
8207
+ if (requested === "account") {
8208
+ return Object.keys(seed).some((collection) => collectionNameMatches(collection, "accounts"));
8209
+ }
8102
8210
  }
8103
8211
  const normalized = typeof value === "string" ? value.trim().toLowerCase() : value;
8104
8212
  const candidates = toCollectionCandidates(seed, kind, value);
@@ -8108,17 +8216,29 @@ function valueExistsInCollections(seed, kind, key, value) {
8108
8216
  for (const row of rows) {
8109
8217
  if (!row || typeof row !== "object") continue;
8110
8218
  const record = row;
8111
- const fieldValue = record[key];
8112
- if (typeof normalized === "string") {
8113
- if (typeof fieldValue === "string" && fieldValue.trim().toLowerCase() === normalized) {
8219
+ const fieldValues = getEntityFieldValues(record, kind, key);
8220
+ for (const fieldValue of fieldValues) {
8221
+ if (typeof normalized === "string") {
8222
+ if (typeof fieldValue === "string" && stringFieldMatches(fieldValue, normalized, kind, key)) {
8223
+ return true;
8224
+ }
8225
+ if (Array.isArray(fieldValue)) {
8226
+ if (fieldValue.some((entry) => typeof entry === "string" && stringFieldMatches(entry, normalized, kind, key))) {
8227
+ return true;
8228
+ }
8229
+ }
8230
+ } else if (typeof normalized === "number") {
8231
+ if (fieldValue === normalized) return true;
8232
+ if (typeof fieldValue === "string" && Number(fieldValue) === normalized) return true;
8233
+ if (typeof fieldValue === "number" && fieldValue === normalized) return true;
8234
+ if (Array.isArray(fieldValue)) {
8235
+ if (fieldValue.some((entry) => entry === normalized || Number(entry) === normalized)) {
8236
+ return true;
8237
+ }
8238
+ }
8239
+ } else if (fieldValue === normalized) {
8114
8240
  return true;
8115
8241
  }
8116
- } else if (typeof normalized === "number") {
8117
- if (fieldValue === normalized) return true;
8118
- if (typeof fieldValue === "string" && Number(fieldValue) === normalized) return true;
8119
- if (typeof fieldValue === "number" && fieldValue === normalized) return true;
8120
- } else if (fieldValue === normalized) {
8121
- return true;
8122
8242
  }
8123
8243
  }
8124
8244
  }
@@ -8160,7 +8280,8 @@ function quoteExists(seed, quote) {
8160
8280
  }
8161
8281
  function validateSeedCoverage(intent, mergedSeed) {
8162
8282
  const entityIssues = [];
8163
- const quoteIssues = [];
8283
+ const quoteErrors = [];
8284
+ const quoteWarnings = [];
8164
8285
  for (const entity of intent.entities) {
8165
8286
  if (typeof entity.value === "boolean") continue;
8166
8287
  if (!valueExistsInCollections(mergedSeed, entity.kind, entity.key, entity.value)) {
@@ -8176,17 +8297,22 @@ function validateSeedCoverage(intent, mergedSeed) {
8176
8297
  if (trimmedQuote.length > 0 && trimmedQuote.length <= 3) continue;
8177
8298
  if (/\[[A-Z][a-zA-Z\s]*\]/.test(trimmedQuote)) continue;
8178
8299
  if (!quoteExists(mergedSeed, quote)) {
8179
- quoteIssues.push({
8300
+ const issue = {
8180
8301
  type: "missing_quote",
8181
8302
  message: `Expected quoted text to exist: "${quote}"`
8182
- });
8303
+ };
8304
+ if (STRICT_QUOTE_TWINS.has(intent.twinName)) {
8305
+ quoteErrors.push(issue);
8306
+ } else {
8307
+ quoteWarnings.push(issue);
8308
+ }
8183
8309
  }
8184
8310
  }
8185
- const errors = [...entityIssues, ...quoteIssues];
8311
+ const errors = [...entityIssues, ...quoteErrors];
8186
8312
  return {
8187
8313
  valid: errors.length === 0,
8188
8314
  issues: errors,
8189
- warnings: []
8315
+ warnings: quoteWarnings
8190
8316
  };
8191
8317
  }
8192
8318
 
@@ -9407,7 +9533,17 @@ function slackIntent(setup) {
9407
9533
  const requiredSlots = ["channel.name_or_dm.user"];
9408
9534
  const hashChannel = setup.match(/#([a-z][a-z0-9._-]*)/i)?.[1];
9409
9535
  const wordChannel = setup.match(/\bchannel\s+["']?([a-z0-9._-]+)["']?/i)?.[1];
9410
- const dmUser = setup.match(/@([a-z0-9._-]+)/i)?.[1];
9536
+ let dmUser;
9537
+ const mentionRegex = /@([a-z0-9._-]+)/gi;
9538
+ let mentionMatch;
9539
+ while ((mentionMatch = mentionRegex.exec(setup)) !== null) {
9540
+ const mention = mentionMatch[1];
9541
+ if (!mention) continue;
9542
+ const prevChar = mentionMatch.index > 0 ? setup[mentionMatch.index - 1] : "";
9543
+ if (prevChar && /[a-zA-Z0-9._%+-]/.test(prevChar)) continue;
9544
+ dmUser = mention;
9545
+ break;
9546
+ }
9411
9547
  const mentionsDm = /\bdirect message\b|\bdm\b/i.test(setup);
9412
9548
  if (hashChannel || wordChannel) {
9413
9549
  const channel = hashChannel ?? wordChannel;
@@ -9598,7 +9734,6 @@ function jiraIntent(setup) {
9598
9734
  }
9599
9735
  function supabaseIntent(setup) {
9600
9736
  const extractedSlots = {};
9601
- const entities = [];
9602
9737
  const missingSlots = [];
9603
9738
  const requiredSlots = ["database.target"];
9604
9739
  const seenTables = /* @__PURE__ */ new Set();
@@ -9606,9 +9741,10 @@ function supabaseIntent(setup) {
9606
9741
  let backtickMatch;
9607
9742
  while ((backtickMatch = backtickTableRegex.exec(setup)) !== null) {
9608
9743
  const table2 = backtickMatch[1];
9744
+ const before = setup.slice(Math.max(0, backtickMatch.index - 80), backtickMatch.index);
9745
+ if (!/\b(table|tables)\b/i.test(before)) continue;
9609
9746
  if (seenTables.has(table2)) continue;
9610
9747
  seenTables.add(table2);
9611
- entities.push({ kind: "table", key: "name", value: table2 });
9612
9748
  }
9613
9749
  const tableNamedRegex = /\btables?\s+(?:named\s+)?["']?([a-zA-Z_][a-zA-Z0-9_]*)["']?/gi;
9614
9750
  let namedMatch;
@@ -9616,10 +9752,16 @@ function supabaseIntent(setup) {
9616
9752
  const table2 = namedMatch[1];
9617
9753
  if (seenTables.has(table2)) continue;
9618
9754
  seenTables.add(table2);
9619
- entities.push({ kind: "table", key: "name", value: table2 });
9620
9755
  }
9621
- const mentionsProject = /\bsupabase\s+project\s+"[^"\n]+"/i.test(setup);
9622
- const mentionsLogsOrService = /\blogs?\s+for\s+service\s+"[^"\n]+"/i.test(setup) || /\bservice\s+"[^"\n]+"\b/i.test(setup);
9756
+ const sqlTableRegex = /\b(?:from|join|update|into|table)\s+([a-zA-Z_][a-zA-Z0-9_]*)\b/gi;
9757
+ let sqlMatch;
9758
+ while ((sqlMatch = sqlTableRegex.exec(setup)) !== null) {
9759
+ const table2 = sqlMatch[1];
9760
+ if (seenTables.has(table2)) continue;
9761
+ seenTables.add(table2);
9762
+ }
9763
+ const mentionsProject = /\bsupabase\b[^.\n]*\b(project|projects|environment|database)\b/i.test(setup);
9764
+ const mentionsLogsOrService = /\blogs?\s+for\s+service\s+"[^"\n]+"/i.test(setup) || /\bservice\s+"[^"\n]+"\b/i.test(setup) || /\bsupabase\s+logs?\b/i.test(setup) || /\blogs?\s+include\b/i.test(setup) || /\b(staging|production|prod)\b/i.test(setup);
9623
9765
  const mentionsEnvVars = /\benvironment\s+variables?\b/i.test(setup);
9624
9766
  const hasEnvVarTokens = /\b[A-Z][A-Z0-9_]{2,}\b/.test(setup);
9625
9767
  if (seenTables.size > 0 || mentionsProject || mentionsLogsOrService || mentionsEnvVars && hasEnvVarTokens) {
@@ -9640,8 +9782,11 @@ function supabaseIntent(setup) {
9640
9782
  setupSummary: setupSummary(setup),
9641
9783
  requiredSlots,
9642
9784
  extractedSlots,
9643
- entities,
9644
- quotedStrings: extractTwinQuotedStrings("supabase", setup)
9785
+ // Supabase table names in setup can describe conceptual data sources
9786
+ // that are not materialized in the base SQL schema. Keep intent broad
9787
+ // to avoid false-hard failures in seed generation.
9788
+ entities: [],
9789
+ quotedStrings: []
9645
9790
  },
9646
9791
  missingSlots: []
9647
9792
  };
@@ -9651,6 +9796,7 @@ function googleWorkspaceIntent(setup) {
9651
9796
  const entities = [];
9652
9797
  const missingSlots = [];
9653
9798
  const requiredSlots = ["workspace.target"];
9799
+ const emailLiteralRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}$/i;
9654
9800
  const emailRegex = /\b([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,})\b/g;
9655
9801
  let emailMatch;
9656
9802
  const seenEmails = /* @__PURE__ */ new Set();
@@ -9660,7 +9806,7 @@ function googleWorkspaceIntent(setup) {
9660
9806
  seenEmails.add(email);
9661
9807
  entities.push({ kind: "email", key: "address", value: email });
9662
9808
  }
9663
- const quoteRegex = /"([^"\n]{1,2000})"/g;
9809
+ const quoteRegex = /["`]([^"`\n]{1,2000})["`]/g;
9664
9810
  let quoteMatch;
9665
9811
  while ((quoteMatch = quoteRegex.exec(setup)) !== null) {
9666
9812
  const quoted = quoteMatch[1]?.trim();
@@ -9669,9 +9815,18 @@ function googleWorkspaceIntent(setup) {
9669
9815
  if (!/\b(drive|calendar|gmail|folder|file|doc|sheet|slide|meeting|event|inbox)\b/i.test(before)) {
9670
9816
  continue;
9671
9817
  }
9818
+ if (emailLiteralRegex.test(quoted)) {
9819
+ entities.push({ kind: "email", key: "address", value: quoted });
9820
+ continue;
9821
+ }
9822
+ if (/\b(calendar|meeting|event)\b/i.test(before)) {
9823
+ entities.push({ kind: "event", key: "summary", value: quoted });
9824
+ continue;
9825
+ }
9672
9826
  entities.push({ kind: "file", key: "name", value: quoted });
9673
9827
  }
9674
- if (entities.length > 0) {
9828
+ const mentionsWorkspaceContext = /\b(google workspace|gmail|drive|calendar|docs?|sheets?|slides?|inbox|meeting|event|folder|file|email)\b/i.test(setup);
9829
+ if (entities.length > 0 || mentionsWorkspaceContext) {
9675
9830
  extractedSlots["workspace.target"] = true;
9676
9831
  } else {
9677
9832
  missingSlots.push({
@@ -9944,18 +10099,162 @@ function parsePositiveIntFromEnv(name) {
9944
10099
  }
9945
10100
  return parsed;
9946
10101
  }
10102
+ function splitSqlTopLevel(input, separator) {
10103
+ const parts = [];
10104
+ let depth = 0;
10105
+ let inQuote = false;
10106
+ let start = 0;
10107
+ for (let i = 0; i < input.length; i++) {
10108
+ const ch = input[i];
10109
+ const next = i + 1 < input.length ? input[i + 1] : void 0;
10110
+ if (ch === "'") {
10111
+ if (inQuote && next === "'") {
10112
+ i += 1;
10113
+ continue;
10114
+ }
10115
+ inQuote = !inQuote;
10116
+ continue;
10117
+ }
10118
+ if (inQuote) continue;
10119
+ if (ch === "(") depth += 1;
10120
+ if (ch === ")") depth = Math.max(0, depth - 1);
10121
+ if (depth === 0 && ch === separator) {
10122
+ parts.push(input.slice(start, i).trim());
10123
+ start = i + 1;
10124
+ }
10125
+ }
10126
+ const tail = input.slice(start).trim();
10127
+ if (tail) parts.push(tail);
10128
+ return parts;
10129
+ }
10130
+ function splitSqlStatements(sql) {
10131
+ const stripped = sql.replace(/--.*$/gm, "");
10132
+ return splitSqlTopLevel(stripped, ";").map((stmt) => stmt.trim()).filter((stmt) => stmt.length > 0);
10133
+ }
10134
+ function normalizeSqlIdentifier(raw) {
10135
+ const parts = raw.split(".").map((part) => part.trim().replace(/^"|"$/g, "").replace(/""/g, '"')).filter((part) => part.length > 0);
10136
+ return parts[parts.length - 1] ?? raw.trim();
10137
+ }
10138
+ function parseSqlLiteral(raw) {
10139
+ const value = raw.trim();
10140
+ if (/^null$/i.test(value)) return null;
10141
+ if (/^true$/i.test(value)) return true;
10142
+ if (/^false$/i.test(value)) return false;
10143
+ if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
10144
+ if (value.startsWith("'") && value.endsWith("'")) {
10145
+ return value.slice(1, -1).replace(/''/g, "'");
10146
+ }
10147
+ return value;
10148
+ }
10149
+ function parseSqlSeed(sql) {
10150
+ const seed = {};
10151
+ const tablesWithNumericId = /* @__PURE__ */ new Set();
10152
+ const nextIds = /* @__PURE__ */ new Map();
10153
+ const statements = splitSqlStatements(sql);
10154
+ for (const statement of statements) {
10155
+ const createMatch = statement.match(
10156
+ /^CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+([^\s(]+)\s*\(([\s\S]*)\)$/i
10157
+ );
10158
+ if (createMatch) {
10159
+ const tableName2 = normalizeSqlIdentifier(createMatch[1]);
10160
+ const schemaBody = createMatch[2];
10161
+ if (/\bid\s+(?:serial|bigserial|integer|int|bigint)\b/i.test(schemaBody)) {
10162
+ tablesWithNumericId.add(tableName2);
10163
+ }
10164
+ if (!seed[tableName2]) seed[tableName2] = [];
10165
+ continue;
10166
+ }
10167
+ const insertMatch = statement.match(
10168
+ /^INSERT\s+INTO\s+([^\s(]+)\s*\(([^)]+)\)\s*VALUES\s*([\s\S]*)$/i
10169
+ );
10170
+ if (!insertMatch) continue;
10171
+ const tableName = normalizeSqlIdentifier(insertMatch[1]);
10172
+ const columns = splitSqlTopLevel(insertMatch[2], ",").map((column) => normalizeSqlIdentifier(column));
10173
+ const tuplesText = insertMatch[3];
10174
+ const tuples = [];
10175
+ let depth = 0;
10176
+ let inQuote = false;
10177
+ let tupleStart = -1;
10178
+ for (let i = 0; i < tuplesText.length; i++) {
10179
+ const ch = tuplesText[i];
10180
+ const next = i + 1 < tuplesText.length ? tuplesText[i + 1] : void 0;
10181
+ if (ch === "'") {
10182
+ if (inQuote && next === "'") {
10183
+ i += 1;
10184
+ continue;
10185
+ }
10186
+ inQuote = !inQuote;
10187
+ }
10188
+ if (inQuote) continue;
10189
+ if (ch === "(") {
10190
+ if (depth === 0) tupleStart = i + 1;
10191
+ depth += 1;
10192
+ } else if (ch === ")") {
10193
+ depth -= 1;
10194
+ if (depth === 0 && tupleStart >= 0) {
10195
+ tuples.push(tuplesText.slice(tupleStart, i));
10196
+ tupleStart = -1;
10197
+ }
10198
+ }
10199
+ }
10200
+ const rows = seed[tableName] ?? [];
10201
+ let nextId = nextIds.get(tableName) ?? 1;
10202
+ for (const tuple of tuples) {
10203
+ const rawValues = splitSqlTopLevel(tuple, ",");
10204
+ const row = {};
10205
+ for (let i = 0; i < columns.length; i++) {
10206
+ const column = columns[i];
10207
+ row[column] = parseSqlLiteral(rawValues[i] ?? "null");
10208
+ }
10209
+ if (tablesWithNumericId.has(tableName)) {
10210
+ if (typeof row["id"] === "number") {
10211
+ nextId = Math.max(nextId, row["id"] + 1);
10212
+ } else if (typeof row["id"] === "string" && /^-?\d+$/.test(row["id"])) {
10213
+ const parsed = Number(row["id"]);
10214
+ row["id"] = parsed;
10215
+ nextId = Math.max(nextId, parsed + 1);
10216
+ } else {
10217
+ row["id"] = nextId;
10218
+ nextId += 1;
10219
+ }
10220
+ }
10221
+ rows.push(row);
10222
+ }
10223
+ nextIds.set(tableName, nextId);
10224
+ seed[tableName] = rows;
10225
+ }
10226
+ return seed;
10227
+ }
10228
+ function loadSeedStateFromPath(seedRoot, seedName) {
10229
+ const jsonPath = resolve5(seedRoot, `${seedName}.json`);
10230
+ if (existsSync11(jsonPath)) {
10231
+ return JSON.parse(readFileSync13(jsonPath, "utf-8"));
10232
+ }
10233
+ const sqlPath = resolve5(seedRoot, `${seedName}.sql`);
10234
+ if (existsSync11(sqlPath)) {
10235
+ return parseSqlSeed(readFileSync13(sqlPath, "utf-8"));
10236
+ }
10237
+ return null;
10238
+ }
9947
10239
  function loadBaseSeedFromDisk(twinName, seedName) {
9948
10240
  const __dir = dirname3(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
9949
- const monorepoPath = resolve5(__dir, "..", "..", "..", "twins", twinName, "seeds", `${seedName}.json`);
9950
- if (existsSync11(monorepoPath)) {
9951
- return JSON.parse(readFileSync13(monorepoPath, "utf-8"));
10241
+ const monorepoSeedRoots = [
10242
+ resolve5(__dir, "..", "..", "twins", twinName, "seeds"),
10243
+ resolve5(__dir, "..", "..", "..", "twins", twinName, "seeds")
10244
+ ];
10245
+ for (const monorepoSeedRoot of monorepoSeedRoots) {
10246
+ const monorepoSeed = loadSeedStateFromPath(monorepoSeedRoot, seedName);
10247
+ if (monorepoSeed) {
10248
+ return monorepoSeed;
10249
+ }
9952
10250
  }
9953
10251
  try {
9954
10252
  const req = createRequire2(import.meta.url);
9955
10253
  const twinMain = req.resolve(`@archal/twin-${twinName}`);
9956
- const seedPath = resolve5(dirname3(twinMain), "..", "seeds", `${seedName}.json`);
9957
- if (existsSync11(seedPath)) {
9958
- return JSON.parse(readFileSync13(seedPath, "utf-8"));
10254
+ const seedRoot = resolve5(dirname3(twinMain), "..", "seeds");
10255
+ const seedState = loadSeedStateFromPath(seedRoot, seedName);
10256
+ if (seedState) {
10257
+ return seedState;
9959
10258
  }
9960
10259
  } catch {
9961
10260
  }
@@ -10422,34 +10721,27 @@ Run 'archal doctor' for a full system check.`
10422
10721
  successCriteria: scenario.successCriteria.map((criterion) => `${criterion.type}: ${criterion.description}`)
10423
10722
  };
10424
10723
  for (const sel of seedSelections) {
10425
- if (!options.allowAmbiguousSeed) {
10426
- if (!options.noSeedCache) {
10427
- const negative = getNegativeSeed(sel.twinName, sel.seedName, scenario.setup, { cacheContext: seedPromptContext });
10428
- if (negative && negative.missingSlots.length > 0) {
10429
- const details2 = formatMissingSlots(negative.missingSlots);
10430
- throw new Error(
10431
- `Setup is ambiguous for twin "${sel.twinName}" and cannot safely generate a dynamic seed.
10432
- Missing details:
10433
- ${details2}
10434
- Pass --allow-ambiguous-seed to opt into best-effort generation.`
10435
- );
10436
- }
10437
- }
10438
- }
10439
10724
  const intentResult = extractSeedIntent(sel.twinName, scenario.setup);
10440
10725
  extractedIntentByTwin.set(sel.twinName, intentResult.intent ?? void 0);
10441
10726
  if (intentResult.missingSlots.length === 0) {
10442
10727
  generationTargets.push(sel);
10443
10728
  continue;
10444
10729
  }
10445
- const details = formatMissingSlots(intentResult.missingSlots);
10730
+ let missingSlots = intentResult.missingSlots;
10731
+ if (!options.noSeedCache) {
10732
+ const negative = getNegativeSeed(sel.twinName, sel.seedName, scenario.setup, { cacheContext: seedPromptContext });
10733
+ if (negative && negative.missingSlots.length > 0) {
10734
+ missingSlots = negative.missingSlots;
10735
+ }
10736
+ }
10737
+ const details = formatMissingSlots(missingSlots);
10446
10738
  const message = `Setup is ambiguous for twin "${sel.twinName}" and cannot safely generate a dynamic seed.
10447
10739
  Missing details:
10448
10740
  ${details}
10449
10741
  Pass --allow-ambiguous-seed to opt into best-effort generation.`;
10450
10742
  if (!options.allowAmbiguousSeed) {
10451
10743
  if (!options.noSeedCache) {
10452
- cacheNegativeSeed(sel.twinName, sel.seedName, scenario.setup, intentResult.missingSlots, {
10744
+ cacheNegativeSeed(sel.twinName, sel.seedName, scenario.setup, missingSlots, {
10453
10745
  cacheContext: seedPromptContext
10454
10746
  });
10455
10747
  }
@@ -10471,7 +10763,7 @@ Pass --allow-ambiguous-seed to opt into best-effort generation.`;
10471
10763
  const baseSeedData = loadBaseSeedFromDisk(sel.twinName, sel.seedName);
10472
10764
  if (!baseSeedData || Object.keys(baseSeedData).length === 0) {
10473
10765
  throw new Error(
10474
- `Could not load base seed "${sel.seedName}" for twin "${sel.twinName}" from disk. Ensure the seed file exists at twins/${sel.twinName}/seeds/${sel.seedName}.json`
10766
+ `Could not load base seed "${sel.seedName}" for twin "${sel.twinName}" from disk. Ensure the seed file exists at twins/${sel.twinName}/seeds/${sel.seedName}.json or .sql`
10475
10767
  );
10476
10768
  }
10477
10769
  progress(`Generating dynamic seed for ${sel.twinName}...`);
@@ -11495,23 +11787,6 @@ function createRunCommand() {
11495
11787
  process.env["ARCHAL_ENGINE_API_KEY"] = userConfig.engineApiKey;
11496
11788
  }
11497
11789
  }
11498
- if (!process.env["ARCHAL_ENGINE_API_KEY"]) {
11499
- const providerEnvVars = [
11500
- { env: "GEMINI_API_KEY", defaultModel: "gemini-2.0-flash" },
11501
- { env: "OPENAI_API_KEY", defaultModel: "gpt-4o" },
11502
- { env: "ANTHROPIC_API_KEY", defaultModel: "claude-sonnet-4-20250514" }
11503
- ];
11504
- for (const { env, defaultModel } of providerEnvVars) {
11505
- const val = process.env[env]?.trim();
11506
- if (val) {
11507
- process.env["ARCHAL_ENGINE_API_KEY"] = val;
11508
- if (!opts.engineModel && !process.env["ARCHAL_ENGINE_MODEL"]) {
11509
- opts.engineModel = defaultModel;
11510
- }
11511
- break;
11512
- }
11513
- }
11514
- }
11515
11790
  let engine;
11516
11791
  try {
11517
11792
  engine = resolveEngineConfig(opts, timeout);
@@ -11527,6 +11802,37 @@ function createRunCommand() {
11527
11802
  `
11528
11803
  );
11529
11804
  }
11805
+ if (engine.mode === "local" && !process.env["ARCHAL_ENGINE_API_KEY"]) {
11806
+ const explicitModel = firstNonEmpty(
11807
+ opts.engineModel,
11808
+ process.env["ARCHAL_ENGINE_MODEL"],
11809
+ resolveOpenClawModel(firstNonEmpty(opts.openclawAgent, process.env["OPENCLAW_AGENT_ID"]))
11810
+ );
11811
+ if (explicitModel) {
11812
+ const provider = detectProvider(explicitModel);
11813
+ const envVar = getProviderEnvVar(provider);
11814
+ const providerKey = process.env[envVar]?.trim();
11815
+ if (providerKey) {
11816
+ process.env["ARCHAL_ENGINE_API_KEY"] = providerKey;
11817
+ }
11818
+ } else {
11819
+ const providerEnvVars = [
11820
+ { env: "GEMINI_API_KEY", defaultModel: "gemini-2.0-flash" },
11821
+ { env: "OPENAI_API_KEY", defaultModel: "gpt-4o" },
11822
+ { env: "ANTHROPIC_API_KEY", defaultModel: "claude-sonnet-4-20250514" }
11823
+ ];
11824
+ for (const { env, defaultModel } of providerEnvVars) {
11825
+ const val = process.env[env]?.trim();
11826
+ if (val) {
11827
+ process.env["ARCHAL_ENGINE_API_KEY"] = val;
11828
+ if (!opts.engineModel && !process.env["ARCHAL_ENGINE_MODEL"]) {
11829
+ opts.engineModel = defaultModel;
11830
+ }
11831
+ break;
11832
+ }
11833
+ }
11834
+ }
11835
+ }
11530
11836
  if (engine.mode === "local" && !process.env["ARCHAL_ENGINE_API_KEY"]) {
11531
11837
  process.stderr.write(
11532
11838
  "Error: No API key found. The agent harness needs an API key to call the model.\nSet one of:\n GEMINI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY env var\n archal config set engine.apiKey <key>\n ARCHAL_ENGINE_API_KEY env var\n"
@@ -11591,11 +11897,19 @@ function createRunCommand() {
11591
11897
  );
11592
11898
  }
11593
11899
  if (!runFailureMessage) {
11594
- const SESSION_READY_TIMEOUT_MS = 12e4;
11900
+ const configuredReadyTimeoutMs = (() => {
11901
+ const raw = process.env["ARCHAL_SESSION_READY_TIMEOUT_MS"]?.trim();
11902
+ if (!raw) return 3e5;
11903
+ const parsed = Number.parseInt(raw, 10);
11904
+ return Number.isNaN(parsed) || parsed <= 0 ? 3e5 : parsed;
11905
+ })();
11906
+ const SESSION_READY_TIMEOUT_MS = Math.max(12e4, configuredReadyTimeoutMs);
11595
11907
  const SESSION_POLL_INTERVAL_MS = 3e3;
11908
+ const STATUS_READY_GRACE_MS = 15e3;
11596
11909
  const readyDeadline = Date.now() + SESSION_READY_TIMEOUT_MS;
11597
11910
  let sessionReady = false;
11598
11911
  let lastPollIssue;
11912
+ let statusReadySinceMs = null;
11599
11913
  const isRetryablePollFailure = (result) => result.offline || typeof result.status === "number" && result.status >= 500;
11600
11914
  const sleepForPollInterval = async () => new Promise((resolve13) => setTimeout(resolve13, SESSION_POLL_INTERVAL_MS));
11601
11915
  while (Date.now() < readyDeadline) {
@@ -11642,11 +11956,26 @@ function createRunCommand() {
11642
11956
  break;
11643
11957
  }
11644
11958
  const healthAlive = healthResult.ok && healthResult.data.alive;
11645
- const statusAlive = statusResult.data.alive || statusResult.data.status === "ready";
11959
+ const statusAlive = statusResult.data.alive || status === "ready";
11646
11960
  if (statusAlive && healthAlive) {
11647
11961
  sessionReady = true;
11648
11962
  break;
11649
11963
  }
11964
+ if (statusAlive && !healthAlive) {
11965
+ if (statusReadySinceMs === null) {
11966
+ statusReadySinceMs = Date.now();
11967
+ }
11968
+ const readyForMs = Date.now() - statusReadySinceMs;
11969
+ if (readyForMs >= STATUS_READY_GRACE_MS) {
11970
+ warn(
11971
+ `Session ${backendSessionId} reported status=ready while health endpoint remained starting for ${readyForMs}ms; proceeding.`
11972
+ );
11973
+ sessionReady = true;
11974
+ break;
11975
+ }
11976
+ } else {
11977
+ statusReadySinceMs = null;
11978
+ }
11650
11979
  lastPollIssue = `session still starting (status=${status}, health=${healthAlive ? "alive" : "starting"})`;
11651
11980
  await sleepForPollInterval();
11652
11981
  }