@mestreyoda/fabrica 0.1.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.
Files changed (45) hide show
  1. package/ARCHITECTURE.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +289 -0
  4. package/defaults/AGENTS.md +150 -0
  5. package/defaults/HEARTBEAT.md +3 -0
  6. package/defaults/IDENTITY.md +6 -0
  7. package/defaults/SOUL.md +39 -0
  8. package/defaults/TOOLS.md +15 -0
  9. package/defaults/fabrica/prompts/architect.md +147 -0
  10. package/defaults/fabrica/prompts/developer.md +211 -0
  11. package/defaults/fabrica/prompts/reviewer.md +114 -0
  12. package/defaults/fabrica/prompts/security-checklist.md +58 -0
  13. package/defaults/fabrica/prompts/tester.md +150 -0
  14. package/defaults/fabrica/workflow.yaml +184 -0
  15. package/dist/index.js +143075 -0
  16. package/dist/index.js.map +7 -0
  17. package/dist/lib/worker.cjs +214 -0
  18. package/dist/worker.cjs +4754 -0
  19. package/fabrica.manifest.json +24 -0
  20. package/genesis/configs/classification-rules.json +32 -0
  21. package/genesis/configs/interview-templates.json +73 -0
  22. package/genesis/configs/labels.json +202 -0
  23. package/genesis/configs/triage-matrix.json +39 -0
  24. package/genesis/scripts/classify-idea.sh +161 -0
  25. package/genesis/scripts/conduct-interview.sh +199 -0
  26. package/genesis/scripts/create-task.sh +797 -0
  27. package/genesis/scripts/delivery-target-lib.sh +88 -0
  28. package/genesis/scripts/generate-qa-contract.sh +188 -0
  29. package/genesis/scripts/generate-spec.sh +171 -0
  30. package/genesis/scripts/genesis-telemetry.sh +97 -0
  31. package/genesis/scripts/genesis-utils.sh +617 -0
  32. package/genesis/scripts/impact-analysis.sh +135 -0
  33. package/genesis/scripts/interview.sh +98 -0
  34. package/genesis/scripts/map-project.sh +309 -0
  35. package/genesis/scripts/receive-idea.sh +69 -0
  36. package/genesis/scripts/register-project.sh +520 -0
  37. package/genesis/scripts/research-idea.sh +84 -0
  38. package/genesis/scripts/scaffold-project.sh +1396 -0
  39. package/genesis/scripts/security-review.sh +141 -0
  40. package/genesis/scripts/sideband-lib.sh +243 -0
  41. package/genesis/scripts/stack-detection-lib.sh +130 -0
  42. package/genesis/scripts/triage.sh +598 -0
  43. package/genesis/scripts/validate-step.sh +81 -0
  44. package/openclaw.plugin.json +45 -0
  45. package/package.json +60 -0
