@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,598 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Step 10: Triage — prioritize and dispatch issue to DevClaw pipeline
5
+ # Input: stdin JSON (issue data + spec)
6
+ # Output: JSON with triage decision to stdout
7
+ # Applies labels + workflow transitions to dispatch DevClaw safely
8
+
9
+ GENESIS_LOG="${GENESIS_LOG:-$HOME/.openclaw/workspace/logs/genesis.log}"
10
+ mkdir -p "$(dirname "$GENESIS_LOG")"
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ TRIAGE_MATRIX="$SCRIPT_DIR/../configs/triage-matrix.json"
14
+ source "$SCRIPT_DIR/sideband-lib.sh"
15
+ source "$SCRIPT_DIR/genesis-telemetry.sh"
16
+
17
+ # Load .env if available
18
+ genesis_load_env_file "$HOME/.openclaw/.env"
19
+
20
+ if [[ -n "${1:-}" && -f "${1:-}" ]]; then
21
+ INPUT="$(cat "$1")"
22
+ else
23
+ INPUT="$(cat)"
24
+ fi
25
+ TARGET_RESOLUTION="$(genesis_resolve_canonical_target "$INPUT" || jq -n '{metadata:{}}')"
26
+ INPUT="$(printf '%s' "$INPUT" | jq --argjson resolved "$TARGET_RESOLUTION" '
27
+ .metadata = ((.metadata // {}) + ($resolved.metadata // {}))
28
+ ')"
29
+ if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then
30
+ echo "ERROR: triage received non-JSON input (likely previous step leaked stdout)" >&2
31
+ exit 1
32
+ fi
33
+ SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
34
+ genesis_metric_start "triage" "$SESSION_ID"
35
+ SPEC="$(echo "$INPUT" | jq '.spec // {}')"
36
+ ISSUES="$(echo "$INPUT" | jq '.issues // []')"
37
+ IMPACT="$(echo "$INPUT" | jq '.impact // {}')"
38
+ SECURITY="$(echo "$INPUT" | jq '.security // {}')"
39
+ DRY_RUN="$(echo "$INPUT" | jq -r '.dry_run // false')"
40
+ SPEC_TYPE="$(echo "$INPUT" | jq -r '.spec.type // "feature"')"
41
+ SPEC_DELIVERY_TARGET="$(echo "$INPUT" | jq -r '.spec.delivery_target // "unknown"')"
42
+
43
+ if ! echo "$SPEC" | jq -e 'type == "object"' >/dev/null 2>&1; then
44
+ echo "ERROR: triage spec payload must be an object" >&2
45
+ exit 1
46
+ fi
47
+ if ! echo "$ISSUES" | jq -e 'type == "array"' >/dev/null 2>&1; then
48
+ echo "ERROR: triage issues payload must be an array" >&2
49
+ exit 1
50
+ fi
51
+
52
+ echo "Running triage for session $SESSION_ID..." >&2
53
+
54
+ # Dry-run mode: keep pipeline deterministic without touching labels.
55
+ if [[ "$DRY_RUN" == "true" ]]; then
56
+ echo "$INPUT" | jq '. + {
57
+ step: "triage",
58
+ triage: {
59
+ skipped: true,
60
+ reason: "dry_run",
61
+ ready_for_dispatch: false,
62
+ errors: []
63
+ }
64
+ }'
65
+ exit 0
66
+ fi
67
+
68
+ # Get issue number
69
+ ISSUE_NUMBER="$(echo "$ISSUES" | jq -r '.[0].number // 0')"
70
+ if [[ "$ISSUE_NUMBER" == "0" ]]; then
71
+ echo "ERROR: No issue number found in input" >&2
72
+ exit 1
73
+ fi
74
+
75
+ # Determine repo (prefer current pipeline state; avoid implicit default target for product flows)
76
+ REPO_URL="$(echo "$INPUT" | jq -r '.scaffold.repo_url // .metadata.repo_url // ""')"
77
+
78
+ # Sideband: if scaffold created a new repo, use that (validated + TTL-bound)
79
+ SCAFFOLD_PAYLOAD="$(genesis_sideband_read_payload "scaffold" "$SESSION_ID" "${GENESIS_SIDEBAND_TTL_SECONDS:-1800}" || true)"
80
+ if [[ -z "$REPO_URL" && -n "$SCAFFOLD_PAYLOAD" ]]; then
81
+ SCAFFOLD_REPO="$(echo "$SCAFFOLD_PAYLOAD" | jq -r '.scaffold.repo_url // empty')"
82
+ if [[ -n "$SCAFFOLD_REPO" ]]; then
83
+ REPO_URL="$SCAFFOLD_REPO"
84
+ echo "Using scaffolded repo URL for triage: $REPO_URL" >&2
85
+ fi
86
+ fi
87
+
88
+ CANDIDATE_PROJECT_SLUG="$(echo "$INPUT" | jq -r '.project_slug // .metadata.project_slug // .scaffold.project_slug // empty')"
89
+ if [[ -z "$CANDIDATE_PROJECT_SLUG" || "$CANDIDATE_PROJECT_SLUG" == "null" ]]; then
90
+ CANDIDATE_PROJECT_SLUG="$(echo "$INPUT" | jq -r '.metadata.project_name // empty')"
91
+ fi
92
+ REQUESTED_CHANNEL_ID="$(echo "$INPUT" | jq -r '.project_channel_id // empty')"
93
+ PROJECT_SLUG=""
94
+
95
+ if [[ -z "$REPO_URL" ]]; then
96
+ if [[ -n "$CANDIDATE_PROJECT_SLUG" && "$CANDIDATE_PROJECT_SLUG" != "null" ]]; then
97
+ PROJECT_REF="$(genesis_project_resolve_ref "$CANDIDATE_PROJECT_SLUG" || true)"
98
+ if [[ -n "$PROJECT_REF" ]]; then
99
+ RESOLVED_REMOTE="$(printf '%s' "$PROJECT_REF" | cut -f3)"
100
+ if [[ -n "$RESOLVED_REMOTE" ]]; then
101
+ REPO_URL="$RESOLVED_REMOTE"
102
+ echo "Resolved repo from project slug '$CANDIDATE_PROJECT_SLUG': $REPO_URL" >&2
103
+ fi
104
+ fi
105
+ fi
106
+ fi
107
+ if [[ -n "$REPO_URL" && "$REPO_URL" == "~"* ]]; then
108
+ REPO_URL="$(genesis_expand_path "$REPO_URL")"
109
+ fi
110
+ if [[ -z "$REPO_URL" ]]; then
111
+ echo "ERROR: No repo URL resolved from pipeline state for triage (scaffold/metadata)." >&2
112
+ exit 1
113
+ fi
114
+
115
+ if [[ -n "$REPO_URL" ]] && ! genesis_parse_owner_repo "$REPO_URL" >/dev/null 2>&1; then
116
+ PROJECT_REF="$(genesis_project_resolve_ref "$REPO_URL" || true)"
117
+ if [[ -n "$PROJECT_REF" ]]; then
118
+ PROJECT_SLUG="$(printf '%s' "$PROJECT_REF" | cut -f1)"
119
+ RESOLVED_REMOTE="$(printf '%s' "$PROJECT_REF" | cut -f3)"
120
+ if [[ -n "$RESOLVED_REMOTE" ]]; then
121
+ REPO_URL="$RESOLVED_REMOTE"
122
+ fi
123
+ fi
124
+ fi
125
+
126
+ OWNER_REPO="$(genesis_parse_owner_repo "$REPO_URL" || true)"
127
+ if [[ -z "$OWNER_REPO" ]]; then
128
+ echo "ERROR: Invalid GitHub repository reference: $REPO_URL" >&2
129
+ exit 1
130
+ fi
131
+
132
+ # Resolve project slug deterministically from projects.json.
133
+ REPO_PROJECT_SLUG="$(genesis_find_project_slug_by_repo "$REPO_URL" || true)"
134
+ if [[ -n "$REPO_PROJECT_SLUG" ]]; then
135
+ if [[ -n "$CANDIDATE_PROJECT_SLUG" && "$CANDIDATE_PROJECT_SLUG" != "$REPO_PROJECT_SLUG" ]]; then
136
+ echo "WARNING: Provided project slug '$CANDIDATE_PROJECT_SLUG' does not match repo mapping; using '$REPO_PROJECT_SLUG'." >&2
137
+ fi
138
+ PROJECT_SLUG="$REPO_PROJECT_SLUG"
139
+ elif [[ -n "$CANDIDATE_PROJECT_SLUG" ]] && genesis_project_exists "$CANDIDATE_PROJECT_SLUG"; then
140
+ PROJECT_SLUG="$CANDIDATE_PROJECT_SLUG"
141
+ elif [[ -n "$CANDIDATE_PROJECT_SLUG" ]]; then
142
+ echo "WARNING: Provided project slug '$CANDIDATE_PROJECT_SLUG' is not registered in projects.json." >&2
143
+ fi
144
+
145
+ PROJECT_KIND="implementation"
146
+ PROJECT_ARCHIVED="false"
147
+ if [[ -n "$PROJECT_SLUG" ]] && genesis_project_exists "$PROJECT_SLUG"; then
148
+ PROJECT_KIND="$(genesis_project_kind "$PROJECT_SLUG" || echo implementation)"
149
+ PROJECT_ARCHIVED="$(genesis_project_archived "$PROJECT_SLUG" || echo false)"
150
+ fi
151
+
152
+ if [[ "$PROJECT_ARCHIVED" == "true" ]]; then
153
+ echo "ERROR: Target project \"$PROJECT_SLUG\" is archived and cannot be triaged or dispatched." >&2
154
+ exit 1
155
+ fi
156
+
157
+ if [[ "$PROJECT_KIND" == "pointer" ]] && [[ "$SPEC_TYPE" != "research" ]]; then
158
+ echo "ERROR: Target project \"$PROJECT_SLUG\" is marked as pointer/scaffold and cannot receive implementation dispatch." >&2
159
+ exit 1
160
+ fi
161
+
162
+ if [[ -n "$PROJECT_SLUG" ]] && genesis_is_factory_project_slug "$PROJECT_SLUG"; then
163
+ if ! genesis_payload_factory_change "$INPUT"; then
164
+ echo "ERROR: Target project \"$PROJECT_SLUG\" is reserved for Factory-internal changes. User/product requests must target a dedicated project repository." >&2
165
+ exit 1
166
+ fi
167
+ fi
168
+
169
+ PROJECT_CHANNEL_ID=""
170
+ if [[ -n "$PROJECT_SLUG" ]]; then
171
+ PROJECT_CHANNEL_ID="$(genesis_project_channel_id "$PROJECT_SLUG" "$REQUESTED_CHANNEL_ID" || true)"
172
+ if [[ -n "$REQUESTED_CHANNEL_ID" && -n "$PROJECT_CHANNEL_ID" && "$REQUESTED_CHANNEL_ID" != "$PROJECT_CHANNEL_ID" ]]; then
173
+ echo "WARNING: Requested channel '$REQUESTED_CHANNEL_ID' is not valid for '$PROJECT_SLUG'; using '$PROJECT_CHANNEL_ID' from projects.json." >&2
174
+ fi
175
+ fi
176
+ USE_DEVCLAW_TASKS=false
177
+ if [[ -n "$PROJECT_SLUG" && -n "$PROJECT_CHANNEL_ID" ]] && genesis_openclaw_bin >/dev/null 2>&1 && genesis_openclaw_supports devclaw task; then
178
+ USE_DEVCLAW_TASKS=true
179
+ fi
180
+
181
+ if [[ ! -f "$TRIAGE_MATRIX" ]]; then
182
+ echo "ERROR: Missing triage matrix file: $TRIAGE_MATRIX" >&2
183
+ exit 1
184
+ fi
185
+
186
+ TYPE="$(echo "$SPEC" | jq -r '.type // "feature"')"
187
+ TITLE="$(echo "$SPEC" | jq -r '.title // ""')"
188
+ AC_COUNT="$(echo "$SPEC" | jq '.acceptance_criteria // [] | length')"
189
+ SCOPE_COUNT="$(echo "$SPEC" | jq '.scope_v1 // [] | length')"
190
+ DOD_COUNT="$(echo "$SPEC" | jq '.definition_of_done // [] | length')"
191
+ DELIVERY_TARGET="$(echo "$SPEC" | jq -r '.delivery_target // "unknown"')"
192
+ OBJECTIVE_RAW="$(echo "$SPEC" | jq -r '.objective // ""')"
193
+ OBJECTIVE="$(genesis_trim "$OBJECTIVE_RAW")"
194
+ OBJECTIVE_LOWER="$(printf '%s' "$OBJECTIVE" | tr '[:upper:]' '[:lower:]')"
195
+ RAW_IDEA_LOWER="$(echo "$INPUT" | jq -r '.raw_idea // ""' | tr '[:upper:]' '[:lower:]')"
196
+ META_AUTH_SIGNAL="$(echo "$INPUT" | jq -r '.metadata.auth_gate.signal // false')"
197
+ FACTORY_CHANGE_RAW="$(echo "$INPUT" | jq -r '.factory_change // false')"
198
+ FACTORY_CHANGE=false
199
+ if [[ "$FACTORY_CHANGE_RAW" == "true" ]]; then
200
+ FACTORY_CHANGE=true
201
+ elif printf '%s\n%s\n' "$TITLE" "$OBJECTIVE" | tr '[:upper:]' '[:lower:]' | grep -Eq 'factory|openclaw|devclaw|workflow|pipeline|orchestr'; then
202
+ FACTORY_CHANGE=true
203
+ fi
204
+ FILES_CHANGED="$(echo "$IMPACT" | jq '.estimated_files_changed // 0')"
205
+ RISK_COUNT="$(echo "$IMPACT" | jq '.risk_areas // [] | length')"
206
+ SEC_NOTES="$(echo "$SECURITY" | jq '.spec_security_notes // [] | length')"
207
+ TOTAL_RISKS=$((RISK_COUNT + SEC_NOTES))
208
+
209
+ echo "Type=$TYPE, ACs=$AC_COUNT, Files=$FILES_CHANGED, Risks=$TOTAL_RISKS" >&2
210
+
211
+ # Calculate effort
212
+ EFFORT="medium"
213
+ if [[ "$FILES_CHANGED" -le 3 ]] && [[ "$AC_COUNT" -le 3 ]]; then
214
+ EFFORT="small"
215
+ elif [[ "$FILES_CHANGED" -le 10 ]] && [[ "$AC_COUNT" -le 7 ]]; then
216
+ EFFORT="medium"
217
+ elif [[ "$FILES_CHANGED" -le 25 ]] && [[ "$AC_COUNT" -le 15 ]]; then
218
+ EFFORT="large"
219
+ else
220
+ EFFORT="xlarge"
221
+ fi
222
+
223
+ EFFORT_LABEL="$(jq -r --arg e "$EFFORT" '.effort_rules[$e].label // empty' "$TRIAGE_MATRIX")"
224
+
225
+ # Calculate priority (walk the rules in order, first match wins)
226
+ PRIORITY="P3"
227
+ PRIORITY_LABEL="priority:normal"
228
+ PRIORITY_MATCHED=false
229
+
230
+ while IFS= read -r rule; do
231
+ [[ -n "$rule" ]] || continue
232
+ RULE_TYPE="$(echo "$rule" | jq -r '.when.type // empty')"
233
+ RULE_EFFORT="$(echo "$rule" | jq -r '.when.effort // empty')"
234
+ RULE_MIN_RISK="$(echo "$rule" | jq -r '.when.min_risk_count // empty')"
235
+ RULE_MAX_RISK="$(echo "$rule" | jq -r '.when.max_risk_count // empty')"
236
+ RULE_PRIORITY="$(echo "$rule" | jq -r '.priority // empty')"
237
+ RULE_LABEL="$(echo "$rule" | jq -r '.label // empty')"
238
+
239
+ [[ -n "$RULE_PRIORITY" && -n "$RULE_LABEL" ]] || continue
240
+ [[ -z "$RULE_TYPE" || "$RULE_TYPE" == "$TYPE" ]] || continue
241
+ [[ -z "$RULE_EFFORT" || "$RULE_EFFORT" == "$EFFORT" ]] || continue
242
+ if [[ -n "$RULE_MIN_RISK" && "$RULE_MIN_RISK" != "null" ]]; then
243
+ [[ "$TOTAL_RISKS" -ge "$RULE_MIN_RISK" ]] || continue
244
+ fi
245
+ if [[ -n "$RULE_MAX_RISK" && "$RULE_MAX_RISK" != "null" ]]; then
246
+ [[ "$TOTAL_RISKS" -le "$RULE_MAX_RISK" ]] || continue
247
+ fi
248
+
249
+ PRIORITY="$RULE_PRIORITY"
250
+ PRIORITY_LABEL="$RULE_LABEL"
251
+ PRIORITY_MATCHED=true
252
+ break
253
+ done < <(jq -c '.priority_rules_v2 // [] | .[]' "$TRIAGE_MATRIX")
254
+
255
+ if [[ "$PRIORITY_MATCHED" != "true" ]]; then
256
+ if [[ "$TYPE" == "bugfix" ]] && [[ "$TOTAL_RISKS" -gt 2 ]]; then
257
+ PRIORITY="P0"; PRIORITY_LABEL="priority:critical"
258
+ elif [[ "$TYPE" == "bugfix" ]]; then
259
+ PRIORITY="P1"; PRIORITY_LABEL="priority:high"
260
+ elif [[ "$TYPE" == "infra" ]]; then
261
+ PRIORITY="P2"; PRIORITY_LABEL="priority:medium"
262
+ elif [[ "$TYPE" == "feature" ]] && [[ "$EFFORT" == "small" ]]; then
263
+ PRIORITY="P2"; PRIORITY_LABEL="priority:medium"
264
+ elif [[ "$TYPE" == "feature" ]]; then
265
+ PRIORITY="P3"; PRIORITY_LABEL="priority:normal"
266
+ else
267
+ PRIORITY="P3"; PRIORITY_LABEL="priority:normal"
268
+ fi
269
+ fi
270
+
271
+ # Get type label
272
+ TYPE_LABEL="$(jq -r --arg t "$TYPE" '.auto_labels[$t] // empty' "$TRIAGE_MATRIX")"
273
+ DISPATCH_LABEL="$(jq -r '.dispatch_label // empty' "$TRIAGE_MATRIX")"
274
+
275
+ if [[ -z "$EFFORT_LABEL" || -z "$PRIORITY_LABEL" || -z "$DISPATCH_LABEL" ]]; then
276
+ echo "ERROR: Invalid triage matrix labels (effort/priority/dispatch)" >&2
277
+ exit 1
278
+ fi
279
+
280
+ echo "Triage: $PRIORITY ($PRIORITY_LABEL), effort=$EFFORT ($EFFORT_LABEL)" >&2
281
+
282
+ READY_FOR_DISPATCH=true
283
+ TRIAGE_ERRORS=()
284
+ DISPATCH_STAGE_APPLIED=false
285
+ LEVEL_LABEL_APPLIED=false
286
+ TARGET_TRANSITIONED=false
287
+
288
+ # Definition of Ready (DoR) gate: fail-closed before dispatch.
289
+ if [[ -z "$OBJECTIVE" ]]; then
290
+ READY_FOR_DISPATCH=false
291
+ TRIAGE_ERRORS+=("dor_missing_objective")
292
+ fi
293
+ if [[ "$SCOPE_COUNT" -lt 1 ]]; then
294
+ READY_FOR_DISPATCH=false
295
+ TRIAGE_ERRORS+=("dor_missing_scope")
296
+ fi
297
+ if [[ "$AC_COUNT" -lt 1 ]]; then
298
+ READY_FOR_DISPATCH=false
299
+ TRIAGE_ERRORS+=("dor_missing_acceptance_criteria")
300
+ fi
301
+ if [[ "$DOD_COUNT" -lt 1 ]]; then
302
+ READY_FOR_DISPATCH=false
303
+ TRIAGE_ERRORS+=("dor_missing_definition_of_done")
304
+ fi
305
+ AC_LOWER="$(echo "$SPEC" | jq -r '.acceptance_criteria // [] | join(" ")' | tr '[:upper:]' '[:lower:]')"
306
+ SCOPE_LOWER="$(echo "$SPEC" | jq -r '.scope_v1 // [] | join(" ")' | tr '[:upper:]' '[:lower:]')"
307
+ OOS_LOWER="$(echo "$SPEC" | jq -r '.out_of_scope // [] | join(" ")' | tr '[:upper:]' '[:lower:]')"
308
+ if [[ "$DELIVERY_TARGET" == "web-ui" ]]; then
309
+ if ! echo "$AC_LOWER $SCOPE_LOWER" | grep -Eqi '\b(tela|página|pagina|ui|interface|dashboard|fluxo)\b'; then
310
+ READY_FOR_DISPATCH=false
311
+ TRIAGE_ERRORS+=("dor_web_ui_missing_ui_evidence")
312
+ fi
313
+ fi
314
+ if [[ "$DELIVERY_TARGET" == "api" ]]; then
315
+ if ! echo "$AC_LOWER $SCOPE_LOWER" | grep -Eqi '\b(api|endpoint|rota|route|http|rest)\b'; then
316
+ READY_FOR_DISPATCH=false
317
+ TRIAGE_ERRORS+=("dor_api_missing_endpoint_evidence")
318
+ fi
319
+ fi
320
+ if [[ "$DELIVERY_TARGET" == "hybrid" ]]; then
321
+ if ! echo "$AC_LOWER $SCOPE_LOWER" | grep -Eqi '\b(tela|página|pagina|ui|interface|dashboard|fluxo)\b'; then
322
+ READY_FOR_DISPATCH=false
323
+ TRIAGE_ERRORS+=("dor_hybrid_missing_ui_evidence")
324
+ fi
325
+ if ! echo "$AC_LOWER $SCOPE_LOWER" | grep -Eqi '\b(api|endpoint|rota|route|http|rest)\b'; then
326
+ READY_FOR_DISPATCH=false
327
+ TRIAGE_ERRORS+=("dor_hybrid_missing_api_evidence")
328
+ fi
329
+ fi
330
+
331
+ # Auth critical requirements gate.
332
+ AUTH_REGEX='\b(login|autentic|senha|perfil|permiss|acesso|rbac|admin)\b'
333
+ AUTH_SIGNAL=false
334
+ if [[ "$META_AUTH_SIGNAL" == "true" ]]; then
335
+ AUTH_SIGNAL=true
336
+ elif echo "$RAW_IDEA_LOWER $OBJECTIVE_LOWER" | grep -Eqi "$AUTH_REGEX"; then
337
+ AUTH_SIGNAL=true
338
+ fi
339
+ if [[ "$AUTH_SIGNAL" == "true" ]]; then
340
+ if ! echo "$AC_LOWER $SCOPE_LOWER $OBJECTIVE_LOWER" | grep -Eqi "$AUTH_REGEX"; then
341
+ READY_FOR_DISPATCH=false
342
+ TRIAGE_ERRORS+=("dor_auth_requirements_missing")
343
+ fi
344
+ if echo "$OOS_LOWER" | grep -Eqi "$AUTH_REGEX" && ! echo "$AC_LOWER" | grep -Eqi "$AUTH_REGEX"; then
345
+ READY_FOR_DISPATCH=false
346
+ TRIAGE_ERRORS+=("dor_auth_moved_to_out_of_scope_without_acceptance")
347
+ fi
348
+ fi
349
+
350
+ # Route by type (config-driven).
351
+ TARGET_QUEUE_LABEL="$(jq -r --arg t "$TYPE" '.target_state_by_type[$t] // .target_state_by_type.default // "To Do"' "$TRIAGE_MATRIX")"
352
+ if [[ -z "$TARGET_QUEUE_LABEL" || "$TARGET_QUEUE_LABEL" == "null" ]]; then
353
+ TARGET_QUEUE_LABEL="To Do"
354
+ fi
355
+
356
+ # Apply labels via gh
357
+ ALL_LABELS="$PRIORITY_LABEL,$EFFORT_LABEL"
358
+ [[ -n "$TYPE_LABEL" ]] && ALL_LABELS="$ALL_LABELS,$TYPE_LABEL"
359
+ if [[ "$READY_FOR_DISPATCH" != "true" ]]; then
360
+ ALL_LABELS="$ALL_LABELS,needs-human"
361
+ fi
362
+
363
+ echo "Applying labels to issue #$ISSUE_NUMBER: $ALL_LABELS" >&2
364
+
365
+ if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
366
+ TASK_LABELS_CMD=(labels --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --add "$ALL_LABELS")
367
+ if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
368
+ TASK_LABELS_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
369
+ fi
370
+ if [[ "$FACTORY_CHANGE" == "true" ]]; then
371
+ TASK_LABELS_CMD+=(--factory-change)
372
+ fi
373
+ if ! genesis_devclaw_task_json "${TASK_LABELS_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG"; then
374
+ READY_FOR_DISPATCH=false
375
+ TRIAGE_ERRORS+=("apply_labels_failed")
376
+ fi
377
+ else
378
+ if ! gh issue edit "$ISSUE_NUMBER" \
379
+ --repo "$OWNER_REPO" \
380
+ --add-label "$ALL_LABELS" >/dev/null 2>&1; then
381
+ READY_FOR_DISPATCH=false
382
+ TRIAGE_ERRORS+=("apply_labels_failed")
383
+ else
384
+ :
385
+ fi
386
+ fi
387
+
388
+ if [[ "$READY_FOR_DISPATCH" != "true" ]]; then
389
+ DOR_COMMENT="## Triage blocked by Definition of Ready
390
+
391
+ The issue stayed in **Planning** because required fields are missing:
392
+ $(printf '%s\n' "${TRIAGE_ERRORS[@]}" | sed 's/^/- /')
393
+ "
394
+ if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
395
+ DOR_COMMENT_FILE="$(mktemp)"
396
+ printf '%s\n' "$DOR_COMMENT" > "$DOR_COMMENT_FILE"
397
+ TASK_DOR_CMD=(comment --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --body-file "$DOR_COMMENT_FILE")
398
+ if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
399
+ TASK_DOR_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
400
+ fi
401
+ genesis_devclaw_task_json "${TASK_DOR_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG" || true
402
+ rm -f "$DOR_COMMENT_FILE"
403
+ else
404
+ gh issue comment "$ISSUE_NUMBER" --repo "$OWNER_REPO" --body "$DOR_COMMENT" >/dev/null 2>&1 || true
405
+ fi
406
+ fi
407
+
408
+ echo "Issue #$ISSUE_NUMBER triaged (dispatch label '$DISPATCH_LABEL' requested)" >&2
409
+
410
+ # === Auto-transition: Planning → target queue ===
411
+ # Determine worker level from effort/complexity
412
+ LEVEL="medior"
413
+ if [[ "$EFFORT" == "small" ]]; then
414
+ LEVEL="junior"
415
+ elif [[ "$EFFORT" == "large" ]] || [[ "$EFFORT" == "xlarge" ]]; then
416
+ LEVEL="senior"
417
+ fi
418
+ if [[ "$TARGET_QUEUE_LABEL" == "To Research" && "$LEVEL" == "medior" ]]; then
419
+ LEVEL="junior"
420
+ fi
421
+
422
+ if [[ "$READY_FOR_DISPATCH" == "true" && "$TARGET_QUEUE_LABEL" == "To Do" && "$USE_DEVCLAW_TASKS" == "true" ]]; then
423
+ echo "Dispatching via deterministic DevClaw task_start (Planning → To Do, level=$LEVEL)..." >&2
424
+ TASK_START_CMD=(start --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --level "$LEVEL")
425
+ if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
426
+ TASK_START_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
427
+ fi
428
+ if [[ "$FACTORY_CHANGE" == "true" ]]; then
429
+ TASK_START_CMD+=(--factory-change)
430
+ fi
431
+ if ! genesis_devclaw_task_json "${TASK_START_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG"; then
432
+ READY_FOR_DISPATCH=false
433
+ TRIAGE_ERRORS+=("task_start_failed")
434
+ else
435
+ TARGET_TRANSITIONED=true
436
+ LEVEL_LABEL_APPLIED=true
437
+ DISPATCH_STAGE_APPLIED=true
438
+ fi
439
+ elif [[ "$READY_FOR_DISPATCH" == "true" && "$USE_DEVCLAW_TASKS" == "true" ]]; then
440
+ echo "Routing deterministically via DevClaw task route (Planning → $TARGET_QUEUE_LABEL, level=$LEVEL)..." >&2
441
+ TASK_ROUTE_CMD=(route --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --to-label "$TARGET_QUEUE_LABEL")
442
+ if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
443
+ TASK_ROUTE_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
444
+ fi
445
+ if [[ -n "$LEVEL" ]]; then
446
+ TASK_ROUTE_CMD+=(--level "$LEVEL")
447
+ fi
448
+ if [[ "$FACTORY_CHANGE" == "true" ]]; then
449
+ TASK_ROUTE_CMD+=(--factory-change)
450
+ fi
451
+ if ! genesis_devclaw_task_json "${TASK_ROUTE_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG"; then
452
+ READY_FOR_DISPATCH=false
453
+ TRIAGE_ERRORS+=("task_route_failed")
454
+ else
455
+ TARGET_TRANSITIONED=true
456
+ LEVEL_LABEL_APPLIED=true
457
+ fi
458
+ else
459
+ # Add developer level label (legacy gh path or non-deterministic fallback)
460
+ if [[ "$READY_FOR_DISPATCH" == "true" && "$TARGET_QUEUE_LABEL" == "To Do" ]]; then
461
+ echo "Setting developer level: developer:$LEVEL" >&2
462
+ if ! gh issue edit "$ISSUE_NUMBER" \
463
+ --repo "$OWNER_REPO" \
464
+ --add-label "developer:$LEVEL" >/dev/null 2>>"$GENESIS_LOG"; then
465
+ READY_FOR_DISPATCH=false
466
+ TRIAGE_ERRORS+=("level_label_failed")
467
+ else
468
+ LEVEL_LABEL_APPLIED=true
469
+ fi
470
+ else
471
+ echo "Skipping developer level label (target=$TARGET_QUEUE_LABEL, ready=$READY_FOR_DISPATCH)" >&2
472
+ fi
473
+
474
+ # Swap Planning → target queue (heartbeat will auto-dispatch workers)
475
+ if [[ "$READY_FOR_DISPATCH" == "true" ]]; then
476
+ echo "Transitioning issue #$ISSUE_NUMBER: Planning → $TARGET_QUEUE_LABEL" >&2
477
+
478
+ TRANSITION_LABELS="$TARGET_QUEUE_LABEL"
479
+ if [[ "$TARGET_QUEUE_LABEL" == "To Do" ]]; then
480
+ TRANSITION_LABELS="$TRANSITION_LABELS,$DISPATCH_LABEL"
481
+ fi
482
+
483
+ if ! gh issue edit "$ISSUE_NUMBER" \
484
+ --repo "$OWNER_REPO" \
485
+ --remove-label "Planning" \
486
+ --add-label "$TRANSITION_LABELS" >/dev/null 2>>"$GENESIS_LOG"; then
487
+ CURRENT_LABELS="$(gh issue view "$ISSUE_NUMBER" --repo "$OWNER_REPO" --json labels --jq '.labels[].name' 2>/dev/null || true)"
488
+ if echo "$CURRENT_LABELS" | grep -qxF "$TARGET_QUEUE_LABEL"; then
489
+ TARGET_TRANSITIONED=true
490
+ [[ "$TARGET_QUEUE_LABEL" == "To Do" ]] && DISPATCH_STAGE_APPLIED=true
491
+ else
492
+ READY_FOR_DISPATCH=false
493
+ TRIAGE_ERRORS+=("planning_to_target_failed")
494
+ fi
495
+ else
496
+ TARGET_TRANSITIONED=true
497
+ [[ "$TARGET_QUEUE_LABEL" == "To Do" ]] && DISPATCH_STAGE_APPLIED=true
498
+ fi
499
+ else
500
+ echo "Skipping Planning → target transition due to previous triage failure" >&2
501
+ fi
502
+ fi
503
+
504
+ if [[ "$READY_FOR_DISPATCH" == "true" ]]; then
505
+ if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
506
+ TASK_CLEAR_NEEDS_HUMAN_CMD=(labels --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --remove "needs-human")
507
+ if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
508
+ TASK_CLEAR_NEEDS_HUMAN_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
509
+ fi
510
+ if [[ "$FACTORY_CHANGE" == "true" ]]; then
511
+ TASK_CLEAR_NEEDS_HUMAN_CMD+=(--factory-change)
512
+ fi
513
+ genesis_devclaw_task_json "${TASK_CLEAR_NEEDS_HUMAN_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG" || true
514
+ else
515
+ gh issue edit "$ISSUE_NUMBER" --repo "$OWNER_REPO" --remove-label "needs-human" >/dev/null 2>&1 || true
516
+ fi
517
+ echo "Issue #$ISSUE_NUMBER dispatched to $TARGET_QUEUE_LABEL (level=$LEVEL)" >&2
518
+ else
519
+ echo "Issue #$ISSUE_NUMBER NOT dispatched; triage failed closed: ${TRIAGE_ERRORS[*]}" >&2
520
+ if [[ "$USE_DEVCLAW_TASKS" == "true" ]]; then
521
+ TASK_SET_NEEDS_HUMAN_CMD=(labels --project "$PROJECT_SLUG" --issue-id "$ISSUE_NUMBER" --add "needs-human")
522
+ if [[ -n "$PROJECT_CHANNEL_ID" ]]; then
523
+ TASK_SET_NEEDS_HUMAN_CMD+=(--channel-id "$PROJECT_CHANNEL_ID")
524
+ fi
525
+ if [[ "$FACTORY_CHANGE" == "true" ]]; then
526
+ TASK_SET_NEEDS_HUMAN_CMD+=(--factory-change)
527
+ fi
528
+ if ! genesis_devclaw_task_json "${TASK_SET_NEEDS_HUMAN_CMD[@]}" >/dev/null 2>>"$GENESIS_LOG"; then
529
+ TRIAGE_ERRORS+=("mark_needs_human_failed")
530
+ fi
531
+ elif ! gh issue edit "$ISSUE_NUMBER" --repo "$OWNER_REPO" --add-label "needs-human" >/dev/null 2>&1; then
532
+ TRIAGE_ERRORS+=("mark_needs_human_failed")
533
+ fi
534
+
535
+ if [[ "$TARGET_TRANSITIONED" == "true" ]]; then
536
+ if ! gh issue edit "$ISSUE_NUMBER" \
537
+ --repo "$OWNER_REPO" \
538
+ --remove-label "$TARGET_QUEUE_LABEL" \
539
+ --add-label "Planning" >/dev/null 2>>"$GENESIS_LOG"; then
540
+ TRIAGE_ERRORS+=("rollback_target_failed")
541
+ fi
542
+ fi
543
+
544
+ ROLLBACK_REMOVE_LABELS=()
545
+ if [[ "$LEVEL_LABEL_APPLIED" == "true" ]]; then
546
+ ROLLBACK_REMOVE_LABELS+=("developer:$LEVEL")
547
+ fi
548
+ if [[ "$DISPATCH_STAGE_APPLIED" == "true" ]]; then
549
+ ROLLBACK_REMOVE_LABELS+=("$DISPATCH_LABEL")
550
+ fi
551
+
552
+ if [[ "${#ROLLBACK_REMOVE_LABELS[@]}" -gt 0 ]]; then
553
+ ROLLBACK_REMOVE_CSV="$(IFS=,; echo "${ROLLBACK_REMOVE_LABELS[*]}")"
554
+ if ! gh issue edit "$ISSUE_NUMBER" \
555
+ --repo "$OWNER_REPO" \
556
+ --remove-label "$ROLLBACK_REMOVE_CSV" >/dev/null 2>>"$GENESIS_LOG"; then
557
+ TRIAGE_ERRORS+=("rollback_labels_failed")
558
+ fi
559
+ fi
560
+ fi
561
+
562
+ # Cleanup sideband files
563
+ genesis_sideband_cleanup "$SESSION_ID"
564
+
565
+ if [[ "${#TRIAGE_ERRORS[@]}" -gt 0 ]]; then
566
+ TRIAGE_ERRORS_JSON="$(printf '%s\n' "${TRIAGE_ERRORS[@]}" | jq -R . | jq -s .)"
567
+ else
568
+ TRIAGE_ERRORS_JSON="[]"
569
+ fi
570
+ echo "triage.sh completed for session $SESSION_ID (ready=$READY_FOR_DISPATCH, errors=${#TRIAGE_ERRORS[@]})" >&2
571
+ jq -n \
572
+ --arg sid "$SESSION_ID" \
573
+ --arg priority "$PRIORITY" \
574
+ --arg effort "$EFFORT" \
575
+ --arg target "$TARGET_QUEUE_LABEL" \
576
+ --arg project_slug "$PROJECT_SLUG" \
577
+ --arg project_channel_id "$PROJECT_CHANNEL_ID" \
578
+ --arg labels "$ALL_LABELS" \
579
+ --argjson ready "$([[ "$READY_FOR_DISPATCH" == "true" ]] && echo "true" || echo "false")" \
580
+ --argjson errors "$TRIAGE_ERRORS_JSON" \
581
+ --argjson num "$ISSUE_NUMBER" \
582
+ '{
583
+ session_id: $sid,
584
+ step: "triage",
585
+ triage: {
586
+ priority: $priority,
587
+ effort: $effort,
588
+ target_state: $target,
589
+ project_slug: (if $project_slug != "" then $project_slug else null end),
590
+ project_channel_id: (if $project_channel_id != "" then $project_channel_id else null end),
591
+ labels_applied: ($labels | split(",")),
592
+ issue_number: $num,
593
+ ready_for_dispatch: $ready,
594
+ errors: $errors
595
+ }
596
+ }'
597
+
598
+ genesis_metric_end "ok"
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Validate critical Genesis step envelopes with deterministic jq checks.
5
+ # Usage: validate-step.sh <classify|spec|impact>
6
+ # Input: JSON on stdin
7
+ # Output: same JSON on stdout (if valid)
8
+
9
+ STEP="${1:-}"
10
+ if [[ -n "${1:-}" && -f "${1:-}" ]]; then
11
+ INPUT="$(cat "$1")"
12
+ else
13
+ INPUT="$(cat)"
14
+ fi
15
+
16
+ fail() {
17
+ local reason="${1:-validation_failed}"
18
+ echo "ERROR: [$STEP] $reason" >&2
19
+ exit 1
20
+ }
21
+
22
+ require_json() {
23
+ echo "$INPUT" | jq -e . >/dev/null 2>&1 || fail "invalid_json"
24
+ }
25
+
26
+ validate_classify() {
27
+ echo "$INPUT" | jq -e '
28
+ (.session_id | type == "string" and length > 0) and
29
+ (.step == "classify") and
30
+ (.raw_idea | type == "string" and length > 0) and
31
+ (.classification | type == "object") and
32
+ (.classification.type | IN("feature","bugfix","refactor","research","infra")) and
33
+ (.classification.confidence | type == "number" and . >= 0 and . <= 1) and
34
+ (.classification.reasoning | type == "string" and length > 0) and
35
+ ((.classification.delivery_target // "unknown") | IN("web-ui","api","cli","hybrid","unknown"))
36
+ ' >/dev/null 2>&1 || fail "schema_violation"
37
+ }
38
+
39
+ validate_spec() {
40
+ echo "$INPUT" | jq -e '
41
+ (.session_id | type == "string" and length > 0) and
42
+ (.step == "spec") and
43
+ (.spec | type == "object") and
44
+ (.spec.title | type == "string" and length > 0) and
45
+ (.spec.type | IN("feature","bugfix","refactor","research","infra")) and
46
+ (.spec.objective | type == "string" and length >= 10) and
47
+ (.spec.scope_v1 | type == "array" and length >= 1) and
48
+ (.spec.out_of_scope | type == "array") and
49
+ (.spec.acceptance_criteria | type == "array" and length >= 1) and
50
+ (.spec.definition_of_done | type == "array" and length >= 1) and
51
+ (.spec.constraints | type == "string") and
52
+ ((.spec.delivery_target // "unknown") | IN("web-ui","api","cli","hybrid","unknown"))
53
+ ' >/dev/null 2>&1 || fail "schema_violation"
54
+ }
55
+
56
+ validate_impact() {
57
+ echo "$INPUT" | jq -e '
58
+ (.session_id | type == "string" and length > 0) and
59
+ (.step == "impact") and
60
+ (.impact | type == "object") and
61
+ (.impact.affected_files | type == "array") and
62
+ (.impact.new_files_needed | type == "array") and
63
+ (.impact.affected_modules | type == "array") and
64
+ (.impact.risk_areas | type == "array") and
65
+ (.impact.estimated_files_changed | type == "number" and . >= 0 and (floor == .)) and
66
+ (.impact.is_greenfield | type == "boolean")
67
+ ' >/dev/null 2>&1 || fail "schema_violation"
68
+ }
69
+
70
+ require_json
71
+
72
+ case "$STEP" in
73
+ classify) validate_classify ;;
74
+ spec) validate_spec ;;
75
+ impact) validate_impact ;;
76
+ *)
77
+ fail "unknown_step"
78
+ ;;
79
+ esac
80
+
81
+ echo "$INPUT"