@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.
- package/ARCHITECTURE.md +87 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/defaults/AGENTS.md +150 -0
- package/defaults/HEARTBEAT.md +3 -0
- package/defaults/IDENTITY.md +6 -0
- package/defaults/SOUL.md +39 -0
- package/defaults/TOOLS.md +15 -0
- package/defaults/fabrica/prompts/architect.md +147 -0
- package/defaults/fabrica/prompts/developer.md +211 -0
- package/defaults/fabrica/prompts/reviewer.md +114 -0
- package/defaults/fabrica/prompts/security-checklist.md +58 -0
- package/defaults/fabrica/prompts/tester.md +150 -0
- package/defaults/fabrica/workflow.yaml +184 -0
- package/dist/index.js +143075 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/worker.cjs +214 -0
- package/dist/worker.cjs +4754 -0
- package/fabrica.manifest.json +24 -0
- package/genesis/configs/classification-rules.json +32 -0
- package/genesis/configs/interview-templates.json +73 -0
- package/genesis/configs/labels.json +202 -0
- package/genesis/configs/triage-matrix.json +39 -0
- package/genesis/scripts/classify-idea.sh +161 -0
- package/genesis/scripts/conduct-interview.sh +199 -0
- package/genesis/scripts/create-task.sh +797 -0
- package/genesis/scripts/delivery-target-lib.sh +88 -0
- package/genesis/scripts/generate-qa-contract.sh +188 -0
- package/genesis/scripts/generate-spec.sh +171 -0
- package/genesis/scripts/genesis-telemetry.sh +97 -0
- package/genesis/scripts/genesis-utils.sh +617 -0
- package/genesis/scripts/impact-analysis.sh +135 -0
- package/genesis/scripts/interview.sh +98 -0
- package/genesis/scripts/map-project.sh +309 -0
- package/genesis/scripts/receive-idea.sh +69 -0
- package/genesis/scripts/register-project.sh +520 -0
- package/genesis/scripts/research-idea.sh +84 -0
- package/genesis/scripts/scaffold-project.sh +1396 -0
- package/genesis/scripts/security-review.sh +141 -0
- package/genesis/scripts/sideband-lib.sh +243 -0
- package/genesis/scripts/stack-detection-lib.sh +130 -0
- package/genesis/scripts/triage.sh +598 -0
- package/genesis/scripts/validate-step.sh +81 -0
- package/openclaw.plugin.json +45 -0
- package/package.json +60 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
genesis_normalize_delivery_target() {
|
|
5
|
+
local raw="${1:-}"
|
|
6
|
+
raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | sed -E 's/[[:space:]_]+/-/g')"
|
|
7
|
+
case "$raw" in
|
|
8
|
+
web|web-ui|webui|frontend|front-end|ui|interface|site|website|pwa)
|
|
9
|
+
printf '%s' "web-ui"
|
|
10
|
+
;;
|
|
11
|
+
api|backend|service|rest|graphql|webhook)
|
|
12
|
+
printf '%s' "api"
|
|
13
|
+
;;
|
|
14
|
+
cli|terminal|console|command-line|linha-de-comando)
|
|
15
|
+
printf '%s' "cli"
|
|
16
|
+
;;
|
|
17
|
+
hybrid|fullstack|full-stack)
|
|
18
|
+
printf '%s' "hybrid"
|
|
19
|
+
;;
|
|
20
|
+
*)
|
|
21
|
+
printf '%s' "unknown"
|
|
22
|
+
;;
|
|
23
|
+
esac
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
genesis_detect_delivery_target_from_text() {
|
|
27
|
+
local text="${1:-}"
|
|
28
|
+
local lower
|
|
29
|
+
lower="$(printf '%s' "$text" | tr '[:upper:]' '[:lower:]')"
|
|
30
|
+
|
|
31
|
+
local has_web="false"
|
|
32
|
+
local has_api="false"
|
|
33
|
+
local has_cli="false"
|
|
34
|
+
|
|
35
|
+
if echo "$lower" | grep -Eqi '\b(app web|web app|site|website|frontend|front-end|interface|ui|ux|tela|página|pagina|dashboard|painel|pwa)\b'; then
|
|
36
|
+
has_web="true"
|
|
37
|
+
fi
|
|
38
|
+
if echo "$lower" | grep -Eqi '\b(api|endpoint|backend|rest|graphql|webhook|serviço|servico)\b'; then
|
|
39
|
+
has_api="true"
|
|
40
|
+
fi
|
|
41
|
+
if echo "$lower" | grep -Eqi '\b(cli|terminal|console|linha de comando|command line|comando|programinha|programa|script|calcular|converter|ferramenta)\b'; then
|
|
42
|
+
has_cli="true"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
local count=0
|
|
46
|
+
[[ "$has_web" == "true" ]] && count=$((count + 1))
|
|
47
|
+
[[ "$has_api" == "true" ]] && count=$((count + 1))
|
|
48
|
+
[[ "$has_cli" == "true" ]] && count=$((count + 1))
|
|
49
|
+
|
|
50
|
+
if [[ "$count" -gt 1 ]]; then
|
|
51
|
+
printf '%s' "hybrid"
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
if [[ "$has_web" == "true" ]]; then
|
|
55
|
+
printf '%s' "web-ui"
|
|
56
|
+
return 0
|
|
57
|
+
fi
|
|
58
|
+
if [[ "$has_api" == "true" ]]; then
|
|
59
|
+
printf '%s' "api"
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
if [[ "$has_cli" == "true" ]]; then
|
|
63
|
+
printf '%s' "cli"
|
|
64
|
+
return 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
printf '%s' "unknown"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
genesis_cross_validate_delivery_target() {
|
|
71
|
+
local spec_target="${1:-}"
|
|
72
|
+
local raw_idea="${2:-}"
|
|
73
|
+
local text_target
|
|
74
|
+
text_target="$(genesis_detect_delivery_target_from_text "$raw_idea")"
|
|
75
|
+
|
|
76
|
+
if [[ "$text_target" != "unknown" && "$spec_target" != "unknown" && "$text_target" != "$spec_target" ]]; then
|
|
77
|
+
echo "WARNING: delivery_target conflict — spec='$spec_target' vs text='$text_target'. Preferring text." >&2
|
|
78
|
+
printf '%s' "$text_target"
|
|
79
|
+
return 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [[ "$spec_target" == "unknown" && "$text_target" != "unknown" ]]; then
|
|
83
|
+
printf '%s' "$text_target"
|
|
84
|
+
return 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
printf '%s' "$spec_target"
|
|
88
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Step 7: Generate QA contract (qa.sh) from spec
|
|
5
|
+
# Input: stdin JSON (with spec.acceptance_criteria)
|
|
6
|
+
# Output: JSON with qa_contract to stdout
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
source "$SCRIPT_DIR/stack-detection-lib.sh"
|
|
10
|
+
source "$SCRIPT_DIR/genesis-telemetry.sh"
|
|
11
|
+
|
|
12
|
+
if [[ -n "${1:-}" && -f "${1:-}" ]]; then
|
|
13
|
+
INPUT="$(cat "$1")"
|
|
14
|
+
else
|
|
15
|
+
INPUT="$(cat)"
|
|
16
|
+
fi
|
|
17
|
+
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
|
|
18
|
+
SPEC="$(echo "$INPUT" | jq '.spec // {}')"
|
|
19
|
+
TYPE="$(echo "$INPUT" | jq -r '.classification.type // "feature"')"
|
|
20
|
+
METADATA="$(echo "$INPUT" | jq '.metadata // {}')"
|
|
21
|
+
CLASSIFICATION="$(echo "$INPUT" | jq '.classification // {}')"
|
|
22
|
+
INTERVIEW="$(echo "$INPUT" | jq '.interview // {}')"
|
|
23
|
+
IMPACT="$(echo "$INPUT" | jq '.impact // {}')"
|
|
24
|
+
PROJECT_MAP="$(echo "$INPUT" | jq '.project_map // {}')"
|
|
25
|
+
SCAFFOLD="$(echo "$INPUT" | jq '.scaffold // {}')"
|
|
26
|
+
STACK_HINT="$(echo "$SCAFFOLD" | jq -r '.stack // ""' | tr '[:upper:]' '[:lower:]')"
|
|
27
|
+
|
|
28
|
+
genesis_metric_start "generate-qa-contract" "$SESSION_ID"
|
|
29
|
+
echo "Generating QA contract for session $SESSION_ID..." >&2
|
|
30
|
+
|
|
31
|
+
# Detect stack flags using shared stack-detection-lib.sh
|
|
32
|
+
LANGUAGES="$(echo "$PROJECT_MAP" | jq -r '.stats.languages // [] | .[]' 2>/dev/null || echo "")"
|
|
33
|
+
SCOPE_TEXT="$(echo "$SPEC" | jq -r '(.scope_v1 // []) | join(" ")' | tr '[:upper:]' '[:lower:]')"
|
|
34
|
+
|
|
35
|
+
read -r IS_PY IS_JS IS_GO <<< "$(genesis_detect_stack_flags_from_context "$STACK_HINT" "$LANGUAGES" "$SCOPE_TEXT")"
|
|
36
|
+
|
|
37
|
+
if ! $IS_PY && ! $IS_JS && ! $IS_GO; then
|
|
38
|
+
echo "No stack detected — qa.sh will fail closed until stack is explicit" >&2
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo "Stack detected: PY=$IS_PY JS=$IS_JS GO=$IS_GO" >&2
|
|
42
|
+
|
|
43
|
+
# Build acceptance criteria comments for qa.sh
|
|
44
|
+
AC_COMMENTS=""
|
|
45
|
+
AC_INDEX=0
|
|
46
|
+
while IFS= read -r ac; do
|
|
47
|
+
[[ -z "$ac" ]] && continue
|
|
48
|
+
AC_INDEX=$((AC_INDEX + 1))
|
|
49
|
+
AC_COMMENTS="$AC_COMMENTS
|
|
50
|
+
# AC$AC_INDEX: $ac"
|
|
51
|
+
done < <(echo "$SPEC" | jq -r '.acceptance_criteria // [] | .[]')
|
|
52
|
+
|
|
53
|
+
# Generate qa.sh script content
|
|
54
|
+
QA_SCRIPT='#!/usr/bin/env bash
|
|
55
|
+
set -euo pipefail
|
|
56
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
57
|
+
cd "$ROOT_DIR"
|
|
58
|
+
QA_VENV="${QA_VENV:-$HOME/.openclaw/qa-venv}"
|
|
59
|
+
[ -d "$QA_VENV/bin" ] && export PATH="$QA_VENV/bin:$PATH"
|
|
60
|
+
PASS=0; FAIL=0
|
|
61
|
+
gate() { local n="$1"; shift; printf "==> %s\n" "$n"; if "$@"; then PASS=$((PASS+1)); printf " PASS\n"; else FAIL=$((FAIL+1)); printf " FAIL\n"; return 1; fi; }
|
|
62
|
+
|
|
63
|
+
# SECURITY: This script output may be embedded in PR descriptions.
|
|
64
|
+
# It must NEVER print environment variables, tokens, API keys, or host paths.
|
|
65
|
+
|
|
66
|
+
# === Acceptance Criteria ==='"$AC_COMMENTS"'
|
|
67
|
+
|
|
68
|
+
# === Stack Detection ==='
|
|
69
|
+
|
|
70
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
71
|
+
if ! '"$IS_PY"' && ! '"$IS_JS"' && ! '"$IS_GO"'; then
|
|
72
|
+
echo "UNKNOWN STACK: unable to infer stack from scaffold/project map/spec."
|
|
73
|
+
echo "Update scaffold.stack or project_map.stats.languages and regenerate QA contract."
|
|
74
|
+
exit 1
|
|
75
|
+
fi'
|
|
76
|
+
|
|
77
|
+
if $IS_PY; then
|
|
78
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
79
|
+
IS_PY=true
|
|
80
|
+
$IS_PY && command -v ruff >/dev/null && gate "Lint (Python)" ruff check . || true
|
|
81
|
+
$IS_PY && command -v mypy >/dev/null && gate "Types (Python)" mypy . --ignore-missing-imports || true'
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
if $IS_JS; then
|
|
85
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
86
|
+
IS_JS=true
|
|
87
|
+
$IS_JS && [ -f node_modules/.bin/eslint ] && gate "Lint (JS/TS)" npx eslint . || true
|
|
88
|
+
$IS_JS && [ -f tsconfig.json ] && gate "Types (TS)" npx tsc --noEmit || true'
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
if $IS_GO; then
|
|
92
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
93
|
+
IS_GO=true
|
|
94
|
+
$IS_GO && gate "Lint (Go)" go vet ./... || true'
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
98
|
+
|
|
99
|
+
# === Security Gate ===
|
|
100
|
+
command -v openclaw >/dev/null && gate "Security (ClawScan)" openclaw invoke --skill clawscan --tool scan-local --args-json '"'"'{"path":"."}'"'"' || true
|
|
101
|
+
|
|
102
|
+
# === Test Gate ==='
|
|
103
|
+
|
|
104
|
+
if $IS_PY; then
|
|
105
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
106
|
+
$IS_PY && gate "Tests (Python)" python -m pytest -v --tb=short || true'
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if $IS_JS; then
|
|
110
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
111
|
+
$IS_JS && gate "Tests (JS)" npm test || true'
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if $IS_GO; then
|
|
115
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
116
|
+
$IS_GO && gate "Tests (Go)" go test -v ./... || true'
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
120
|
+
|
|
121
|
+
# === Coverage Gate (>= 80%) ===
|
|
122
|
+
'
|
|
123
|
+
|
|
124
|
+
if $IS_PY; then
|
|
125
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
126
|
+
$IS_PY && gate "Coverage (Python >=80%)" python -m pytest -q --cov=. --cov-report=term-missing --cov-fail-under=80 || true'
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
if $IS_JS; then
|
|
130
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
131
|
+
$IS_JS && gate "Coverage (JS/TS >=80%)" npx vitest run --coverage --coverage.thresholds.lines=80 || true'
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
if $IS_GO; then
|
|
135
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
136
|
+
$IS_GO && gate "Coverage (Go >=80%)" bash -lc '\''go test -coverprofile=coverage.out ./... && go tool cover -func=coverage.out | awk "/^total:/ {gsub(/%/,\"\",\\$3); exit !(\\$3>=80)}"'\'' || true'
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
QA_SCRIPT="$QA_SCRIPT"'
|
|
140
|
+
|
|
141
|
+
TOTAL=$((PASS+FAIL))
|
|
142
|
+
if [[ "$TOTAL" -eq 0 ]]; then
|
|
143
|
+
echo "NO QA GATES EXECUTED"
|
|
144
|
+
exit 1
|
|
145
|
+
fi
|
|
146
|
+
echo "QA: $PASS/$TOTAL passed"
|
|
147
|
+
[ "$FAIL" -gt 0 ] && echo "FAILED" && exit 1
|
|
148
|
+
echo "ALL QA GATES PASSED"'
|
|
149
|
+
|
|
150
|
+
# Build gates list
|
|
151
|
+
GATES='["lint", "types", "security", "tests", "coverage"]'
|
|
152
|
+
|
|
153
|
+
# Build acceptance tests from ACs
|
|
154
|
+
ACCEPTANCE_TESTS="$(echo "$SPEC" | jq '[.acceptance_criteria // [] | .[] | {criterion: ., gate: "tests", automated: false}]')"
|
|
155
|
+
|
|
156
|
+
echo "QA contract generated: $AC_INDEX acceptance criteria, gates=$(echo "$GATES" | jq 'length')" >&2
|
|
157
|
+
|
|
158
|
+
jq -n \
|
|
159
|
+
--arg sid "$SESSION_ID" \
|
|
160
|
+
--arg script "$QA_SCRIPT" \
|
|
161
|
+
--argjson gates "$GATES" \
|
|
162
|
+
--argjson tests "$ACCEPTANCE_TESTS" \
|
|
163
|
+
--argjson spec "$SPEC" \
|
|
164
|
+
--argjson cls "$CLASSIFICATION" \
|
|
165
|
+
--argjson interview "$INTERVIEW" \
|
|
166
|
+
--argjson impact "$IMPACT" \
|
|
167
|
+
--argjson map "$PROJECT_MAP" \
|
|
168
|
+
--argjson scaffold "$SCAFFOLD" \
|
|
169
|
+
--argjson meta "$METADATA" \
|
|
170
|
+
'{
|
|
171
|
+
session_id: $sid,
|
|
172
|
+
step: "qa",
|
|
173
|
+
qa_contract: {
|
|
174
|
+
script_content: $script,
|
|
175
|
+
gates: $gates,
|
|
176
|
+
coverage_threshold: 80,
|
|
177
|
+
acceptance_tests: $tests
|
|
178
|
+
},
|
|
179
|
+
spec: $spec,
|
|
180
|
+
classification: $cls,
|
|
181
|
+
interview: $interview,
|
|
182
|
+
impact: $impact,
|
|
183
|
+
project_map: $map,
|
|
184
|
+
scaffold: $scaffold,
|
|
185
|
+
metadata: $meta
|
|
186
|
+
}'
|
|
187
|
+
|
|
188
|
+
genesis_metric_end "ok"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Step 4: Generate structured specification from classification + interview answers
|
|
5
|
+
# Input: stdin JSON (classification + interview with answers)
|
|
6
|
+
# Output: JSON with full spec to stdout
|
|
7
|
+
# Note: The LLM fills answers via llm-task. This script structures them into
|
|
8
|
+
# the mandatory headings required by issue_checklist.py:
|
|
9
|
+
# Objetivo, Escopo V1, Fora de escopo, Acceptance Criteria, DoD, Restrições
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
source "$SCRIPT_DIR/delivery-target-lib.sh"
|
|
13
|
+
|
|
14
|
+
if [[ -n "${1:-}" && -f "${1:-}" ]]; then
|
|
15
|
+
INPUT="$(cat "$1")"
|
|
16
|
+
else
|
|
17
|
+
INPUT="$(cat)"
|
|
18
|
+
fi
|
|
19
|
+
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
|
|
20
|
+
RAW_IDEA="$(echo "$INPUT" | jq -r '.raw_idea')"
|
|
21
|
+
TYPE="$(echo "$INPUT" | jq -r '.classification.type')"
|
|
22
|
+
CLASSIFICATION="$(echo "$INPUT" | jq '.classification')"
|
|
23
|
+
INTERVIEW="$(echo "$INPUT" | jq '.interview // {}')"
|
|
24
|
+
METADATA="$(echo "$INPUT" | jq '.metadata // {}')"
|
|
25
|
+
|
|
26
|
+
echo "Generating specification for session $SESSION_ID (type=$TYPE)..." >&2
|
|
27
|
+
|
|
28
|
+
# Extract structured data from the LLM interview output
|
|
29
|
+
# The llm-task step should return a JSON with these fields
|
|
30
|
+
TITLE="$(echo "$INPUT" | jq -r '.spec_data.title // .interview.spec_data.title // .raw_idea' | cut -c1-120)"
|
|
31
|
+
OBJECTIVE="$(echo "$INPUT" | jq -r '.spec_data.objective // .interview.spec_data.objective // "See raw idea"')"
|
|
32
|
+
SCOPE="$(echo "$INPUT" | jq '.spec_data.scope_v1 // .interview.spec_data.scope_v1 // [.raw_idea]')"
|
|
33
|
+
OUT_OF_SCOPE="$(echo "$INPUT" | jq '.spec_data.out_of_scope // .interview.spec_data.out_of_scope // ["To be defined during implementation"]')"
|
|
34
|
+
ACS="$(echo "$INPUT" | jq '.spec_data.acceptance_criteria // .interview.spec_data.acceptance_criteria // ["Feature works as described in the objective"]')"
|
|
35
|
+
DOD="$(echo "$INPUT" | jq '.spec_data.definition_of_done // .interview.spec_data.definition_of_done // ["Code reviewed and merged", "Tests pass", "QA contract passes"]')"
|
|
36
|
+
CONSTRAINTS="$(echo "$INPUT" | jq -r '.spec_data.constraints // .interview.spec_data.constraints // "None specified"')"
|
|
37
|
+
RISKS="$(echo "$INPUT" | jq '.spec_data.risks // .interview.spec_data.risks // []')"
|
|
38
|
+
DELIVERY_TARGET_RAW="$(echo "$INPUT" | jq -r '.spec_data.delivery_target // .interview.spec_data.delivery_target // .classification.delivery_target // .metadata.delivery_target // empty')"
|
|
39
|
+
if [[ -z "$DELIVERY_TARGET_RAW" || "$DELIVERY_TARGET_RAW" == "null" ]]; then
|
|
40
|
+
DELIVERY_TARGET="$(genesis_detect_delivery_target_from_text "$RAW_IDEA")"
|
|
41
|
+
else
|
|
42
|
+
DELIVERY_TARGET_NORMALIZED="$(genesis_normalize_delivery_target "$DELIVERY_TARGET_RAW")"
|
|
43
|
+
DELIVERY_TARGET="$(genesis_cross_validate_delivery_target "$DELIVERY_TARGET_NORMALIZED" "$RAW_IDEA")"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Validate minimum requirements
|
|
47
|
+
scope_count="$(echo "$SCOPE" | jq 'length')"
|
|
48
|
+
ac_count="$(echo "$ACS" | jq 'length')"
|
|
49
|
+
dod_count="$(echo "$DOD" | jq 'length')"
|
|
50
|
+
|
|
51
|
+
if [[ "$scope_count" -lt 1 ]]; then
|
|
52
|
+
echo "WARNING: Empty scope — using raw idea as scope item" >&2
|
|
53
|
+
SCOPE="$(jq -n --arg i "$RAW_IDEA" '[$i]')"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [[ "$ac_count" -lt 1 ]]; then
|
|
57
|
+
echo "WARNING: No acceptance criteria — adding default" >&2
|
|
58
|
+
ACS='["Feature works as described in the objective"]'
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
if [[ "$dod_count" -lt 1 ]]; then
|
|
62
|
+
echo "WARNING: No definition of done — adding defaults" >&2
|
|
63
|
+
DOD='["Code reviewed and merged", "Tests pass", "QA contract passes"]'
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
ACS_JOINED_LOWER="$(echo "$ACS" | jq -r 'join(" ")' | tr '[:upper:]' '[:lower:]')"
|
|
67
|
+
append_ac() {
|
|
68
|
+
local item="$1"
|
|
69
|
+
ACS="$(echo "$ACS" | jq --arg i "$item" '. + [$i]')"
|
|
70
|
+
}
|
|
71
|
+
append_dod() {
|
|
72
|
+
local item="$1"
|
|
73
|
+
DOD="$(echo "$DOD" | jq --arg i "$item" '. + [$i]')"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
case "$DELIVERY_TARGET" in
|
|
77
|
+
web-ui)
|
|
78
|
+
if ! echo "$ACS_JOINED_LOWER" | grep -Eqi '\b(tela|página|pagina|ui|interface|dashboard|fluxo)\b'; then
|
|
79
|
+
append_ac "Existe ao menos uma tela funcional do fluxo principal, navegavel de ponta a ponta."
|
|
80
|
+
fi
|
|
81
|
+
;;
|
|
82
|
+
api)
|
|
83
|
+
if ! echo "$ACS_JOINED_LOWER" | grep -Eqi '\b(api|endpoint|rota|route|http|rest)\b'; then
|
|
84
|
+
append_ac "Existe ao menos um endpoint/API funcional do fluxo principal, com resposta valida."
|
|
85
|
+
fi
|
|
86
|
+
;;
|
|
87
|
+
cli)
|
|
88
|
+
if ! echo "$ACS_JOINED_LOWER" | grep -Eqi '\b(cli|terminal|comando|linha de comando|console)\b'; then
|
|
89
|
+
append_ac "Existe ao menos um comando CLI funcional do fluxo principal."
|
|
90
|
+
fi
|
|
91
|
+
;;
|
|
92
|
+
hybrid)
|
|
93
|
+
if ! echo "$ACS_JOINED_LOWER" | grep -Eqi '\b(tela|página|pagina|ui|interface|dashboard|fluxo)\b'; then
|
|
94
|
+
append_ac "Existe ao menos uma interface/tela funcional para o fluxo principal."
|
|
95
|
+
fi
|
|
96
|
+
if ! echo "$ACS_JOINED_LOWER" | grep -Eqi '\b(api|endpoint|rota|route|http|rest)\b'; then
|
|
97
|
+
append_ac "Existe ao menos uma API/endpoint funcional para suportar o fluxo principal."
|
|
98
|
+
fi
|
|
99
|
+
;;
|
|
100
|
+
esac
|
|
101
|
+
|
|
102
|
+
# Auth requirements gate (hybrid policy):
|
|
103
|
+
# - If idea indicates auth/per-profile needs and spec has no auth evidence,
|
|
104
|
+
# auto-append minimal AC/DoD to prevent contract drift.
|
|
105
|
+
AUTH_REGEX='\b(login|autentic|senha|perfil|permiss|acesso|rbac|admin)\b'
|
|
106
|
+
AUTH_SIGNAL=false
|
|
107
|
+
AUTH_EVIDENCE=false
|
|
108
|
+
AUTH_SIGNAL_TEXT="$(printf '%s %s' "$RAW_IDEA" "$OBJECTIVE" | tr '[:upper:]' '[:lower:]')"
|
|
109
|
+
AUTH_EVIDENCE_TEXT="$(printf '%s %s %s' "$OBJECTIVE" "$(echo "$SCOPE" | jq -r 'join(" ")')" "$(echo "$ACS" | jq -r 'join(" ")')" | tr '[:upper:]' '[:lower:]')"
|
|
110
|
+
|
|
111
|
+
if echo "$AUTH_SIGNAL_TEXT" | grep -Eqi "$AUTH_REGEX"; then
|
|
112
|
+
AUTH_SIGNAL=true
|
|
113
|
+
fi
|
|
114
|
+
if echo "$AUTH_EVIDENCE_TEXT" | grep -Eqi "$AUTH_REGEX"; then
|
|
115
|
+
AUTH_EVIDENCE=true
|
|
116
|
+
fi
|
|
117
|
+
if [[ "$AUTH_SIGNAL" == "true" && "$AUTH_EVIDENCE" != "true" ]]; then
|
|
118
|
+
append_ac "Usuarios autenticados conseguem iniciar sessao com credenciais validas."
|
|
119
|
+
append_ac "Acoes criticas exigem autorizacao por perfil (ex.: admin/operador/leitura)."
|
|
120
|
+
append_dod "Existe teste cobrindo autorizacao por perfil em ao menos um fluxo critico."
|
|
121
|
+
AUTH_EVIDENCE=true
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
CONSTRAINTS_BASE="$CONSTRAINTS"
|
|
125
|
+
if [[ -n "$CONSTRAINTS_BASE" && "$CONSTRAINTS_BASE" != "None specified" ]]; then
|
|
126
|
+
CONSTRAINTS="$CONSTRAINTS_BASE Delivery target: $DELIVERY_TARGET."
|
|
127
|
+
else
|
|
128
|
+
CONSTRAINTS="Delivery target: $DELIVERY_TARGET."
|
|
129
|
+
fi
|
|
130
|
+
ac_count="$(echo "$ACS" | jq 'length')"
|
|
131
|
+
dod_count="$(echo "$DOD" | jq 'length')"
|
|
132
|
+
META_ENRICHED="$(echo "$METADATA" | jq --argjson signal "$AUTH_SIGNAL" --argjson evidence "$AUTH_EVIDENCE" '. + {auth_gate: {signal: $signal, evidence: $evidence}}')"
|
|
133
|
+
|
|
134
|
+
echo "Spec: title='$TITLE', scope=$scope_count items, ACs=$ac_count, DoD=$dod_count" >&2
|
|
135
|
+
|
|
136
|
+
jq -n \
|
|
137
|
+
--arg sid "$SESSION_ID" \
|
|
138
|
+
--arg raw_idea "$RAW_IDEA" \
|
|
139
|
+
--arg title "$TITLE" \
|
|
140
|
+
--arg type "$TYPE" \
|
|
141
|
+
--arg objective "$OBJECTIVE" \
|
|
142
|
+
--argjson scope "$SCOPE" \
|
|
143
|
+
--argjson oos "$OUT_OF_SCOPE" \
|
|
144
|
+
--argjson acs "$ACS" \
|
|
145
|
+
--argjson dod "$DOD" \
|
|
146
|
+
--arg constraints "$CONSTRAINTS" \
|
|
147
|
+
--argjson risks "$RISKS" \
|
|
148
|
+
--arg delivery_target "$DELIVERY_TARGET" \
|
|
149
|
+
--argjson cls "$CLASSIFICATION" \
|
|
150
|
+
--argjson interview "$INTERVIEW" \
|
|
151
|
+
--argjson meta "$META_ENRICHED" \
|
|
152
|
+
'{
|
|
153
|
+
session_id: $sid,
|
|
154
|
+
step: "spec",
|
|
155
|
+
raw_idea: $raw_idea,
|
|
156
|
+
spec: {
|
|
157
|
+
title: $title,
|
|
158
|
+
type: $type,
|
|
159
|
+
objective: $objective,
|
|
160
|
+
scope_v1: $scope,
|
|
161
|
+
out_of_scope: $oos,
|
|
162
|
+
acceptance_criteria: $acs,
|
|
163
|
+
definition_of_done: $dod,
|
|
164
|
+
constraints: $constraints,
|
|
165
|
+
risks: $risks,
|
|
166
|
+
delivery_target: $delivery_target
|
|
167
|
+
},
|
|
168
|
+
classification: $cls,
|
|
169
|
+
interview: $interview,
|
|
170
|
+
metadata: $meta
|
|
171
|
+
}'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# genesis-telemetry.sh — Structured telemetry for Genesis pipeline steps
|
|
3
|
+
# Emits JSON metrics to a log file for observability.
|
|
4
|
+
# shellcheck shell=bash
|
|
5
|
+
|
|
6
|
+
if [ -z "${BASH_VERSION:-}" ]; then
|
|
7
|
+
echo "ERROR: genesis-telemetry.sh requires bash." >&2
|
|
8
|
+
return 1 2>/dev/null || exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
[[ -n "${_GENESIS_TELEMETRY_LOADED:-}" ]] && return 0
|
|
12
|
+
_GENESIS_TELEMETRY_LOADED=1
|
|
13
|
+
|
|
14
|
+
GENESIS_METRICS_FILE="${GENESIS_METRICS_FILE:-$HOME/.openclaw/workspace/logs/genesis-metrics.jsonl}"
|
|
15
|
+
mkdir -p "$(dirname "$GENESIS_METRICS_FILE")" 2>/dev/null || true
|
|
16
|
+
|
|
17
|
+
# _genesis_epoch_ms — current time in milliseconds
|
|
18
|
+
_genesis_epoch_ms() {
|
|
19
|
+
if date +%s%3N >/dev/null 2>&1; then
|
|
20
|
+
date +%s%3N
|
|
21
|
+
else
|
|
22
|
+
echo "$(date +%s)000"
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# genesis_metric_start <step_name> <session_id>
|
|
27
|
+
# Call at the beginning of a pipeline step. Stores start time in env.
|
|
28
|
+
# Returns: nothing (sets _GENESIS_METRIC_START_MS)
|
|
29
|
+
genesis_metric_start() {
|
|
30
|
+
local step="${1:-unknown}"
|
|
31
|
+
local session_id="${2:-}"
|
|
32
|
+
export _GENESIS_METRIC_STEP="$step"
|
|
33
|
+
export _GENESIS_METRIC_SESSION="$session_id"
|
|
34
|
+
export _GENESIS_METRIC_START_MS="$(_genesis_epoch_ms)"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# genesis_metric_end <status> [error_message]
|
|
38
|
+
# Call at the end of a pipeline step. Emits JSON metric line.
|
|
39
|
+
# status: "ok", "error", "skipped"
|
|
40
|
+
genesis_metric_end() {
|
|
41
|
+
local status="${1:-ok}"
|
|
42
|
+
local error_msg="${2:-}"
|
|
43
|
+
local end_ms duration_ms
|
|
44
|
+
|
|
45
|
+
end_ms="$(_genesis_epoch_ms)"
|
|
46
|
+
if [[ -n "${_GENESIS_METRIC_START_MS:-}" ]]; then
|
|
47
|
+
duration_ms=$(( end_ms - _GENESIS_METRIC_START_MS ))
|
|
48
|
+
else
|
|
49
|
+
duration_ms=0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
jq -n -c \
|
|
53
|
+
--arg step "${_GENESIS_METRIC_STEP:-unknown}" \
|
|
54
|
+
--arg session_id "${_GENESIS_METRIC_SESSION:-}" \
|
|
55
|
+
--arg status "$status" \
|
|
56
|
+
--argjson duration_ms "$duration_ms" \
|
|
57
|
+
--arg error "$error_msg" \
|
|
58
|
+
--arg timestamp "$(date -Iseconds)" \
|
|
59
|
+
'{
|
|
60
|
+
step: $step,
|
|
61
|
+
session_id: $session_id,
|
|
62
|
+
status: $status,
|
|
63
|
+
duration_ms: $duration_ms,
|
|
64
|
+
timestamp: $timestamp
|
|
65
|
+
}
|
|
66
|
+
+ (if $error != "" then {error: $error} else {} end)
|
|
67
|
+
' >> "$GENESIS_METRICS_FILE" 2>/dev/null || true
|
|
68
|
+
|
|
69
|
+
unset _GENESIS_METRIC_STEP _GENESIS_METRIC_SESSION _GENESIS_METRIC_START_MS
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# genesis_emit_metric <step> <session_id> <status> <duration_ms> [error]
|
|
73
|
+
# Direct metric emission (for scripts that don't use start/end pattern).
|
|
74
|
+
genesis_emit_metric() {
|
|
75
|
+
local step="${1:-unknown}"
|
|
76
|
+
local session_id="${2:-}"
|
|
77
|
+
local status="${3:-ok}"
|
|
78
|
+
local duration_ms="${4:-0}"
|
|
79
|
+
local error_msg="${5:-}"
|
|
80
|
+
|
|
81
|
+
jq -n -c \
|
|
82
|
+
--arg step "$step" \
|
|
83
|
+
--arg session_id "$session_id" \
|
|
84
|
+
--arg status "$status" \
|
|
85
|
+
--argjson duration_ms "$duration_ms" \
|
|
86
|
+
--arg error "$error_msg" \
|
|
87
|
+
--arg timestamp "$(date -Iseconds)" \
|
|
88
|
+
'{
|
|
89
|
+
step: $step,
|
|
90
|
+
session_id: $session_id,
|
|
91
|
+
status: $status,
|
|
92
|
+
duration_ms: $duration_ms,
|
|
93
|
+
timestamp: $timestamp
|
|
94
|
+
}
|
|
95
|
+
+ (if $error != "" then {error: $error} else {} end)
|
|
96
|
+
' >> "$GENESIS_METRICS_FILE" 2>/dev/null || true
|
|
97
|
+
}
|