@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,520 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Step: Register scaffolded project in DevClaw
5
+ # Input: stdin JSON (from scaffold.stdout)
6
+ # Output: JSON with registration data + sideband file
7
+ # Creates: projects.json entry, workflow.yaml, role prompts, repository labels
8
+
9
+ GENESIS_LOG="${GENESIS_LOG:-$HOME/.openclaw/workspace/logs/genesis.log}"
10
+ mkdir -p "$(dirname "$GENESIS_LOG")"
11
+ exec 2> >(tee -a "$GENESIS_LOG" >&2)
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ WORKSPACE="$HOME/.openclaw/workspace"
15
+ source "$SCRIPT_DIR/sideband-lib.sh"
16
+ source "$SCRIPT_DIR/genesis-telemetry.sh"
17
+
18
+ # Load .env if available
19
+ genesis_load_env_file "$HOME/.openclaw/.env"
20
+
21
+ if [[ -n "${1:-}" && -f "${1:-}" ]]; then
22
+ INPUT="$(cat "$1")"
23
+ else
24
+ INPUT="$(cat)"
25
+ fi
26
+ SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
27
+ genesis_metric_start "register-project" "$SESSION_ID"
28
+ echo "=== $(date -Iseconds) | register-project.sh | session=$SESSION_ID ===" >&2
29
+
30
+ SCAFFOLD_CREATED="$(echo "$INPUT" | jq -r '.scaffold.created // false')"
31
+ DRY_RUN="${GENESIS_DRY_RUN:-$(echo "$INPUT" | jq -r '.dry_run // false')}"
32
+
33
+ if [[ "$DRY_RUN" == "true" ]]; then
34
+ echo "Dry-run enabled — skipping project registration" >&2
35
+ echo "$INPUT" | jq '. + {project_registered: false, project_registration_pending: "dry_run"}'
36
+ exit 0
37
+ fi
38
+
39
+ # Non-scaffold: passthrough
40
+ if [[ "$SCAFFOLD_CREATED" != "true" ]]; then
41
+ echo "No scaffold — skipping registration" >&2
42
+ echo "$INPUT" | jq '. + {project_registered: false}'
43
+ exit 0
44
+ fi
45
+
46
+ SLUG="$(echo "$INPUT" | jq -r '.scaffold.project_slug')"
47
+ REPO_URL="$(echo "$INPUT" | jq -r '.scaffold.repo_url')"
48
+ REPO_LOCAL="$(echo "$INPUT" | jq -r '.scaffold.repo_local')"
49
+ STACK="$(echo "$INPUT" | jq -r '.scaffold.stack')"
50
+ REQUESTED_CHANNEL_ID="$(echo "$INPUT" | jq -r '.project_channel_id // .channel_id // .telegram_chat_id // .chat_id // empty')"
51
+ # Prefer Fabrica Projects group channel for new projects
52
+ # Falls back to TELEGRAM_CHAT_ID only if FABRICA_PROJECTS_CHANNEL_ID is not set
53
+ TELEGRAM_CHAT="${FABRICA_PROJECTS_CHANNEL_ID:-${TELEGRAM_CHAT_ID:-}}"
54
+ if [[ -n "$REQUESTED_CHANNEL_ID" ]]; then
55
+ TELEGRAM_CHAT="$REQUESTED_CHANNEL_ID"
56
+ fi
57
+ ALLOW_SHARED_CHANNELS="${GENESIS_ALLOW_SHARED_CHANNELS:-true}"
58
+ ALLOW_SHARED_CHANNELS="${ALLOW_SHARED_CHANNELS,,}"
59
+ CI_WORKFLOWS_REPO="${CI_WORKFLOWS_REPO:-MestreY0d4-Uninter/fabrica-automation}"
60
+ OWNER_REPO="$(genesis_parse_owner_repo "$REPO_URL" || true)"
61
+ if [[ -z "$OWNER_REPO" ]]; then
62
+ echo "ERROR: Invalid GitHub repository reference: $REPO_URL" >&2
63
+ exit 1
64
+ fi
65
+ REPO_REMOTE="https://github.com/$OWNER_REPO.git"
66
+
67
+ echo "Registering project: $SLUG (stack=$STACK)" >&2
68
+
69
+ PROJECTS_JSON="$WORKSPACE/devclaw/projects.json"
70
+ USE_DEVCLAW_PROJECT_REGISTER_CLI=false
71
+ USE_DEVCLAW_PROJECT_LABELS_CLI=false
72
+ if genesis_openclaw_supports devclaw project register; then
73
+ USE_DEVCLAW_PROJECT_REGISTER_CLI=true
74
+ fi
75
+ if genesis_openclaw_supports devclaw project ensure-labels; then
76
+ USE_DEVCLAW_PROJECT_LABELS_CLI=true
77
+ fi
78
+
79
+ # Shared-channel policy guard: avoid silent collisions
80
+ if [[ -n "$TELEGRAM_CHAT" ]] && [[ -f "$PROJECTS_JSON" ]]; then
81
+ CHANNEL_MATCHES="$(jq -r --arg slug "$SLUG" --arg chatid "$TELEGRAM_CHAT" '
82
+ .projects
83
+ | to_entries[]
84
+ | select(.key != $slug)
85
+ | select((.value.channels // []) | any(.channelId == $chatid))
86
+ | .key
87
+ ' "$PROJECTS_JSON" 2>/dev/null || true)"
88
+
89
+ if [[ -n "$CHANNEL_MATCHES" ]]; then
90
+ CHANNEL_MATCHES_CSV="$(echo "$CHANNEL_MATCHES" | paste -sd "," -)"
91
+ if [[ "$ALLOW_SHARED_CHANNELS" != "true" ]]; then
92
+ echo "WARNING: channelId $TELEGRAM_CHAT already linked to projects: $CHANNEL_MATCHES_CSV" >&2
93
+ echo "GENESIS_ALLOW_SHARED_CHANNELS=$ALLOW_SHARED_CHANNELS -> not attaching channel to new project $SLUG" >&2
94
+ TELEGRAM_CHAT=""
95
+ else
96
+ echo "WARNING: channelId $TELEGRAM_CHAT already linked to projects: $CHANNEL_MATCHES_CSV" >&2
97
+ echo "Proceeding with shared channel (GENESIS_ALLOW_SHARED_CHANNELS=true). Use projectSlug in tools when channel is ambiguous." >&2
98
+ fi
99
+ fi
100
+ fi
101
+
102
+ # --- Check if already registered ---
103
+ PROJECT_ALREADY_REGISTERED=false
104
+ PROJECT_REGISTERED=false
105
+ PROJECT_REGISTRATION_PENDING=""
106
+
107
+ if jq -e --arg slug "$SLUG" '.projects[$slug]' "$PROJECTS_JSON" &>/dev/null; then
108
+ echo "Project $SLUG already registered in projects.json — skipping" >&2
109
+ PROJECT_ALREADY_REGISTERED=true
110
+ PROJECT_REGISTERED=true
111
+ if [[ -n "$TELEGRAM_CHAT" ]]; then
112
+ PROJECT_HAS_CHANNEL="$(jq -r --arg slug "$SLUG" --arg chatid "$TELEGRAM_CHAT" '
113
+ ((.projects[$slug].channels // []) | any((.channelId // "") == $chatid))
114
+ ' "$PROJECTS_JSON" 2>/dev/null || echo "false")"
115
+ if [[ "$PROJECT_HAS_CHANNEL" != "true" ]]; then
116
+ CHANNEL_LINKED=false
117
+ if genesis_openclaw_supports devclaw channel register; then
118
+ echo "Linking channelId $TELEGRAM_CHAT to existing project $SLUG..." >&2
119
+ if genesis_openclaw_exec devclaw channel register \
120
+ --project "$SLUG" \
121
+ --channel-id "$TELEGRAM_CHAT" \
122
+ --type "telegram" >/dev/null 2>>"$GENESIS_LOG"; then
123
+ echo "Linked channelId $TELEGRAM_CHAT to project $SLUG" >&2
124
+ CHANNEL_LINKED=true
125
+ else
126
+ echo "WARNING: failed to link channelId $TELEGRAM_CHAT to existing project $SLUG" >&2
127
+ fi
128
+ fi
129
+ if [[ "$CHANNEL_LINKED" != "true" ]]; then
130
+ echo "Applying local channel-link fallback for $SLUG -> $TELEGRAM_CHAT" >&2
131
+ if jq --arg slug "$SLUG" --arg chatid "$TELEGRAM_CHAT" '
132
+ .projects[$slug].dispatchEnabled = true
133
+ | .projects[$slug].defaultNotifyChannel = (.projects[$slug].defaultNotifyChannel // "primary")
134
+ | .projects[$slug].projectKind = (.projects[$slug].projectKind // "implementation")
135
+ | .projects[$slug].archived = (.projects[$slug].archived // false)
136
+ | .projects[$slug].channels = (
137
+ ((.projects[$slug].channels // []) + [{
138
+ channel: "telegram",
139
+ name: "primary",
140
+ events: ["*"],
141
+ channelId: $chatid
142
+ }]) | unique_by(.channel + ":" + (.channelId // ""))
143
+ )
144
+ ' "$PROJECTS_JSON" > "${PROJECTS_JSON}.tmp" \
145
+ && mv "${PROJECTS_JSON}.tmp" "$PROJECTS_JSON"; then
146
+ echo "Linked channelId $TELEGRAM_CHAT to project $SLUG via local fallback" >&2
147
+ else
148
+ echo "WARNING: local fallback failed to link channelId $TELEGRAM_CHAT to existing project $SLUG" >&2
149
+ fi
150
+ fi
151
+ fi
152
+ fi
153
+ else
154
+ if [[ -z "$TELEGRAM_CHAT" ]]; then
155
+ echo "No TELEGRAM_CHAT_ID available — skipping registration until a channel is linked." >&2
156
+ PROJECT_REGISTRATION_PENDING="missing_channel"
157
+ elif [[ "$USE_DEVCLAW_PROJECT_REGISTER_CLI" == "true" ]]; then
158
+ echo "Registering project via deterministic DevClaw project_register..." >&2
159
+ genesis_openclaw_exec devclaw project register \
160
+ --name "$SLUG" \
161
+ --repo "$REPO_LOCAL" \
162
+ --base-branch "main" \
163
+ --deploy-branch "main" \
164
+ --group-name "Project: $SLUG" \
165
+ --channel-id "$TELEGRAM_CHAT" \
166
+ --channel-type "telegram" \
167
+ --set-default-notify \
168
+ --json >/dev/null 2>>"$GENESIS_LOG"
169
+ PROJECT_REGISTERED=true
170
+ else
171
+ echo "DevClaw project CLI unavailable/incompatible — using local fallback registration." >&2
172
+ echo "Adding $SLUG to projects.json..." >&2
173
+
174
+ # Build the new project entry
175
+ NEW_ENTRY="$(jq -n \
176
+ --arg slug "$SLUG" \
177
+ --arg repo "$REPO_LOCAL" \
178
+ --arg remote "$REPO_REMOTE" \
179
+ --arg chatid "$TELEGRAM_CHAT" \
180
+ '{
181
+ slug: $slug,
182
+ name: $slug,
183
+ repo: $repo,
184
+ repoRemote: $remote,
185
+ groupName: ("Project: " + $slug),
186
+ deployUrl: "",
187
+ baseBranch: "main",
188
+ deployBranch: "main",
189
+ defaultNotifyChannel: (if $chatid != "" then "primary" else "" end),
190
+ projectKind: "implementation",
191
+ archived: false,
192
+ dispatchEnabled: (if $chatid != "" then true else false end),
193
+ channels: (if $chatid != "" then [{
194
+ channel: "telegram",
195
+ name: "primary",
196
+ events: ["*"],
197
+ channelId: $chatid
198
+ }] else [] end),
199
+ provider: "github",
200
+ workers: {
201
+ developer: {
202
+ levels: {
203
+ junior: [
204
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
205
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
206
+ ],
207
+ medior: [
208
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
209
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
210
+ ],
211
+ senior: [
212
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
213
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
214
+ ]
215
+ }
216
+ },
217
+ reviewer: {
218
+ levels: {
219
+ junior: [
220
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
221
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
222
+ ],
223
+ senior: [
224
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
225
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
226
+ ]
227
+ }
228
+ },
229
+ tester: {
230
+ levels: {
231
+ junior: [
232
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
233
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
234
+ ],
235
+ medior: [
236
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
237
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
238
+ ],
239
+ senior: [
240
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null},
241
+ {active: false, issueId: null, sessionKey: null, startTime: null, previousLabel: null}
242
+ ]
243
+ }
244
+ }
245
+ }
246
+ }'
247
+ )"
248
+
249
+ # Merge into projects.json (locked write)
250
+ if command -v flock >/dev/null 2>&1; then
251
+ LOCK_FILE="${PROJECTS_JSON}.lock"
252
+ exec 9>"$LOCK_FILE"
253
+ flock -x 9
254
+ jq --arg slug "$SLUG" --argjson entry "$NEW_ENTRY" \
255
+ '.projects[$slug] = $entry' "$PROJECTS_JSON" > "${PROJECTS_JSON}.tmp" \
256
+ && mv "${PROJECTS_JSON}.tmp" "$PROJECTS_JSON"
257
+ flock -u 9
258
+ exec 9>&-
259
+ else
260
+ echo "WARNING: flock not available; proceeding without explicit file lock" >&2
261
+ jq --arg slug "$SLUG" --argjson entry "$NEW_ENTRY" \
262
+ '.projects[$slug] = $entry' "$PROJECTS_JSON" > "${PROJECTS_JSON}.tmp" \
263
+ && mv "${PROJECTS_JSON}.tmp" "$PROJECTS_JSON"
264
+ fi
265
+
266
+ echo "Added $SLUG to projects.json" >&2
267
+ PROJECT_REGISTERED=true
268
+ fi
269
+ fi
270
+
271
+ if [[ "$PROJECT_REGISTERED" != "true" ]]; then
272
+ REGISTER_PAYLOAD="$(jq -n \
273
+ --arg slug "$SLUG" \
274
+ --arg reason "$PROJECT_REGISTRATION_PENDING" \
275
+ '{project_slug: $slug, project_registered: false, project_registration_pending: $reason}')"
276
+ SIDEBAND="$(genesis_sideband_write "register" "$SESSION_ID" "$REGISTER_PAYLOAD")"
277
+ echo "Sideband written to $SIDEBAND" >&2
278
+ echo "Registration pending for $SLUG ($PROJECT_REGISTRATION_PENDING)" >&2
279
+ echo "$INPUT" | jq \
280
+ --arg slug "$SLUG" \
281
+ --arg reason "$PROJECT_REGISTRATION_PENDING" \
282
+ '. + {project_registered: false, project_slug: $slug, project_registration_pending: $reason}'
283
+ exit 0
284
+ fi
285
+
286
+ # --- Create workflow.yaml ---
287
+ WORKFLOW_DIR="$WORKSPACE/devclaw/projects/$SLUG"
288
+ mkdir -p "$WORKFLOW_DIR"
289
+
290
+ if [[ -f "$WORKFLOW_DIR/workflow.yaml" ]]; then
291
+ echo "workflow.yaml already exists — skipping" >&2
292
+ else
293
+ echo "Creating workflow.yaml..." >&2
294
+ cat > "$WORKFLOW_DIR/workflow.yaml" <<'YAMLEOF'
295
+ # Project workflow — inherits model allocation from workspace workflow.yaml
296
+ # Add role/model overrides here only when this project needs different models.
297
+
298
+ timeouts:
299
+ dispatchMs: 1800000
300
+ staleWorkerHours: 2
301
+
302
+ workflow:
303
+ reviewPolicy: agent
304
+ testPolicy: agent
305
+ maxWorkersPerLevel: 1
306
+ YAMLEOF
307
+ echo "Created workflow.yaml" >&2
308
+ fi
309
+
310
+ # --- Copy role prompts ---
311
+ ROLES_SRC="$WORKSPACE/projects/roles/devclaw-automation"
312
+ ROLES_DEFAULT="$WORKSPACE/projects/roles/default"
313
+ ROLES_WORKSPACE_DEFAULT="$WORKSPACE/devclaw/prompts"
314
+ ROLES_DST="$WORKSPACE/devclaw/projects/$SLUG/prompts"
315
+ mkdir -p "$ROLES_DST"
316
+
317
+ for ROLE in developer reviewer tester; do
318
+ if [[ -f "$ROLES_DST/$ROLE.md" ]]; then
319
+ echo "Role prompt $ROLE.md already exists — skipping" >&2
320
+ continue
321
+ fi
322
+
323
+ # Try project-specific source first, then default
324
+ SRC=""
325
+ if [[ -f "$ROLES_SRC/$ROLE.md" ]]; then
326
+ SRC="$ROLES_SRC/$ROLE.md"
327
+ elif [[ -f "$ROLES_DEFAULT/$ROLE.md" ]]; then
328
+ SRC="$ROLES_DEFAULT/$ROLE.md"
329
+ elif [[ -f "$ROLES_WORKSPACE_DEFAULT/$ROLE.md" ]]; then
330
+ SRC="$ROLES_WORKSPACE_DEFAULT/$ROLE.md"
331
+ fi
332
+
333
+ if [[ -n "$SRC" ]]; then
334
+ # Copy and replace project name references
335
+ sed "s/devclaw-automation/$SLUG/g" "$SRC" > "$ROLES_DST/$ROLE.md"
336
+ echo "Copied $ROLE.md (from $(basename "$(dirname "$SRC")"))" >&2
337
+ else
338
+ echo "WARNING: No source for $ROLE.md — skipping" >&2
339
+ fi
340
+ done
341
+
342
+ # --- Ensure labels in GitHub repo ---
343
+ LABELS_JSON="$SCRIPT_DIR/../configs/labels.json"
344
+
345
+ if [[ -f "$LABELS_JSON" ]]; then
346
+ # Wait for newly-created repo to propagate across GitHub API nodes
347
+ if [[ "$SCAFFOLD_CREATED" == "true" && "$PROJECT_ALREADY_REGISTERED" != "true" ]]; then
348
+ echo "Waiting for repo propagation (3s)..." >&2
349
+ sleep 3
350
+ fi
351
+
352
+ echo "Ensuring labels in $OWNER_REPO..." >&2
353
+ if [[ "$USE_DEVCLAW_PROJECT_LABELS_CLI" == "true" ]]; then
354
+ ENSURE_LABELS_CMD=(devclaw project ensure-labels --project "$SLUG" --labels-file "$LABELS_JSON")
355
+ if [[ -n "$TELEGRAM_CHAT" ]]; then
356
+ ENSURE_LABELS_CMD+=(--notify-channel-id "$TELEGRAM_CHAT")
357
+ fi
358
+ genesis_openclaw_exec "${ENSURE_LABELS_CMD[@]}" --json >/dev/null 2>>"$GENESIS_LOG"
359
+ else
360
+ echo "DevClaw ensure-labels CLI unavailable/incompatible — falling back to gh for custom repo labels." >&2
361
+ LABEL_COUNT=0
362
+ LABEL_TOTAL="$(jq 'length' "$LABELS_JSON")"
363
+ while IFS= read -r label_line; do
364
+ LABEL_NAME="$(echo "$label_line" | jq -r '.name')"
365
+ LABEL_COLOR="$(echo "$label_line" | jq -r '.color')"
366
+ LABEL_DESC="$(echo "$label_line" | jq -r '.description')"
367
+
368
+ if gh label create "$LABEL_NAME" \
369
+ --repo "$OWNER_REPO" \
370
+ --color "$LABEL_COLOR" \
371
+ --description "$LABEL_DESC" \
372
+ --force >>"$GENESIS_LOG" 2>&1; then
373
+ LABEL_COUNT=$((LABEL_COUNT + 1))
374
+ else
375
+ echo " WARN: failed to create label '$LABEL_NAME'" >&2
376
+ fi
377
+ sleep 0.1
378
+ done < <(jq -c '.[]' "$LABELS_JSON")
379
+ echo "Created/updated $LABEL_COUNT / $LABEL_TOTAL labels" >&2
380
+
381
+ if [[ -n "$TELEGRAM_CHAT" ]]; then
382
+ gh label create "notify:$TELEGRAM_CHAT" \
383
+ --repo "$OWNER_REPO" \
384
+ --color "e4e4e4" \
385
+ --description "" \
386
+ --force >>"$GENESIS_LOG" 2>&1 || true
387
+ fi
388
+ fi
389
+ else
390
+ echo "WARNING: labels.json not found — skipping label creation" >&2
391
+ fi
392
+
393
+ # --- Set up CI foundation (Contract Pack + dependabot) ---
394
+ if [[ -n "$REPO_LOCAL" && -d "$REPO_LOCAL" ]]; then
395
+ echo "Setting up CI foundation..." >&2
396
+ GITHUB_DIR="$REPO_LOCAL/.github"
397
+ WORKFLOWS_DIR="$GITHUB_DIR/workflows"
398
+ mkdir -p "$WORKFLOWS_DIR"
399
+
400
+ # Determine contract pack based on stack
401
+ PACK_WORKFLOW=""
402
+ case "${STACK,,}" in
403
+ python|fastapi|flask|django|python-cli)
404
+ PACK_WORKFLOW="contract-pack-python.yml"
405
+ DEPENDABOT_ECOSYSTEM="pip"
406
+ ;;
407
+ node|nodejs|express|react|next|vue|typescript)
408
+ PACK_WORKFLOW="contract-pack-node.yml"
409
+ DEPENDABOT_ECOSYSTEM="npm"
410
+ ;;
411
+ *)
412
+ DEPENDABOT_ECOSYSTEM=""
413
+ ;;
414
+ esac
415
+
416
+ # Create CI workflow that calls shared workflows
417
+ cat > "$WORKFLOWS_DIR/ci.yml" <<CIEOF
418
+ name: CI
419
+
420
+ on:
421
+ push:
422
+ branches: [main]
423
+ pull_request:
424
+ branches: [main]
425
+
426
+ jobs:
427
+ ci:
428
+ uses: ${CI_WORKFLOWS_REPO}/.github/workflows/ci-base.yml@main
429
+ security:
430
+ uses: ${CI_WORKFLOWS_REPO}/.github/workflows/security.yml@main
431
+ CIEOF
432
+
433
+ # Add contract pack if stack is known
434
+ if [[ -n "$PACK_WORKFLOW" ]]; then
435
+ cat >> "$WORKFLOWS_DIR/ci.yml" <<PACKEOF
436
+ quality:
437
+ uses: ${CI_WORKFLOWS_REPO}/.github/workflows/${PACK_WORKFLOW}@main
438
+ PACKEOF
439
+ echo " Contract Pack: $PACK_WORKFLOW" >&2
440
+ fi
441
+
442
+ # Create dependabot config
443
+ cat > "$GITHUB_DIR/dependabot.yml" <<DEPEOF
444
+ version: 2
445
+ updates:
446
+ - package-ecosystem: "github-actions"
447
+ directory: "/"
448
+ schedule:
449
+ interval: "weekly"
450
+ DEPEOF
451
+
452
+ if [[ -n "$DEPENDABOT_ECOSYSTEM" ]]; then
453
+ cat >> "$GITHUB_DIR/dependabot.yml" <<DEPEOF
454
+ - package-ecosystem: "${DEPENDABOT_ECOSYSTEM}"
455
+ directory: "/"
456
+ schedule:
457
+ interval: "weekly"
458
+ DEPEOF
459
+ fi
460
+
461
+ # Commit CI foundation if repo has git
462
+ if [[ -d "$REPO_LOCAL/.git" ]]; then
463
+ (cd "$REPO_LOCAL" && git add .github/ && git commit -m "ci: add shared CI foundation" --no-verify >>"$GENESIS_LOG" 2>&1 && git push origin main >>"$GENESIS_LOG" 2>&1) || true
464
+ fi
465
+ echo "CI foundation set up" >&2
466
+ else
467
+ echo "WARNING: REPO_LOCAL not available — skipping CI setup" >&2
468
+ fi
469
+
470
+ # --- Create Telegram Forum Topic ---
471
+ if [[ -n "$TELEGRAM_CHAT" && "$PROJECT_REGISTERED" == "true" && "$PROJECT_ALREADY_REGISTERED" != "true" ]]; then
472
+ TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
473
+ if [[ -n "$TELEGRAM_BOT_TOKEN" ]]; then
474
+ echo "Creating forum topic for $SLUG in chat $TELEGRAM_CHAT..." >&2
475
+ TOPIC_RESPONSE="$(curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/createForumTopic" \
476
+ -H "Content-Type: application/json" \
477
+ -d "{\"chat_id\": ${TELEGRAM_CHAT}, \"name\": \"${SLUG}\"}" 2>/dev/null || true)"
478
+ TOPIC_OK="$(echo "$TOPIC_RESPONSE" | jq -r '.ok // false' 2>/dev/null || echo "false")"
479
+ if [[ "$TOPIC_OK" == "true" ]]; then
480
+ TOPIC_ID="$(echo "$TOPIC_RESPONSE" | jq -r '.result.message_thread_id')"
481
+ echo "Forum topic created: $SLUG (messageThreadId=$TOPIC_ID)" >&2
482
+ if jq --arg slug "$SLUG" --argjson tid "$TOPIC_ID" --arg chatid "$TELEGRAM_CHAT" '
483
+ .projects[$slug].channels = [
484
+ .projects[$slug].channels[] |
485
+ if .channel == "telegram" and .channelId == $chatid
486
+ then . + {messageThreadId: $tid}
487
+ else .
488
+ end
489
+ ]
490
+ ' "$PROJECTS_JSON" > "${PROJECTS_JSON}.tmp" \
491
+ && mv "${PROJECTS_JSON}.tmp" "$PROJECTS_JSON"; then
492
+ echo "Updated projects.json with messageThreadId=$TOPIC_ID" >&2
493
+ else
494
+ echo "WARNING: failed to update projects.json with messageThreadId" >&2
495
+ fi
496
+ else
497
+ TOPIC_ERR="$(echo "$TOPIC_RESPONSE" | jq -r '.description // "unknown error"' 2>/dev/null || echo "unknown")"
498
+ echo "WARNING: forum topic creation failed: $TOPIC_ERR (non-blocking)" >&2
499
+ fi
500
+ else
501
+ echo "TELEGRAM_BOT_TOKEN not set — skipping forum topic creation" >&2
502
+ fi
503
+ fi
504
+
505
+ # --- Write sideband file ---
506
+ REGISTER_PAYLOAD="$(jq -n \
507
+ --arg slug "$SLUG" \
508
+ --argjson registered true \
509
+ '{project_slug: $slug, project_registered: $registered}')"
510
+ SIDEBAND="$(genesis_sideband_write "register" "$SESSION_ID" "$REGISTER_PAYLOAD")"
511
+
512
+ echo "Sideband written to $SIDEBAND" >&2
513
+ echo "Registration complete for $SLUG" >&2
514
+
515
+ # --- Output ---
516
+ echo "$INPUT" | jq \
517
+ --arg slug "$SLUG" \
518
+ '. + {project_registered: true, project_slug: $slug}'
519
+
520
+ genesis_metric_end "ok"
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Step 2b: Web research on idea using Gemini web_search
5
+ # Input: stdin JSON (from classify-idea.sh — contains raw_idea + classification)
6
+ # Output: JSON with research findings added to stdout
7
+ # Graceful degradation: if web_search fails, research={} and pipeline continues
8
+
9
+ GENESIS_LOG="${GENESIS_LOG:-$HOME/.openclaw/workspace/logs/genesis.log}"
10
+ mkdir -p "$(dirname "$GENESIS_LOG")"
11
+ exec 2> >(tee -a "$GENESIS_LOG" >&2)
12
+
13
+ if [[ -n "${1:-}" && -f "${1:-}" ]]; then
14
+ INPUT="$(cat "$1")"
15
+ else
16
+ INPUT="$(cat)"
17
+ fi
18
+ SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
19
+ echo "=== $(date -Iseconds) | research-idea.sh | session=$SESSION_ID ===" >&2
20
+
21
+ RAW_IDEA="$(echo "$INPUT" | jq -r '.raw_idea // .idea // ""')"
22
+ TYPE="$(echo "$INPUT" | jq -r '.classification.type // "feature"')"
23
+
24
+ echo >&2 "[research] session=$SESSION_ID type=$TYPE idea=$(echo "$RAW_IDEA" | cut -c1-80)..."
25
+
26
+ # Check if GEMINI_API_KEY is set (loaded by gateway from .env)
27
+ # If not, passthrough — no blocking
28
+ if [[ -z "${GEMINI_API_KEY:-}" ]]; then
29
+ echo >&2 "[research] GEMINI_API_KEY not set — skipping web research (passthrough)"
30
+ echo "$INPUT" | jq '. + { step: "research", research: {} }'
31
+ exit 0
32
+ fi
33
+
34
+ # Build research prompt for the agent
35
+ RESEARCH_PROMPT="You have access to the web_search tool. Use it to research this software project idea.
36
+
37
+ Idea: $RAW_IDEA
38
+ Type: $TYPE
39
+
40
+ 1. Search for the main technologies/frameworks mentioned or implied
41
+ 2. Search for best practices and architecture patterns for this type of project
42
+ 3. Synthesize your findings
43
+
44
+ Return ONLY valid JSON (no markdown fences, no explanation):
45
+ {
46
+ \"technologies\": [\"tech1 - brief description\", \"tech2 - brief description\"],
47
+ \"best_practices\": [\"practice1\", \"practice2\"],
48
+ \"architecture_patterns\": [\"pattern1\", \"pattern2\"],
49
+ \"references\": [{\"title\": \"...\", \"url\": \"...\"}, {\"title\": \"...\", \"url\": \"...\"}]
50
+ }"
51
+
52
+ RESEARCH="{}"
53
+
54
+ if command -v openclaw &>/dev/null; then
55
+ echo >&2 "[research] Calling web_search via openclaw agent --local..."
56
+ LLM_RAW="$(timeout 90 openclaw agent --local \
57
+ -m "$RESEARCH_PROMPT" \
58
+ --session-id "genesis-research-${SESSION_ID}" \
59
+ --json 2>/dev/null)" || {
60
+ echo >&2 "[research] openclaw agent call failed (exit $?) — continuing with empty research"
61
+ LLM_RAW=""
62
+ }
63
+
64
+ if [[ -n "$LLM_RAW" ]]; then
65
+ LLM_TEXT="$(echo "$LLM_RAW" | jq -r '.payloads[0].text // empty' 2>/dev/null || true)"
66
+ # Strip markdown fences if present
67
+ LLM_TEXT="$(echo "$LLM_TEXT" | sed '/^```/d; /^json$/d')"
68
+
69
+ if [[ -n "$LLM_TEXT" ]] && echo "$LLM_TEXT" | jq -e '.' &>/dev/null 2>&1; then
70
+ RESEARCH="$LLM_TEXT"
71
+ TECH_COUNT="$(echo "$RESEARCH" | jq '.technologies // [] | length')"
72
+ REF_COUNT="$(echo "$RESEARCH" | jq '.references // [] | length')"
73
+ echo >&2 "[research] Success: $TECH_COUNT technologies, $REF_COUNT references found"
74
+ else
75
+ echo >&2 "[research] LLM returned invalid JSON — continuing with empty research"
76
+ fi
77
+ fi
78
+ else
79
+ echo >&2 "[research] openclaw not found — skipping web research"
80
+ fi
81
+
82
+ # Merge research into the pipeline envelope
83
+ echo "$INPUT" | jq --argjson research "$RESEARCH" \
84
+ '. + { step: "research", research: $research }'