@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,797 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Step 9: Create GitHub issue from session state
5
+ # Input: stdin JSON (complete session state)
6
+ # Output: JSON with issue data to stdout
7
+ # Requires: openclaw CLI + DevClaw plugin (deterministic path),
8
+ # gh CLI authenticated (idempotency checks/fallback),
9
+ # GENESIS_REPO_URL in env or metadata
10
+
11
+ GENESIS_LOG="${GENESIS_LOG:-$HOME/.openclaw/workspace/logs/genesis.log}"
12
+ mkdir -p "$(dirname "$GENESIS_LOG")"
13
+ exec 2> >(tee -a "$GENESIS_LOG" >&2)
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ source "$SCRIPT_DIR/sideband-lib.sh"
17
+ source "$SCRIPT_DIR/genesis-telemetry.sh"
18
+
19
+ genesis_normalize_text() {
20
+ tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/ /g; s/^[[:space:]]+//; s/[[:space:]]+$//; s/[[:space:]]+/ /g'
21
+ }
22
+
23
+ genesis_sha1() {
24
+ if command -v sha1sum >/dev/null 2>&1; then
25
+ sha1sum | awk '{print $1}'
26
+ return 0
27
+ fi
28
+ if command -v shasum >/dev/null 2>&1; then
29
+ shasum -a 1 | awk '{print $1}'
30
+ return 0
31
+ fi
32
+ return 1
33
+ }
34
+
35
+ genesis_backlog_label_for_type() {
36
+ local type="${1:-feature}"
37
+ case "$type" in
38
+ research)
39
+ printf '%s\n' "backlog:meta"
40
+ ;;
41
+ *)
42
+ printf '%s\n' "backlog:canonical"
43
+ ;;
44
+ esac
45
+ }
46
+
47
+ genesis_backlog_label_from_input() {
48
+ local input_json="${1:-}"
49
+ local spec_type="${2:-feature}"
50
+ local raw=""
51
+
52
+ raw="$(printf '%s' "$input_json" | jq -r '
53
+ .metadata.backlog.label
54
+ // .metadata.backlog.kind
55
+ // .backlog.label
56
+ // .backlog.kind
57
+ // empty
58
+ ' 2>/dev/null || true)"
59
+ raw="$(genesis_trim "$raw")"
60
+ if [[ -n "$raw" ]]; then
61
+ case "$raw" in
62
+ canonical|duplicate|tracking|meta)
63
+ printf 'backlog:%s\n' "$raw"
64
+ return 0
65
+ ;;
66
+ backlog:canonical|backlog:duplicate|backlog:tracking|backlog:meta)
67
+ printf '%s\n' "$raw"
68
+ return 0
69
+ ;;
70
+ esac
71
+ fi
72
+
73
+ genesis_backlog_label_for_type "$spec_type"
74
+ }
75
+
76
+ genesis_backlog_metadata_json() {
77
+ local input_json="${1:-}"
78
+ local project_slug="${2:-}"
79
+ local spec_type="${3:-feature}"
80
+ local default_series default_order
81
+
82
+ default_series="${project_slug:-genesis-default}"
83
+ default_order="10"
84
+
85
+ printf '%s' "$input_json" | jq -c \
86
+ --arg default_series "$default_series" \
87
+ --arg default_type "$spec_type" \
88
+ --argjson default_order "$default_order" '
89
+ def parse_refs($value):
90
+ if $value == null then []
91
+ elif ($value | type) == "array" then [$value[] | tonumber? // empty]
92
+ elif ($value | type) == "number" then [$value]
93
+ elif ($value | type) == "string" then (
94
+ $value
95
+ | split(",")
96
+ | map(gsub("^\\s+|\\s+$"; ""))
97
+ | map(select(length > 0))
98
+ | map(tonumber? // empty)
99
+ )
100
+ else []
101
+ end;
102
+ def parse_optional_refs($value):
103
+ (parse_refs($value)) as $refs
104
+ | if ($refs | length) > 0 then $refs else null end;
105
+ . as $root
106
+ | ($root.metadata.backlog // $root.backlog // {}) as $backlog
107
+ | {
108
+ series: (
109
+ $backlog.series
110
+ // $root.metadata.backlog_series
111
+ // $root.metadata.project_slug
112
+ // $root.project_slug
113
+ // $root.metadata.project_name
114
+ // $default_series
115
+ ),
116
+ order: (
117
+ $backlog.order
118
+ // $root.metadata.backlog_order
119
+ // $default_order
120
+ ),
121
+ dependsOn: (
122
+ parse_refs(
123
+ $backlog.dependsOn
124
+ // $backlog.depends_on
125
+ // $root.metadata.backlog_depends_on
126
+ )
127
+ ),
128
+ supersededBy: (
129
+ parse_optional_refs(
130
+ $backlog.supersededBy
131
+ // $backlog.superseded_by
132
+ // $root.metadata.backlog_superseded_by
133
+ )
134
+ )
135
+ }
136
+ | .order |= (tonumber? // $default_order)
137
+ ' 2>/dev/null
138
+ }
139
+
140
+ # Load .env if available
141
+ genesis_load_env_file "$HOME/.openclaw/.env"
142
+
143
+ # Dry-run: output preview without creating issue
144
+ if [[ "${GENESIS_DRY_RUN:-false}" == "true" ]]; then
145
+ if [[ -n "${1:-}" && -f "${1:-}" ]]; then
146
+ INPUT="$(cat "$1")"
147
+ else
148
+ INPUT="$(cat)"
149
+ fi
150
+ echo '{"step":"create_task","dry_run":true,"message":"Dry run — issue creation skipped. Pipeline complete.","session_id":"'"$(echo "$INPUT" | jq -r '.session_id')"'"}' >&1
151
+ exit 0
152
+ fi
153
+
154
+ if [[ -n "${1:-}" && -f "${1:-}" ]]; then
155
+ INPUT="$(cat "$1")"
156
+ else
157
+ INPUT="$(cat)"
158
+ fi
159
+ TARGET_RESOLUTION="$(genesis_resolve_canonical_target "$INPUT" || jq -n '{metadata:{}}')"
160
+ INPUT="$(printf '%s' "$INPUT" | jq --argjson resolved "$TARGET_RESOLUTION" '
161
+ .metadata = ((.metadata // {}) + ($resolved.metadata // {}))
162
+ ')"
163
+ SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
164
+ genesis_metric_start "create-task" "$SESSION_ID"
165
+ echo "=== $(date -Iseconds) | create-task.sh | session=$SESSION_ID ===" >&2
166
+ SPEC="$(echo "$INPUT" | jq '.spec // {}')"
167
+ SECURITY="$(echo "$INPUT" | jq '.security // {}')"
168
+ QA_CONTRACT="$(echo "$INPUT" | jq '.qa_contract // {}')"
169
+ METADATA="$(echo "$INPUT" | jq '.metadata // {}')"
170
+ CLASSIFICATION="$(echo "$INPUT" | jq '.classification // {}')"
171
+ INTERVIEW="$(echo "$INPUT" | jq '.interview // {}')"
172
+ IMPACT="$(echo "$INPUT" | jq '.impact // {}')"
173
+ PROJECT_MAP="$(echo "$INPUT" | jq '.project_map // {}')"
174
+ SPEC_TITLE_SIGNAL="$(echo "$SPEC" | jq -r '.title // ""')"
175
+ SPEC_OBJECTIVE_SIGNAL="$(echo "$SPEC" | jq -r '.objective // ""')"
176
+ SPEC_TYPE="$(echo "$SPEC" | jq -r '.type // "feature"')"
177
+ SPEC_DELIVERY_TARGET="$(echo "$SPEC" | jq -r '.delivery_target // "unknown"')"
178
+ FACTORY_CHANGE_FROM_SPEC=false
179
+ if genesis_payload_factory_change "$INPUT"; then
180
+ FACTORY_CHANGE_FROM_SPEC=true
181
+ elif genesis_request_is_factory_change "$(printf '%s\n%s\n' "$SPEC_TITLE_SIGNAL" "$SPEC_OBJECTIVE_SIGNAL")"; then
182
+ FACTORY_CHANGE_FROM_SPEC=true
183
+ fi
184
+
185
+ echo "Creating GitHub issue for session $SESSION_ID..." >&2
186
+
187
+ # Determine repo (prefer current pipeline state; avoid implicit default target for product flows)
188
+ REPO_URL="$(echo "$INPUT" | jq -r '.scaffold.repo_url // .metadata.repo_url // ""')"
189
+ CANDIDATE_PROJECT_SLUG="$(echo "$INPUT" | jq -r '.project_slug // .metadata.project_slug // .scaffold.project_slug // empty')"
190
+ if [[ -z "$CANDIDATE_PROJECT_SLUG" || "$CANDIDATE_PROJECT_SLUG" == "null" ]]; then
191
+ CANDIDATE_PROJECT_SLUG="$(echo "$INPUT" | jq -r '.metadata.project_name // empty')"
192
+ fi
193
+ REQUESTED_CHANNEL_ID="$(echo "$INPUT" | jq -r '.project_channel_id // empty')"
194
+ PROJECT_SLUG=""
195
+
196
+ # Sideband: if scaffold created a new repo, use that (validated + TTL-bound)
197
+ SCAFFOLD_PAYLOAD="$(genesis_sideband_read_payload "scaffold" "$SESSION_ID" "${GENESIS_SIDEBAND_TTL_SECONDS:-1800}" || true)"
198
+ if [[ -z "$REPO_URL" && -n "$SCAFFOLD_PAYLOAD" ]]; then
199
+ SCAFFOLD_REPO="$(echo "$SCAFFOLD_PAYLOAD" | jq -r '.scaffold.repo_url // empty')"
200
+ if [[ -n "$SCAFFOLD_REPO" ]]; then
201
+ REPO_URL="$SCAFFOLD_REPO"
202
+ echo "Using scaffolded repo: $REPO_URL" >&2
203
+ fi
204
+ fi
205
+
206
+ if [[ -z "$REPO_URL" ]]; then
207
+ if [[ -n "$CANDIDATE_PROJECT_SLUG" && "$CANDIDATE_PROJECT_SLUG" != "null" ]]; then
208
+ PROJECT_REF="$(genesis_project_resolve_ref "$CANDIDATE_PROJECT_SLUG" || true)"
209
+ if [[ -n "$PROJECT_REF" ]]; then
210
+ RESOLVED_REMOTE="$(printf '%s' "$PROJECT_REF" | cut -f3)"
211
+ if [[ -n "$RESOLVED_REMOTE" ]]; then
212
+ REPO_URL="$RESOLVED_REMOTE"
213
+ echo "Resolved repo from project slug '$CANDIDATE_PROJECT_SLUG': $REPO_URL" >&2
214
+ fi
215
+ fi
216
+ fi
217
+ fi
218
+ if [[ -z "$REPO_URL" ]]; then
219
+ if [[ "$FACTORY_CHANGE_FROM_SPEC" == "true" && -n "${GENESIS_REPO_URL:-}" ]]; then
220
+ REPO_URL="${GENESIS_REPO_URL}"
221
+ echo "Using GENESIS_REPO_URL fallback for factory/internal change." >&2
222
+ fi
223
+ fi
224
+ if [[ -z "$REPO_URL" ]]; then
225
+ echo "ERROR: No repo URL resolved from pipeline state (scaffold/metadata). Refusing implicit fallback for product request." >&2
226
+ exit 1
227
+ fi
228
+ if [[ -n "$REPO_URL" && "$REPO_URL" == "~"* ]]; then
229
+ REPO_URL="$(genesis_expand_path "$REPO_URL")"
230
+ fi
231
+
232
+ # If repo URL was not explicit owner/repo, resolve project ref deterministically.
233
+ if [[ -n "$REPO_URL" ]] && ! genesis_parse_owner_repo "$REPO_URL" >/dev/null 2>&1; then
234
+ PROJECT_REF="$(genesis_project_resolve_ref "$REPO_URL" || true)"
235
+ if [[ -n "$PROJECT_REF" ]]; then
236
+ PROJECT_SLUG="$(printf '%s' "$PROJECT_REF" | cut -f1)"
237
+ RESOLVED_REMOTE="$(printf '%s' "$PROJECT_REF" | cut -f3)"
238
+ if [[ -n "$RESOLVED_REMOTE" ]]; then
239
+ REPO_URL="$RESOLVED_REMOTE"
240
+ echo "Resolved repo reference via project map: $REPO_URL" >&2
241
+ fi
242
+ fi
243
+ fi
244
+
245
+ # Extract owner/repo from URL
246
+ OWNER_REPO="$(genesis_parse_owner_repo "$REPO_URL" || true)"
247
+ if [[ -z "$OWNER_REPO" ]]; then
248
+ echo "ERROR: Invalid GitHub repository reference: $REPO_URL" >&2
249
+ exit 1
250
+ fi
251
+ OWNER="$(echo "$OWNER_REPO" | cut -d/ -f1)"
252
+ REPO="$(echo "$OWNER_REPO" | cut -d/ -f2)"
253
+
254
+ # Resolve project slug deterministically from projects.json.
255
+ REPO_PROJECT_SLUG="$(genesis_find_project_slug_by_repo "$REPO_URL" || true)"
256
+ if [[ -n "$REPO_PROJECT_SLUG" ]]; then
257
+ if [[ -n "$CANDIDATE_PROJECT_SLUG" && "$CANDIDATE_PROJECT_SLUG" != "$REPO_PROJECT_SLUG" ]]; then
258
+ echo "WARNING: Provided project slug '$CANDIDATE_PROJECT_SLUG' does not match repo mapping; using '$REPO_PROJECT_SLUG'." >&2
259
+ fi
260
+ PROJECT_SLUG="$REPO_PROJECT_SLUG"
261
+ elif [[ -n "$CANDIDATE_PROJECT_SLUG" ]] && genesis_project_exists "$CANDIDATE_PROJECT_SLUG"; then
262
+ PROJECT_SLUG="$CANDIDATE_PROJECT_SLUG"
263
+ elif [[ -n "$CANDIDATE_PROJECT_SLUG" ]]; then
264
+ echo "WARNING: Provided project slug '$CANDIDATE_PROJECT_SLUG' is not registered in projects.json." >&2
265
+ fi
266
+
267
+ PROJECT_KIND="implementation"
268
+ PROJECT_ARCHIVED="false"
269
+ if [[ -n "$PROJECT_SLUG" ]] && genesis_project_exists "$PROJECT_SLUG"; then
270
+ PROJECT_KIND="$(genesis_project_kind "$PROJECT_SLUG" || echo implementation)"
271
+ PROJECT_ARCHIVED="$(genesis_project_archived "$PROJECT_SLUG" || echo false)"
272
+ fi
273
+
274
+ if [[ "$PROJECT_ARCHIVED" == "true" ]]; then
275
+ echo "ERROR: Target project \"$PROJECT_SLUG\" is archived and cannot receive new issues. Redirect to the canonical active project before retrying." >&2
276
+ exit 1
277
+ fi
278
+
279
+ if [[ "$PROJECT_KIND" == "pointer" ]] && [[ "$SPEC_TYPE" != "research" ]]; then
280
+ echo "ERROR: Target project \"$PROJECT_SLUG\" is marked as pointer/scaffold and cannot receive implementation issues." >&2
281
+ exit 1
282
+ fi
283
+
284
+ if [[ -n "$PROJECT_SLUG" ]] && genesis_is_factory_project_slug "$PROJECT_SLUG"; then
285
+ if ! genesis_payload_factory_change "$INPUT"; then
286
+ echo "ERROR: Target project \"$PROJECT_SLUG\" is reserved for Factory-internal changes. User/product requests must target a dedicated project repository." >&2
287
+ exit 1
288
+ fi
289
+ fi
290
+
291
+ PROJECT_CHANNEL_ID=""
292
+ if [[ -n "$PROJECT_SLUG" ]]; then
293
+ PROJECT_CHANNEL_ID="$(genesis_project_channel_id "$PROJECT_SLUG" "$REQUESTED_CHANNEL_ID" || true)"
294
+ if [[ -n "$REQUESTED_CHANNEL_ID" && -n "$PROJECT_CHANNEL_ID" && "$REQUESTED_CHANNEL_ID" != "$PROJECT_CHANNEL_ID" ]]; then
295
+ echo "WARNING: Requested channel '$REQUESTED_CHANNEL_ID' is not valid for '$PROJECT_SLUG'; using '$PROJECT_CHANNEL_ID' from projects.json." >&2
296
+ fi
297
+ fi
298
+ USE_DEVCLAW_TASKS=false
299
+ if [[ -n "$PROJECT_SLUG" && -n "$PROJECT_CHANNEL_ID" ]] && genesis_openclaw_bin >/dev/null 2>&1 && genesis_openclaw_supports devclaw task; then
300
+ USE_DEVCLAW_TASKS=true
301
+ fi
302
+
303
+ echo "Target: $OWNER/$REPO" >&2
304
+
305
+ # Lock by repo+session to avoid duplicate issue creation under concurrent runs.
306
+ CREATE_LOCK_DIR="$HOME/.openclaw/workspace/devclaw/log"
307
+ LOCK_SAFE_KEY="$(printf '%s_%s_%s' "$OWNER" "$REPO" "$SESSION_ID" | tr -c 'A-Za-z0-9._-' '_')"
308
+ CREATE_LOCK_FILE="$CREATE_LOCK_DIR/create-task-${LOCK_SAFE_KEY}.lock"
309
+ mkdir -p "$CREATE_LOCK_DIR"
310
+ if command -v flock >/dev/null 2>&1; then
311
+ exec 9>"$CREATE_LOCK_FILE"
312
+ flock -x 9
313
+ fi
314
+
315
+ # === Idempotency check: prevent duplicate issues for the same session ===
316
+ echo "Checking for existing issue with session_id $SESSION_ID..." >&2
317
+
318
+ EXISTING_ISSUE="$(gh issue list \
319
+ --repo "$OWNER/$REPO" \
320
+ --state all \
321
+ --search "Session: $SESSION_ID in:body" \
322
+ --json number,url,title,state \
323
+ --limit 1 2>/dev/null || echo "[]")"
324
+
325
+ EXISTING_NUMBER="$(echo "$EXISTING_ISSUE" | jq -r '.[0].number // empty' 2>/dev/null || true)"
326
+
327
+ if [[ -n "$EXISTING_NUMBER" ]]; then
328
+ EXISTING_URL="$(echo "$EXISTING_ISSUE" | jq -r '.[0].url')"
329
+ EXISTING_TITLE="$(echo "$EXISTING_ISSUE" | jq -r '.[0].title')"
330
+ EXISTING_STATE="$(echo "$EXISTING_ISSUE" | jq -r '.[0].state')"
331
+
332
+ echo "Found existing issue #$EXISTING_NUMBER ($EXISTING_STATE) for session $SESSION_ID — skipping creation" >&2
333
+
334
+ # Reopen if closed
335
+ if [[ "$EXISTING_STATE" == "CLOSED" ]]; then
336
+ echo "Reopening existing issue #$EXISTING_NUMBER..." >&2
337
+ gh issue reopen "$EXISTING_NUMBER" --repo "$OWNER/$REPO" >/dev/null 2>&1 || true
338
+ fi
339
+
340
+ jq -n \
341
+ --arg sid "$SESSION_ID" \
342
+ --argjson num "$EXISTING_NUMBER" \
343
+ --arg title "$EXISTING_TITLE" \
344
+ --arg url "$EXISTING_URL" \
345
+ --arg project_slug "$PROJECT_SLUG" \
346
+ --arg project_channel_id "$PROJECT_CHANNEL_ID" \
347
+ --arg labels "Planning" \
348
+ --argjson factory_change "$([[ "$FACTORY_CHANGE_FROM_SPEC" == "true" ]] && echo "true" || echo "false")" \
349
+ --argjson spec "$SPEC" \
350
+ --argjson cls "$CLASSIFICATION" \
351
+ --argjson interview "$INTERVIEW" \
352
+ --argjson impact "$IMPACT" \
353
+ --argjson qa "$QA_CONTRACT" \
354
+ --argjson sec "$SECURITY" \
355
+ --argjson map "$PROJECT_MAP" \
356
+ --argjson meta "$METADATA" \
357
+ '{
358
+ session_id: $sid,
359
+ step: "create_task",
360
+ duplicate_prevented: true,
361
+ factory_change: $factory_change,
362
+ project_slug: (if $project_slug != "" then $project_slug else null end),
363
+ project_channel_id: (if $project_channel_id != "" then $project_channel_id else null end),
364
+ issues: [{
365
+ number: $num,
366
+ title: $title,
367
+ url: $url,
368
+ labels: ($labels | split(",")),
369
+ state: "open"
370
+ }],
371
+ spec: $spec,
372
+ classification: $cls,
373
+ interview: $interview,
374
+ impact: $impact,
375
+ qa_contract: $qa,
376
+ security: $sec,
377
+ project_map: $map,
378
+ metadata: $meta
379
+ }'
380
+ exit 0
381
+ fi
382
+ # === End idempotency check ===
383
+
384
+ # Extract spec fields
385
+ TITLE="$(echo "$SPEC" | jq -r '.title')"
386
+ TYPE="$(echo "$SPEC" | jq -r '.type')"
387
+ OBJECTIVE="$(echo "$SPEC" | jq -r '.objective')"
388
+ CONSTRAINTS="$(echo "$SPEC" | jq -r '.constraints // "None"')"
389
+ DELIVERY_TARGET="$(echo "$SPEC" | jq -r '.delivery_target // "unknown"')"
390
+
391
+ DEDUPE_SOURCE="$OBJECTIVE"
392
+ if [[ -z "${DEDUPE_SOURCE// }" || "$DEDUPE_SOURCE" == "null" ]]; then
393
+ DEDUPE_SOURCE="$TITLE"
394
+ fi
395
+ DEDUPE_KEY=""
396
+ DEDUPE_NORM="$(printf '%s' "$DEDUPE_SOURCE" | genesis_normalize_text | cut -c1-240)"
397
+ if [[ -n "$DEDUPE_NORM" ]]; then
398
+ DEDUPE_KEY="$(printf '%s' "$DEDUPE_NORM" | genesis_sha1 || true)"
399
+ fi
400
+
401
+ if [[ -n "$DEDUPE_KEY" ]]; then
402
+ EXISTING_DEDUPE_ISSUE="$(gh issue list \
403
+ --repo "$OWNER/$REPO" \
404
+ --state open \
405
+ --search "dedupe-key:$DEDUPE_KEY in:body" \
406
+ --json number,url,title,state \
407
+ --limit 1 2>/dev/null || echo "[]")"
408
+ DEDUPE_NUMBER="$(echo "$EXISTING_DEDUPE_ISSUE" | jq -r '.[0].number // empty' 2>/dev/null || true)"
409
+ if [[ -n "$DEDUPE_NUMBER" ]]; then
410
+ DEDUPE_URL="$(echo "$EXISTING_DEDUPE_ISSUE" | jq -r '.[0].url')"
411
+ DEDUPE_TITLE="$(echo "$EXISTING_DEDUPE_ISSUE" | jq -r '.[0].title')"
412
+ echo "Found existing open issue #$DEDUPE_NUMBER by dedupe-key ($DEDUPE_KEY) — skipping creation" >&2
413
+ jq -n \
414
+ --arg sid "$SESSION_ID" \
415
+ --argjson num "$DEDUPE_NUMBER" \
416
+ --arg title "$DEDUPE_TITLE" \
417
+ --arg url "$DEDUPE_URL" \
418
+ --arg project_slug "$PROJECT_SLUG" \
419
+ --arg project_channel_id "$PROJECT_CHANNEL_ID" \
420
+ --arg labels "Planning" \
421
+ --arg dedupe_key "$DEDUPE_KEY" \
422
+ --argjson factory_change "$([[ "$FACTORY_CHANGE_FROM_SPEC" == "true" ]] && echo "true" || echo "false")" \
423
+ --argjson spec "$SPEC" \
424
+ --argjson cls "$CLASSIFICATION" \
425
+ --argjson interview "$INTERVIEW" \
426
+ --argjson impact "$IMPACT" \
427
+ --argjson qa "$QA_CONTRACT" \
428
+ --argjson sec "$SECURITY" \
429
+ --argjson map "$PROJECT_MAP" \
430
+ --argjson meta "$METADATA" \
431
+ '{
432
+ session_id: $sid,
433
+ step: "create_task",
434
+ duplicate_prevented: true,
435
+ duplicate_reason: "dedupe-key",
436
+ dedupe_key: $dedupe_key,
437
+ factory_change: $factory_change,
438
+ project_slug: (if $project_slug != "" then $project_slug else null end),
439
+ project_channel_id: (if $project_channel_id != "" then $project_channel_id else null end),
440
+ issues: [{
441
+ number: $num,
442
+ title: $title,
443
+ url: $url,
444
+ labels: ($labels | split(",")),
445
+ state: "open"
446
+ }],
447
+ spec: $spec,
448
+ classification: $cls,
449
+ interview: $interview,
450
+ impact: $impact,
451
+ qa_contract: $qa,
452
+ security: $sec,
453
+ project_map: $map,
454
+ metadata: $meta
455
+ }'
456
+ exit 0
457
+ fi
458
+ fi
459
+
460
+ # Build issue body using template-like expansion
461
+ BACKLOG_LABEL="$(genesis_backlog_label_from_input "$INPUT" "$TYPE")"
462
+ BACKLOG_METADATA_JSON="$(genesis_backlog_metadata_json "$INPUT" "$PROJECT_SLUG" "$TYPE")"
463
+ if [[ -z "$BACKLOG_METADATA_JSON" || "$BACKLOG_METADATA_JSON" == "null" ]]; then
464
+ BACKLOG_METADATA_JSON="$(jq -cn --arg series "${PROJECT_SLUG:-genesis-default}" '{series: $series, order: 10, dependsOn: [], supersededBy: null}')"
465
+ fi
466
+ METADATA="$(printf '%s' "$METADATA" | jq -c --arg label "$BACKLOG_LABEL" --argjson backlog "$BACKLOG_METADATA_JSON" '
467
+ (. // {}) + {backlog: (($backlog + {label: $label}) | with_entries(select(.value != "")))}
468
+ ')"
469
+ BODY="## Objetivo
470
+
471
+ $OBJECTIVE
472
+
473
+ ## Tipo de Entrega
474
+
475
+ $DELIVERY_TARGET
476
+
477
+ ## Escopo V1
478
+
479
+ $(echo "$SPEC" | jq -r '.scope_v1 // [] | .[] | "- " + .')
480
+
481
+ ## Fora de Escopo
482
+
483
+ $(echo "$SPEC" | jq -r '.out_of_scope // [] | .[] | "- " + .')
484
+
485
+ ## Acceptance Criteria
486
+
487
+ $(echo "$SPEC" | jq -r '.acceptance_criteria // [] | .[] | "- [ ] " + .')
488
+
489
+ ## Definition of Done
490
+
491
+ $(echo "$SPEC" | jq -r '.definition_of_done // [] | .[] | "- [ ] " + .')
492
+
493
+ ## Restrições
494
+
495
+ $CONSTRAINTS"
496
+
497
+ if [[ -n "$DEDUPE_KEY" ]]; then
498
+ BODY="$BODY
499
+
500
+ <!-- dedupe-key:$DEDUPE_KEY -->"
501
+ fi
502
+
503
+ BODY="$BODY
504
+
505
+ <!-- devclaw-backlog: $BACKLOG_METADATA_JSON -->"
506
+
507
+ # Add risks if present
508
+ RISKS="$(echo "$SPEC" | jq -r '.risks // [] | .[]')"
509
+ if [[ -n "$RISKS" ]]; then
510
+ BODY="$BODY
511
+
512
+ ## Riscos
513
+
514
+ $(echo "$SPEC" | jq -r '.risks[] | "- " + .')"
515
+ fi
516
+
517
+ # Add security notes if present
518
+ SEC_NOTES="$(echo "$SECURITY" | jq -r '.spec_security_notes // [] | .[]')"
519
+ if [[ -n "$SEC_NOTES" ]]; then
520
+ BODY="$BODY
521
+
522
+ ## Security Notes
523
+
524
+ $(echo "$SECURITY" | jq -r '.spec_security_notes[] | "- " + .')"
525
+ fi
526
+
527
+ BODY="$BODY
528
+
529
+ ---
530
+ _Generated by Genesis Flow | Session: ${SESSION_ID}_"
531
+
532
+ # Determine labels
533
+ DELIVERY_LABEL="delivery:implementation"
534
+ if [[ "$TYPE" == "research" ]]; then
535
+ DELIVERY_LABEL="delivery:research"
536
+ fi
537
+ LABELS="Planning,type:$TYPE,$DELIVERY_LABEL"
538
+ EXTRA_LABELS="type:$TYPE,$DELIVERY_LABEL"
539
+ LABELS="$LABELS,$BACKLOG_LABEL"
540
+ EXTRA_LABELS="$EXTRA_LABELS,$BACKLOG_LABEL"
541
+ if [[ "$TYPE" == "research" ]]; then
542
+ LABELS="$LABELS,no-pr-required"
543
+ EXTRA_LABELS="$EXTRA_LABELS,no-pr-required"
544
+ fi
545
+ SEC_REC="$(echo "$SECURITY" | jq -r '.recommendation // ""')"
546
+ if [[ "$SEC_REC" == HIGH* ]]; then
547
+ LABELS="$LABELS,security:high"
548
+ EXTRA_LABELS="$EXTRA_LABELS,security:high"
549
+ fi
550
+ FACTORY_CHANGE="$FACTORY_CHANGE_FROM_SPEC"
551
+ FACTORY_CHANGE_FLAG=()
552
+ if printf '%s\n%s\n' "$TITLE" "$BODY" | tr '[:upper:]' '[:lower:]' | grep -Eq 'factory|openclaw|devclaw|workflow|pipeline|orchestr'; then
553
+ FACTORY_CHANGE=true
554
+ FACTORY_CHANGE_FLAG=(--factory-change)
555
+ fi
556
+
557
+ echo "Creating issue: '$TITLE' with labels: $LABELS" >&2
558
+
559
+ ISSUE_NUMBER="0"
560
+ ISSUE_URL=""
561
+ if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
562
+ echo "Creating issue via deterministic DevClaw task_create..." >&2
563
+ BODY_FILE="$(mktemp)"
564
+ printf '%s\n' "$BODY" > "$BODY_FILE"
565
+ TASK_CREATE_CMD=(create --project "$PROJECT_SLUG" --title "$TITLE" --body-file "$BODY_FILE")
566
+ if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
567
+ TASK_CREATE_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
568
+ fi
569
+ if [[ "${#FACTORY_CHANGE_FLAG[@]}" -gt 0 ]]; then
570
+ TASK_CREATE_CMD+=("${FACTORY_CHANGE_FLAG[@]}")
571
+ fi
572
+ TASK_CREATE_JSON="$(
573
+ genesis_devclaw_task_json \
574
+ "${TASK_CREATE_CMD[@]}" \
575
+ 2>>"$GENESIS_LOG"
576
+ )" || {
577
+ rm -f "$BODY_FILE"
578
+ echo "ERROR: DevClaw task_create failed" >&2
579
+ exit 1
580
+ }
581
+ rm -f "$BODY_FILE"
582
+
583
+ ISSUE_NUMBER="$(echo "$TASK_CREATE_JSON" | jq -r '.issue.id // 0' 2>/dev/null || echo 0)"
584
+ ISSUE_URL="$(echo "$TASK_CREATE_JSON" | jq -r '.issue.url // ""' 2>/dev/null || echo "")"
585
+ if [[ "$ISSUE_NUMBER" != "0" && -n "$EXTRA_LABELS" ]]; then
586
+ echo "Applying extra labels via deterministic DevClaw task labels: $EXTRA_LABELS" >&2
587
+ TASK_LABELS_CMD=(labels --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --add "$EXTRA_LABELS")
588
+ if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
589
+ TASK_LABELS_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
590
+ fi
591
+ if [[ "${#FACTORY_CHANGE_FLAG[@]}" -gt 0 ]]; then
592
+ TASK_LABELS_CMD+=("${FACTORY_CHANGE_FLAG[@]}")
593
+ fi
594
+ genesis_devclaw_task_json \
595
+ "${TASK_LABELS_CMD[@]}" \
596
+ >/dev/null 2>>"$GENESIS_LOG" || {
597
+ echo "ERROR: DevClaw task labels failed" >&2
598
+ exit 1
599
+ }
600
+ fi
601
+ else
602
+ echo "DevClaw deterministic mode unavailable (missing project slug/channel or openclaw bin); falling back to gh issue create." >&2
603
+ gh label create "$DELIVERY_LABEL" --repo "$OWNER/$REPO" --color 5319E7 --force >/dev/null 2>>"$GENESIS_LOG" || true
604
+ gh label create "$BACKLOG_LABEL" --repo "$OWNER/$REPO" --color BFD4F2 --force >/dev/null 2>>"$GENESIS_LOG" || true
605
+ if [[ "$TYPE" == "research" ]]; then
606
+ gh label create "no-pr-required" --repo "$OWNER/$REPO" --color 0e8a16 --force >/dev/null 2>>"$GENESIS_LOG" || true
607
+ fi
608
+ ISSUE_OUTPUT="$(gh issue create \
609
+ --repo "$OWNER/$REPO" \
610
+ --title "$TITLE" \
611
+ --body "$BODY" \
612
+ --label "$LABELS" \
613
+ 2>&1)" || {
614
+ echo "ERROR: gh issue create failed: $ISSUE_OUTPUT" >&2
615
+ exit 1
616
+ }
617
+
618
+ ISSUE_URL="$(printf '%s\n' "$ISSUE_OUTPUT" | grep -Eo 'https://github\.com/[^[:space:]]+/issues/[0-9]+' | head -1 || true)"
619
+ ISSUE_NUMBER="$(printf '%s' "$ISSUE_URL" | grep -Eo '/issues/[0-9]+' | tr -dc '0-9' || true)"
620
+ if [[ -z "$ISSUE_NUMBER" ]]; then
621
+ ISSUE_NUMBER="0"
622
+ fi
623
+ fi
624
+
625
+ if [[ "$ISSUE_NUMBER" == "0" || -z "$ISSUE_URL" ]]; then
626
+ # Fallback: refetch by session marker to avoid abort on parser mismatch.
627
+ REFETCH_ISSUE="$(gh issue list \
628
+ --repo "$OWNER/$REPO" \
629
+ --state all \
630
+ --search "Session: $SESSION_ID in:body" \
631
+ --json number,url \
632
+ --limit 1 2>/dev/null || echo "[]")"
633
+ ISSUE_NUMBER="$(echo "$REFETCH_ISSUE" | jq -r '.[0].number // 0')"
634
+ ISSUE_URL="$(echo "$REFETCH_ISSUE" | jq -r '.[0].url // ""')"
635
+ fi
636
+
637
+ if [[ "$ISSUE_NUMBER" == "0" || -z "$ISSUE_URL" ]]; then
638
+ echo "ERROR: Could not resolve created issue number/url" >&2
639
+ exit 1
640
+ fi
641
+
642
+ echo "Created issue #$ISSUE_NUMBER: $ISSUE_URL" >&2
643
+
644
+ # Add QA contract as a comment
645
+ QA_SCRIPT="$(echo "$QA_CONTRACT" | jq -r '.script_content // ""')"
646
+ if [[ -n "$QA_SCRIPT" ]] && [[ "$QA_SCRIPT" != "null" ]]; then
647
+ echo "Attaching QA contract as comment..." >&2
648
+ QA_COMMENT="## QA Contract (scripts/qa.sh)
649
+
650
+ \`\`\`bash
651
+ $QA_SCRIPT
652
+ \`\`\`
653
+
654
+ **Gates:** $(echo "$QA_CONTRACT" | jq -r '.gates // [] | join(", ")')
655
+ **Coverage threshold:** $(echo "$QA_CONTRACT" | jq -r '.coverage_threshold // 80')%"
656
+
657
+ if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
658
+ echo "QA comment via DevClaw task_comment..." >&2
659
+ QA_COMMENT_FILE="$(mktemp)"
660
+ printf '%s\n' "$QA_COMMENT" > "$QA_COMMENT_FILE"
661
+ TASK_COMMENT_QA_CMD=(comment --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --body-file "$QA_COMMENT_FILE")
662
+ if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
663
+ TASK_COMMENT_QA_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
664
+ fi
665
+ genesis_devclaw_task_json \
666
+ "${TASK_COMMENT_QA_CMD[@]}" \
667
+ >/dev/null 2>>"$GENESIS_LOG" || echo "WARNING: Failed to attach QA comment via task_comment" >&2
668
+ rm -f "$QA_COMMENT_FILE"
669
+ else
670
+ echo "QA comment via gh issue comment..." >&2
671
+ gh issue comment "$ISSUE_NUMBER" \
672
+ --repo "$OWNER/$REPO" \
673
+ --body "$QA_COMMENT" >/dev/null 2>>"$GENESIS_LOG" || echo "WARNING: Failed to attach QA comment" >&2
674
+ fi
675
+ echo "QA comment step finished." >&2
676
+ fi
677
+
678
+ # Attach map/impact summary as a comment (compact; avoid dumping huge JSON).
679
+ FILES_SCANNED="$(echo "$PROJECT_MAP" | jq -r '.stats.files_scanned // 0' 2>/dev/null || echo 0)"
680
+ SYMBOLS_FOUND="$(echo "$PROJECT_MAP" | jq -r '.stats.symbols_found // 0' 2>/dev/null || echo 0)"
681
+ LANGUAGES="$(echo "$PROJECT_MAP" | jq -r '.stats.languages // [] | join(", ")' 2>/dev/null || echo "")"
682
+ MAP_ROOT="$(echo "$PROJECT_MAP" | jq -r '.root // ""' 2>/dev/null || echo "")"
683
+
684
+ IS_GREENFIELD="$(echo "$IMPACT" | jq -r '.is_greenfield // false' 2>/dev/null || echo false)"
685
+ ESTIMATED_CHANGED="$(echo "$IMPACT" | jq -r '.estimated_files_changed // ""' 2>/dev/null || echo "")"
686
+ RISK_AREAS="$(echo "$IMPACT" | jq -r '.risk_areas // [] | .[:10] | .[]' 2>/dev/null || true)"
687
+ NEW_FILES="$(echo "$IMPACT" | jq -r '.new_files_needed // [] | .[:10] | .[]' 2>/dev/null || true)"
688
+ AFFECTED_FILES="$(echo "$IMPACT" | jq -r '.affected_files // [] | .[:25] | .[]' 2>/dev/null || true)"
689
+
690
+ MAP_HAS_SIGNAL=false
691
+ if [[ "$FILES_SCANNED" != "0" || "$SYMBOLS_FOUND" != "0" || -n "$LANGUAGES" || -n "$MAP_ROOT" ]]; then
692
+ MAP_HAS_SIGNAL=true
693
+ fi
694
+
695
+ IMPACT_HAS_SIGNAL=false
696
+ if [[ -n "$ESTIMATED_CHANGED" || -n "$RISK_AREAS" || -n "$NEW_FILES" || -n "$AFFECTED_FILES" ]]; then
697
+ IMPACT_HAS_SIGNAL=true
698
+ fi
699
+
700
+ if $MAP_HAS_SIGNAL || $IMPACT_HAS_SIGNAL; then
701
+ echo "Attaching map/impact summary as comment..." >&2
702
+
703
+ MAP_LINES=""
704
+ if [[ -n "$MAP_ROOT" ]]; then
705
+ MAP_LINES="$MAP_LINES\n**Map root:** $MAP_ROOT"
706
+ fi
707
+ MAP_LINES="$MAP_LINES\n**Map stats:** files_scanned=$FILES_SCANNED, symbols_found=$SYMBOLS_FOUND"
708
+ if [[ -n "$LANGUAGES" ]]; then
709
+ MAP_LINES="$MAP_LINES\n**Languages:** $LANGUAGES"
710
+ fi
711
+
712
+ IMPACT_LINES="\n**Greenfield:** $IS_GREENFIELD"
713
+ if [[ -n "$ESTIMATED_CHANGED" ]]; then
714
+ IMPACT_LINES="$IMPACT_LINES\n**Estimated files changed:** $ESTIMATED_CHANGED"
715
+ fi
716
+
717
+ LIST_BLOCKS=""
718
+ if [[ -n "$AFFECTED_FILES" ]]; then
719
+ LIST_BLOCKS="$LIST_BLOCKS\n\n### Affected files (top 25)\n$(printf '%s\n' "$AFFECTED_FILES" | sed 's/^/- /')\n"
720
+ fi
721
+ if [[ -n "$NEW_FILES" ]]; then
722
+ LIST_BLOCKS="$LIST_BLOCKS\n\n### New files needed (top 10)\n$(printf '%s\n' "$NEW_FILES" | sed 's/^/- /')\n"
723
+ fi
724
+ if [[ -n "$RISK_AREAS" ]]; then
725
+ LIST_BLOCKS="$LIST_BLOCKS\n\n### Risk areas (top 10)\n$(printf '%s\n' "$RISK_AREAS" | sed 's/^/- /')\n"
726
+ fi
727
+
728
+ IMPACT_COMMENT="## Map/Impact Summary
729
+ $MAP_LINES
730
+
731
+ $IMPACT_LINES
732
+ $LIST_BLOCKS"
733
+
734
+ if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
735
+ echo "Map/impact comment via DevClaw task_comment..." >&2
736
+ IMPACT_COMMENT_FILE="$(mktemp)"
737
+ printf '%s\n' "$IMPACT_COMMENT" > "$IMPACT_COMMENT_FILE"
738
+ TASK_COMMENT_IMPACT_CMD=(comment --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --body-file "$IMPACT_COMMENT_FILE")
739
+ if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
740
+ TASK_COMMENT_IMPACT_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
741
+ fi
742
+ genesis_devclaw_task_json \
743
+ "${TASK_COMMENT_IMPACT_CMD[@]}" \
744
+ >/dev/null 2>>"$GENESIS_LOG" || echo "WARNING: Failed to attach impact/map comment via task_comment" >&2
745
+ rm -f "$IMPACT_COMMENT_FILE"
746
+ else
747
+ echo "Map/impact comment via gh issue comment..." >&2
748
+ gh issue comment "$ISSUE_NUMBER" \
749
+ --repo "$OWNER/$REPO" \
750
+ --body "$IMPACT_COMMENT" >/dev/null 2>>"$GENESIS_LOG" || echo "WARNING: Failed to attach impact/map comment" >&2
751
+ fi
752
+ echo "Map/impact comment step finished." >&2
753
+ fi
754
+
755
+ echo "create-task.sh completed for session $SESSION_ID" >&2
756
+
757
+ jq -n \
758
+ --arg sid "$SESSION_ID" \
759
+ --argjson num "$ISSUE_NUMBER" \
760
+ --arg title "$TITLE" \
761
+ --arg url "$ISSUE_URL" \
762
+ --arg project_slug "$PROJECT_SLUG" \
763
+ --arg project_channel_id "$PROJECT_CHANNEL_ID" \
764
+ --arg labels "$LABELS" \
765
+ --argjson factory_change "$([[ "$FACTORY_CHANGE" == "true" ]] && echo "true" || echo "false")" \
766
+ --argjson spec "$SPEC" \
767
+ --argjson cls "$CLASSIFICATION" \
768
+ --argjson interview "$INTERVIEW" \
769
+ --argjson impact "$IMPACT" \
770
+ --argjson qa "$QA_CONTRACT" \
771
+ --argjson sec "$SECURITY" \
772
+ --argjson map "$PROJECT_MAP" \
773
+ --argjson meta "$METADATA" \
774
+ '{
775
+ session_id: $sid,
776
+ step: "create_task",
777
+ factory_change: $factory_change,
778
+ project_slug: (if $project_slug != "" then $project_slug else null end),
779
+ project_channel_id: (if $project_channel_id != "" then $project_channel_id else null end),
780
+ issues: [{
781
+ number: $num,
782
+ title: $title,
783
+ url: $url,
784
+ labels: ($labels | split(",")),
785
+ state: "open"
786
+ }],
787
+ spec: $spec,
788
+ classification: $cls,
789
+ interview: $interview,
790
+ impact: $impact,
791
+ qa_contract: $qa,
792
+ security: $sec,
793
+ project_map: $map,
794
+ metadata: $meta
795
+ }'
796
+
797
+ genesis_metric_end "ok"