@@ -0,0 +1,617 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # genesis-utils.sh — Shared utility functions for Genesis pipeline
4
+ # Extracted from sideband-lib.sh to allow lighter imports.
5
+ # sideband-lib.sh sources this file for backward compatibility.
6
+ # shellcheck shell=bash
7
+
8
+ if [ -z "${BASH_VERSION:-}" ]; then
9
+ echo "ERROR: genesis-utils.sh requires bash." >&2
10
+ return 1 2>/dev/null || exit 1
11
+ fi
12
+
13
+ # Guard against double-sourcing
14
+ [[ -n "${_GENESIS_UTILS_LOADED:-}" ]] && return 0
15
+ _GENESIS_UTILS_LOADED=1
16
+
17
+ set -euo pipefail
18
+
19
+ # === Category A: Stateless Parsing & Validation ===
20
+
21
+ genesis_trim() {
22
+ local value="${1:-}"
23
+ value="${value#"${value%%[![:space:]]*}"}"
24
+ value="${value%"${value##*[![:space:]]}"}"
25
+ printf '%s\n' "$value"
26
+ }
27
+
28
+ genesis_load_env_file() {
29
+ local env_file="${1:-$HOME/.openclaw/.env}"
30
+ local line key value
31
+
32
+ [[ -f "$env_file" ]] || return 0
33
+
34
+ while IFS= read -r line || [[ -n "$line" ]]; do
35
+ line="${line%$'\r'}"
36
+ line="$(genesis_trim "$line")"
37
+ [[ -z "$line" || "$line" == \#* ]] && continue
38
+
39
+ if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=[[:space:]]*(.*)$ ]]; then
40
+ key="${BASH_REMATCH[1]}"
41
+ value="${BASH_REMATCH[2]}"
42
+ value="${value%$'\r'}"
43
+ if [[ "$value" == \"*\" && "$value" == *\" && "${#value}" -ge 2 ]]; then
44
+ value="${value:1:${#value}-2}"
45
+ elif [[ "$value" == \'*\' && "$value" == *\' && "${#value}" -ge 2 ]]; then
46
+ value="${value:1:${#value}-2}"
47
+ else
48
+ value="${value%%[[:space:]]#*}"
49
+ value="$(genesis_trim "$value")"
50
+ fi
51
+ export "$key=$value"
52
+ fi
53
+ done < "$env_file"
54
+ }
55
+
56
+ genesis_parse_owner_repo() {
57
+ local raw="${1:-}"
58
+ local normalized owner repo
59
+
60
+ normalized="$(genesis_trim "$raw")"
61
+ [[ -n "$normalized" ]] || return 1
62
+
63
+ normalized="${normalized#https://github.com/}"
64
+ normalized="${normalized#http://github.com/}"
65
+ normalized="${normalized#ssh://git@github.com/}"
66
+ normalized="${normalized#git@github.com:}"
67
+ normalized="${normalized#git://github.com/}"
68
+ normalized="${normalized%.git}"
69
+ normalized="${normalized%/}"
70
+
71
+ if [[ "$normalized" != */* ]]; then
72
+ return 1
73
+ fi
74
+
75
+ owner="${normalized%%/*}"
76
+ repo="${normalized#*/}"
77
+
78
+ if [[ -z "$owner" || -z "$repo" || "$repo" == */* ]]; then
79
+ return 1
80
+ fi
81
+ if [[ ! "$owner" =~ ^[A-Za-z0-9._-]+$ ]]; then
82
+ return 1
83
+ fi
84
+ if [[ ! "$repo" =~ ^[A-Za-z0-9._-]+$ ]]; then
85
+ return 1
86
+ fi
87
+
88
+ printf '%s/%s\n' "$owner" "$repo"
89
+ }
90
+
91
+ genesis_repo_key() {
92
+ local raw="${1:-}"
93
+ local owner_repo
94
+ owner_repo="$(genesis_parse_owner_repo "$raw" 2>/dev/null || true)"
95
+ [[ -n "$owner_repo" ]] || return 1
96
+ printf '%s\n' "$(echo "$owner_repo" | tr '[:upper:]' '[:lower:]')"
97
+ }
98
+
99
+ genesis_expand_path() {
100
+ local raw_path="${1:-}"
101
+ local path
102
+ path="$(genesis_trim "$raw_path")"
103
+ case "$path" in
104
+ "~") printf '%s\n' "$HOME" ;;
105
+ "~/"*) printf '%s/%s\n' "$HOME" "${path#\~/}" ;;
106
+ *) printf '%s\n' "$path" ;;
107
+ esac
108
+ }
109
+
110
+ genesis_bool_is_true() {
111
+ local value
112
+ value="$(genesis_trim "${1:-}")"
113
+ case "${value,,}" in
114
+ true|1|yes|y|on)
115
+ return 0
116
+ ;;
117
+ *)
118
+ return 1
119
+ ;;
120
+ esac
121
+ }
122
+
123
+ genesis_extract_answer_value() {
124
+ local input_json="${1:-}"
125
+ local key="${2:-}"
126
+ [[ -n "$input_json" && -n "$key" ]] || return 1
127
+ printf '%s' "$input_json" | jq -r --arg key "$key" '
128
+ .answers[$key]
129
+ // .metadata.answers[$key]
130
+ // empty
131
+ ' 2>/dev/null
132
+ }
133
+
134
+ # === Category B: Project Metadata Queries (projects.json Accessors) ===
135
+
136
+ genesis_find_project_slug_by_repo() {
137
+ local raw_repo="${1:-}"
138
+ local projects_file="${2:-$HOME/.openclaw/workspace/devclaw/projects.json}"
139
+ local wanted line slug remote remote_key
140
+ local -a matches=()
141
+ wanted="$(genesis_repo_key "$raw_repo" 2>/dev/null || true)"
142
+ [[ -n "$wanted" ]] || return 1
143
+ [[ -f "$projects_file" ]] || return 1
144
+
145
+ while IFS=$'\t' read -r slug remote; do
146
+ [[ -n "$slug" && -n "$remote" ]] || continue
147
+ remote_key="$(genesis_repo_key "$remote" 2>/dev/null || true)"
148
+ [[ -n "$remote_key" ]] || continue
149
+ if [[ "$remote_key" == "$wanted" ]]; then
150
+ matches+=("$slug")
151
+ fi
152
+ done < <(jq -r '.projects | to_entries[] | [.key, (.value.repoRemote // .value.remote // .value.remoteUrl // "")] | @tsv' "$projects_file" 2>/dev/null || true)
153
+
154
+ if (( ${#matches[@]} == 1 )); then
155
+ printf '%s\n' "${matches[0]}"
156
+ return 0
157
+ fi
158
+
159
+ return 1
160
+ }
161
+
162
+ genesis_project_primary_channel_id() {
163
+ local slug="${1:-}"
164
+ local projects_file="${2:-$HOME/.openclaw/workspace/devclaw/projects.json}"
165
+ [[ -n "$slug" ]] || return 1
166
+ [[ -f "$projects_file" ]] || return 1
167
+ jq -r --arg slug "$slug" '.projects[$slug].channels // [] | map(select((.channelId // "") != "")) | .[0].channelId // empty' "$projects_file" 2>/dev/null
168
+ }
169
+
170
+ genesis_project_exists() {
171
+ local slug="${1:-}"
172
+ local projects_file="${2:-$HOME/.openclaw/workspace/devclaw/projects.json}"
173
+ [[ -n "$slug" && -f "$projects_file" ]] || return 1
174
+ jq -e --arg slug "$slug" '.projects[$slug] != null' "$projects_file" >/dev/null 2>&1
175
+ }
176
+
177
+ genesis_project_remote() {
178
+ local slug="${1:-}"
179
+ local projects_file="${2:-$HOME/.openclaw/workspace/devclaw/projects.json}"
180
+ [[ -n "$slug" && -f "$projects_file" ]] || return 1
181
+ jq -r --arg slug "$slug" '.projects[$slug].repoRemote // .projects[$slug].remote // .projects[$slug].remoteUrl // empty' "$projects_file" 2>/dev/null
182
+ }
183
+
184
+ genesis_project_channel_id() {
185
+ local slug="${1:-}"
186
+ local requested_channel_id="${2:-}"
187
+ local projects_file="${3:-$HOME/.openclaw/workspace/devclaw/projects.json}"
188
+ local requested
189
+
190
+ requested="$(genesis_trim "$requested_channel_id")"
191
+ [[ -n "$slug" && -f "$projects_file" ]] || return 1
192
+ genesis_project_exists "$slug" "$projects_file" || return 1
193
+
194
+ if [[ -n "$requested" ]]; then
195
+ if jq -e --arg slug "$slug" --arg cid "$requested" \
196
+ '.projects[$slug].channels // [] | map(select((.channelId // "") == $cid)) | length > 0' \
197
+ "$projects_file" >/dev/null 2>&1; then
198
+ printf '%s\n' "$requested"
199
+ return 0
200
+ fi
201
+ fi
202
+
203
+ jq -r --arg slug "$slug" '
204
+ .projects[$slug].channels // []
205
+ | (
206
+ map(select((.channelId // "") != "" and ((.name // "") | ascii_downcase) == "primary"))[0].channelId
207
+ // map(select((.channelId // "") != ""))[0].channelId
208
+ // empty
209
+ )
210
+ ' "$projects_file" 2>/dev/null
211
+ }
212
+
213
+ genesis_project_resolve_ref() {
214
+ local ref="${1:-}"
215
+ local projects_file="${2:-$HOME/.openclaw/workspace/devclaw/projects.json}"
216
+ local normalized
217
+
218
+ normalized="$(genesis_trim "$ref")"
219
+ [[ -n "$normalized" && -f "$projects_file" ]] || return 1
220
+
221
+ jq -r --arg ref "$normalized" '
222
+ .projects
223
+ | to_entries
224
+ | map(
225
+ .value
226
+ + {
227
+ slug: .key,
228
+ ref_key: (.key | ascii_downcase),
229
+ ref_name: ((.value.name // "") | ascii_downcase),
230
+ ref_repo_name: (
231
+ (
232
+ .value.repoRemote
233
+ // .value.remote
234
+ // .value.remoteUrl
235
+ // .value.repo
236
+ // ""
237
+ )
238
+ | sub("^https?://github.com/"; "")
239
+ | sub("^ssh://git@github.com/"; "")
240
+ | sub("^git@github.com:"; "")
241
+ | sub("^git://github.com/"; "")
242
+ | sub("\\.git$"; "")
243
+ | split("/")
244
+ | last
245
+ | ascii_downcase
246
+ )
247
+ }
248
+ )
249
+ | map(select(
250
+ .ref_key == ($ref | ascii_downcase)
251
+ or .ref_name == ($ref | ascii_downcase)
252
+ or .ref_repo_name == ($ref | ascii_downcase)
253
+ ))
254
+ | .[0]
255
+ | select(.slug != null)
256
+ | [
257
+ .slug,
258
+ (.name // ""),
259
+ (.repoRemote // .remote // .remoteUrl // ""),
260
+ (.repo // "")
261
+ ]
262
+ | @tsv
263
+ ' "$projects_file" 2>/dev/null
264
+ }
265
+
266
+ genesis_project_kind() {
267
+ local slug="${1:-}"
268
+ local projects_file="${2:-$HOME/.openclaw/workspace/devclaw/projects.json}"
269
+ [[ -n "$slug" && -f "$projects_file" ]] || return 1
270
+ jq -r --arg slug "$slug" '.projects[$slug].projectKind // "implementation"' "$projects_file" 2>/dev/null
271
+ }
272
+
273
+ genesis_project_archived() {
274
+ local slug="${1:-}"
275
+ local projects_file="${2:-$HOME/.openclaw/workspace/devclaw/projects.json}"
276
+ [[ -n "$slug" && -f "$projects_file" ]] || return 1
277
+ jq -r --arg slug "$slug" '
278
+ (
279
+ .projects[$slug].archived // false
280
+ ) or (
281
+ (.projects[$slug].projectKind // "") == "archived_duplicate"
282
+ )
283
+ ' "$projects_file" 2>/dev/null
284
+ }
285
+
286
+ genesis_project_default_notify_channel() {
287
+ local slug="${1:-}"
288
+ local projects_file="${2:-$HOME/.openclaw/workspace/devclaw/projects.json}"
289
+ [[ -n "$slug" && -f "$projects_file" ]] || return 1
290
+ jq -r --arg slug "$slug" '.projects[$slug].defaultNotifyChannel // empty' "$projects_file" 2>/dev/null
291
+ }
292
+
293
+ genesis_repo_target_candidate() {
294
+ local raw="${1:-}"
295
+ local trimmed owner_repo after_colon candidate
296
+
297
+ trimmed="$(genesis_trim "$raw")"
298
+ [[ -n "$trimmed" ]] || return 1
299
+
300
+ owner_repo="$(genesis_parse_owner_repo "$trimmed" 2>/dev/null || true)"
301
+ if [[ -n "$owner_repo" ]]; then
302
+ printf '%s\n' "$owner_repo"
303
+ return 0
304
+ fi
305
+
306
+ if [[ "$trimmed" == *:* ]]; then
307
+ after_colon="$(genesis_trim "${trimmed##*:}")"
308
+ owner_repo="$(genesis_parse_owner_repo "$after_colon" 2>/dev/null || true)"
309
+ if [[ -n "$owner_repo" ]]; then
310
+ printf '%s\n' "$owner_repo"
311
+ return 0
312
+ fi
313
+ if [[ "$after_colon" =~ ^[A-Za-z0-9._-]{3,100}$ ]]; then
314
+ printf '%s\n' "$after_colon"
315
+ return 0
316
+ fi
317
+ fi
318
+
319
+ candidate="$(printf '%s' "$trimmed" | sed -E 's/^[[:space:]]*(novo|new|existente|existing|repo|repositorio|reposit[oó]rio|projeto|project)[[:space:]]*[:=-]?[[:space:]]*//I')"
320
+ candidate="$(genesis_trim "$candidate")"
321
+ owner_repo="$(genesis_parse_owner_repo "$candidate" 2>/dev/null || true)"
322
+ if [[ -n "$owner_repo" ]]; then
323
+ printf '%s\n' "$owner_repo"
324
+ return 0
325
+ fi
326
+ if [[ "$candidate" =~ ^[A-Za-z0-9._-]{3,100}$ ]]; then
327
+ printf '%s\n' "$candidate"
328
+ return 0
329
+ fi
330
+
331
+ return 1
332
+ }
333
+
334
+ genesis_resolve_canonical_target() {
335
+ local input_json="${1:-}"
336
+ local projects_file="${2:-$HOME/.openclaw/workspace/devclaw/projects.json}"
337
+ local explicit_repo candidate_slug repo_target_raw repo_target_candidate
338
+ local resolved_repo="" resolved_slug="" resolved_name="" source=""
339
+ local project_ref resolved_remote owner_repo
340
+
341
+ [[ -n "$input_json" ]] || {
342
+ jq -n '{metadata:{}}'
343
+ return 0
344
+ }
345
+
346
+ explicit_repo="$(printf '%s' "$input_json" | jq -r '
347
+ .scaffold.repo_url
348
+ // .metadata.repo_url
349
+ // .repo_url
350
+ // .repo
351
+ // empty
352
+ ' 2>/dev/null || true)"
353
+ candidate_slug="$(printf '%s' "$input_json" | jq -r '
354
+ .project_slug
355
+ // .metadata.project_slug
356
+ // .scaffold.project_slug
357
+ // .metadata.project_name
358
+ // .project_name
359
+ // .repo_name
360
+ // empty
361
+ ' 2>/dev/null || true)"
362
+ repo_target_raw="$(genesis_extract_answer_value "$input_json" "repo_target" || true)"
363
+ repo_target_candidate="$(genesis_repo_target_candidate "$repo_target_raw" || true)"
364
+
365
+ explicit_repo="$(genesis_trim "$explicit_repo")"
366
+ candidate_slug="$(genesis_trim "$candidate_slug")"
367
+ repo_target_candidate="$(genesis_trim "$repo_target_candidate")"
368
+
369
+ if [[ -n "$explicit_repo" ]]; then
370
+ owner_repo="$(genesis_parse_owner_repo "$explicit_repo" 2>/dev/null || true)"
371
+ if [[ -n "$owner_repo" ]]; then
372
+ resolved_repo="https://github.com/$owner_repo"
373
+ source="explicit_repo_url"
374
+ else
375
+ project_ref="$(genesis_project_resolve_ref "$explicit_repo" "$projects_file" || true)"
376
+ if [[ -n "$project_ref" ]]; then
377
+ resolved_slug="$(printf '%s' "$project_ref" | cut -f1)"
378
+ resolved_name="$(printf '%s' "$project_ref" | cut -f2)"
379
+ resolved_remote="$(printf '%s' "$project_ref" | cut -f3)"
380
+ if [[ -n "$resolved_remote" ]]; then
381
+ resolved_repo="$resolved_remote"
382
+ fi
383
+ source="explicit_repo_ref"
384
+ fi
385
+ fi
386
+ fi
387
+
388
+ if [[ -z "$resolved_slug" && -n "$candidate_slug" ]]; then
389
+ if genesis_project_exists "$candidate_slug" "$projects_file"; then
390
+ resolved_slug="$candidate_slug"
391
+ resolved_remote="$(genesis_project_remote "$candidate_slug" "$projects_file" || true)"
392
+ if [[ -n "$resolved_remote" ]]; then
393
+ resolved_repo="$resolved_remote"
394
+ fi
395
+ [[ -n "$source" ]] || source="explicit_project_slug"
396
+ else
397
+ project_ref="$(genesis_project_resolve_ref "$candidate_slug" "$projects_file" || true)"
398
+ if [[ -n "$project_ref" ]]; then
399
+ resolved_slug="$(printf '%s' "$project_ref" | cut -f1)"
400
+ resolved_name="$(printf '%s' "$project_ref" | cut -f2)"
401
+ resolved_remote="$(printf '%s' "$project_ref" | cut -f3)"
402
+ if [[ -n "$resolved_remote" ]]; then
403
+ resolved_repo="$resolved_remote"
404
+ fi
405
+ [[ -n "$source" ]] || source="project_ref"
406
+ fi
407
+ fi
408
+ fi
409
+
410
+ if [[ -z "$resolved_slug" && -z "$resolved_repo" && -n "$repo_target_candidate" ]]; then
411
+ owner_repo="$(genesis_parse_owner_repo "$repo_target_candidate" 2>/dev/null || true)"
412
+ if [[ -n "$owner_repo" ]]; then
413
+ resolved_repo="https://github.com/$owner_repo"
414
+ resolved_slug="$(genesis_find_project_slug_by_repo "$resolved_repo" "$projects_file" || true)"
415
+ source="answers.repo_target"
416
+ else
417
+ project_ref="$(genesis_project_resolve_ref "$repo_target_candidate" "$projects_file" || true)"
418
+ if [[ -n "$project_ref" ]]; then
419
+ resolved_slug="$(printf '%s' "$project_ref" | cut -f1)"
420
+ resolved_name="$(printf '%s' "$project_ref" | cut -f2)"
421
+ resolved_remote="$(printf '%s' "$project_ref" | cut -f3)"
422
+ if [[ -n "$resolved_remote" ]]; then
423
+ resolved_repo="$resolved_remote"
424
+ fi
425
+ source="answers.repo_target"
426
+ fi
427
+ fi
428
+ fi
429
+
430
+ if [[ -z "$resolved_slug" && -n "$resolved_repo" ]]; then
431
+ resolved_slug="$(genesis_find_project_slug_by_repo "$resolved_repo" "$projects_file" || true)"
432
+ if [[ -n "$resolved_slug" ]]; then
433
+ [[ -n "$source" ]] || source="repo_remote"
434
+ fi
435
+ fi
436
+
437
+ if [[ -n "$resolved_slug" && -z "$resolved_repo" ]]; then
438
+ resolved_remote="$(genesis_project_remote "$resolved_slug" "$projects_file" || true)"
439
+ if [[ -n "$resolved_remote" ]]; then
440
+ resolved_repo="$resolved_remote"
441
+ fi
442
+ fi
443
+
444
+ if [[ -z "$resolved_name" && -n "$resolved_slug" ]]; then
445
+ resolved_name="$resolved_slug"
446
+ fi
447
+
448
+ jq -n \
449
+ --arg repo_url "$resolved_repo" \
450
+ --arg project_slug "$resolved_slug" \
451
+ --arg project_name "$resolved_name" \
452
+ --arg repo_target_source "$source" \
453
+ '{
454
+ metadata: (
455
+ {}
456
+ + (if $repo_url != "" then {repo_url: $repo_url} else {} end)
457
+ + (if $project_slug != "" then {project_slug: $project_slug} else {} end)
458
+ + (if $project_name != "" then {project_name: $project_name} else {} end)
459
+ + (if $repo_target_source != "" then {repo_target_source: $repo_target_source} else {} end)
460
+ )
461
+ }'
462
+ }
463
+
464
+ # === Category D: OpenClaw CLI Execution ===
465
+
466
+ genesis_openclaw_bin() {
467
+ local candidate
468
+ local -a candidates=(
469
+ "${OPENCLAW_BIN:-}"
470
+ "$(command -v openclaw 2>/dev/null || true)"
471
+ "$HOME/.nvm/versions/node/v24.14.0/bin/openclaw"
472
+ "$HOME/.local/bin/openclaw"
473
+ "/usr/local/bin/openclaw"
474
+ )
475
+
476
+ while IFS= read -r candidate; do
477
+ candidates+=("$candidate")
478
+ done < <(ls -1d "$HOME"/.nvm/versions/node/v*/bin/openclaw 2>/dev/null || true)
479
+
480
+ for candidate in "${candidates[@]}"; do
481
+ [[ -n "$candidate" ]] || continue
482
+ if [[ -x "$candidate" ]]; then
483
+ printf '%s\n' "$candidate"
484
+ return 0
485
+ fi
486
+ done
487
+ return 1
488
+ }
489
+
490
+ genesis_openclaw_timeout_sec() {
491
+ local raw
492
+ raw="$(genesis_trim "${GENESIS_OPENCLAW_TIMEOUT_SEC:-75}")"
493
+ if [[ "$raw" =~ ^[0-9]+$ ]] && [[ "$raw" -ge 5 ]] && [[ "$raw" -le 900 ]]; then
494
+ printf '%s\n' "$raw"
495
+ return 0
496
+ fi
497
+ printf '75\n'
498
+ }
499
+
500
+ genesis_openclaw_retries() {
501
+ local raw
502
+ raw="$(genesis_trim "${GENESIS_OPENCLAW_RETRIES:-1}")"
503
+ if [[ "$raw" =~ ^[0-9]+$ ]] && [[ "$raw" -ge 1 ]] && [[ "$raw" -le 3 ]]; then
504
+ printf '%s\n' "$raw"
505
+ return 0
506
+ fi
507
+ printf '1\n'
508
+ }
509
+
510
+ genesis_openclaw_retry_delay_sec() {
511
+ local raw
512
+ raw="$(genesis_trim "${GENESIS_OPENCLAW_RETRY_DELAY_SEC:-2}")"
513
+ if [[ "$raw" =~ ^[0-9]+$ ]] && [[ "$raw" -ge 1 ]] && [[ "$raw" -le 30 ]]; then
514
+ printf '%s\n' "$raw"
515
+ return 0
516
+ fi
517
+ printf '2\n'
518
+ }
519
+
520
+ genesis_openclaw_exec() {
521
+ local openclaw_bin openclaw_dir timeout_sec
522
+ if ! openclaw_bin="$(genesis_openclaw_bin)"; then
523
+ echo "ERROR: OpenClaw CLI not found. Set OPENCLAW_BIN or add openclaw to PATH." >&2
524
+ return 127
525
+ fi
526
+ openclaw_dir="$(dirname "$openclaw_bin")"
527
+ timeout_sec="$(genesis_openclaw_timeout_sec)"
528
+ if command -v timeout >/dev/null 2>&1; then
529
+ PATH="$openclaw_dir:$PATH" timeout --signal=TERM --kill-after=5s "${timeout_sec}s" "$openclaw_bin" "$@"
530
+ else
531
+ PATH="$openclaw_dir:$PATH" "$openclaw_bin" "$@"
532
+ fi
533
+ }
534
+
535
+ genesis_openclaw_supports() {
536
+ [[ "$#" -gt 0 ]] || return 1
537
+ local raw_args=("$@")
538
+ local args=()
539
+ local word
540
+ for word in "${raw_args[@]}"; do
541
+ # shellcheck disable=SC2206
542
+ args+=($word)
543
+ done
544
+ [[ "${#args[@]}" -gt 0 ]] || return 1
545
+ local i parent_help child
546
+ for (( i=0; i<${#args[@]}; i++ )); do
547
+ child="${args[$i]}"
548
+ if (( i == 0 )); then
549
+ parent_help="$(genesis_openclaw_exec --help 2>&1)" || true
550
+ if ! echo "$parent_help" | grep -qE "^[[:space:]]+${child}([[:space:]]|$)"; then
551
+ genesis_openclaw_exec "$child" --help >/dev/null 2>&1 || return 1
552
+ fi
553
+ else
554
+ parent_help="$(genesis_openclaw_exec "${args[@]:0:$i}" --help 2>&1)" || return 1
555
+ if ! echo "$parent_help" | grep -qE "^[[:space:]]+${child}([[:space:]]|$)"; then
556
+ return 1
557
+ fi
558
+ fi
559
+ done
560
+ return 0
561
+ }
562
+
563
+ genesis_devclaw_task_json() {
564
+ local attempts delay try status
565
+ attempts="$(genesis_openclaw_retries)"
566
+ delay="$(genesis_openclaw_retry_delay_sec)"
567
+ try=1
568
+
569
+ while true; do
570
+ if genesis_openclaw_exec devclaw task "$@" --json; then
571
+ return 0
572
+ fi
573
+ status=$?
574
+ if [[ "$try" -ge "$attempts" ]]; then
575
+ return "$status"
576
+ fi
577
+ echo "WARN: DevClaw task call failed (attempt $try/$attempts, exit=$status). Retrying in ${delay}s..." >&2
578
+ sleep "$delay"
579
+ try=$((try + 1))
580
+ done
581
+ }
582
+
583
+ # === Category E: Factory Logic Detectors ===
584
+
585
+ genesis_is_factory_project_slug() {
586
+ local slug
587
+ slug="$(genesis_trim "${1:-}")"
588
+ [[ -n "$slug" ]] || return 1
589
+ case "${slug,,}" in
590
+ devclaw-automation|factory-*|fabrica-*|genesis-router)
591
+ return 0
592
+ ;;
593
+ *)
594
+ return 1
595
+ ;;
596
+ esac
597
+ }
598
+
599
+ genesis_request_is_factory_change() {
600
+ local text
601
+ text="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')"
602
+ [[ -n "$text" ]] || return 1
603
+ printf '%s\n' "$text" | grep -Eq 'internal|interna|interno|melhoria da f[aá]brica|factory (core|internal)|openclaw|devclaw|genesis|pipeline|workflow|triage|register-project|create-task|scaffold-project|sideband|projects\.json|~\/\.openclaw'
604
+ }
605
+
606
+ genesis_payload_factory_change() {
607
+ local input="${1:-}"
608
+ local raw
609
+ [[ -n "$input" ]] || return 1
610
+ raw="$(echo "$input" | jq -r '
611
+ .factory_change
612
+ // .metadata.factory_change
613
+ // .scaffold.factory_change
614
+ // empty
615
+ ' 2>/dev/null || true)"
616
+ genesis_bool_is_true "$raw"
617
+ }