ai-core-framework 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/.claude-plugin/plugin.json +21 -0
- package/.codex-plugin/plugin.json +35 -0
- package/.cursor-plugin/plugin.json +22 -0
- package/README.md +173 -0
- package/bin/ai-core-framework.js +110 -0
- package/core/README.md +162 -0
- package/core/agents/README.md +32 -0
- package/core/agents/business-analyst.md +269 -0
- package/core/agents/developer.md +375 -0
- package/core/agents/qa-tester.md +477 -0
- package/core/agents/scrum-master.md +136 -0
- package/core/agents/tech-lead.md +345 -0
- package/core/config/backlog.schema.json +38 -0
- package/core/config/docs-policy.default.json +37 -0
- package/core/config/release.schema.json +120 -0
- package/core/config/ticket.schema.json +253 -0
- package/core/rules/00-global-rules.md +373 -0
- package/core/rules/01-git-workflow.md +388 -0
- package/core/rules/02-code-quality.md +77 -0
- package/core/rules/03-security.md +78 -0
- package/core/rules/04-documentation.md +72 -0
- package/core/rules/05-testing-mandatory.md +374 -0
- package/core/rules/06-approval-gates.md +388 -0
- package/core/rules/07-definition-of-ready.md +112 -0
- package/core/rules/08-definition-of-done.md +149 -0
- package/core/scripts/ai-core.sh +456 -0
- package/core/scripts/generate-views.sh +210 -0
- package/core/scripts/install-codex-prompts.sh +127 -0
- package/core/scripts/log-user-request.sh +113 -0
- package/core/scripts/setup-project.sh +183 -0
- package/core/scripts/sync-platforms.sh +322 -0
- package/core/scripts/validate-audit-log.sh +73 -0
- package/core/scripts/validate-docs.sh +365 -0
- package/core/scripts/validate-permissions.sh +132 -0
- package/core/scripts/validate-state.sh +611 -0
- package/core/scripts/workflow.sh +513 -0
- package/core/skills/README.md +21 -0
- package/core/skills/ai-core-commands/SKILL.md +86 -0
- package/core/skills/brainstorming/SKILL.md +40 -0
- package/core/skills/development-implement-task/SKILL.md +308 -0
- package/core/skills/executing-ticket/SKILL.md +28 -0
- package/core/skills/git-branch-status/SKILL.md +56 -0
- package/core/skills/git-cleanup-branches/SKILL.md +57 -0
- package/core/skills/git-scan-untracked/SKILL.md +50 -0
- package/core/skills/meta-generate-views/SKILL.md +54 -0
- package/core/skills/meta-request-log/SKILL.md +61 -0
- package/core/skills/meta-sprint-report/SKILL.md +59 -0
- package/core/skills/meta-sync-platforms/SKILL.md +53 -0
- package/core/skills/meta-ticket-health/SKILL.md +61 -0
- package/core/skills/meta-validate-audit-log/SKILL.md +42 -0
- package/core/skills/meta-validate-docs/SKILL.md +58 -0
- package/core/skills/meta-validate-permissions/SKILL.md +53 -0
- package/core/skills/meta-validate-state/SKILL.md +58 -0
- package/core/skills/planning-analyze-requirements/SKILL.md +471 -0
- package/core/skills/planning-backlog-status/SKILL.md +57 -0
- package/core/skills/planning-document-existing-requirements/SKILL.md +246 -0
- package/core/skills/planning-estimate-task/SKILL.md +60 -0
- package/core/skills/planning-groom-ticket/SKILL.md +442 -0
- package/core/skills/planning-mark-ready/SKILL.md +111 -0
- package/core/skills/planning-plan-refactor/SKILL.md +66 -0
- package/core/skills/planning-plan-sprint/SKILL.md +112 -0
- package/core/skills/planning-prioritize-backlog/SKILL.md +62 -0
- package/core/skills/planning-write-plan/SKILL.md +68 -0
- package/core/skills/project-detect-stack/SKILL.md +71 -0
- package/core/skills/project-discover-codebase/SKILL.md +74 -0
- package/core/skills/project-setup-project/SKILL.md +113 -0
- package/core/skills/qa-bug-status/SKILL.md +52 -0
- package/core/skills/qa-report-bug/SKILL.md +518 -0
- package/core/skills/qa-smoke-test/SKILL.md +387 -0
- package/core/skills/qa-triage-bug/SKILL.md +62 -0
- package/core/skills/qa-verify-fix/SKILL.md +446 -0
- package/core/skills/release-hotfix/SKILL.md +117 -0
- package/core/skills/release-release/SKILL.md +123 -0
- package/core/skills/release-rollback/SKILL.md +62 -0
- package/core/skills/review-create-pr/SKILL.md +418 -0
- package/core/skills/review-merge-pr/SKILL.md +425 -0
- package/core/skills/review-techlead-review/SKILL.md +547 -0
- package/core/skills/using-ai-core/SKILL.md +72 -0
- package/core/skills/verification-before-done/SKILL.md +35 -0
- package/core/skills/writing-implementation-plan/SKILL.md +45 -0
- package/core/templates/ci/ai-core-governance.yml +112 -0
- package/core/templates/ci/node-pnpm.yml +35 -0
- package/core/templates/pm/retrospective-template.md +47 -0
- package/core/templates/pm/sprint-plan-template.md +45 -0
- package/core/templates/pr/pull-request-template.md +247 -0
- package/core/templates/project/CODEOWNERS +11 -0
- package/core/templates/project/docs-policy.json +3 -0
- package/core/templates/project/project-config.yaml +137 -0
- package/core/templates/project/project-structure.yaml +76 -0
- package/core/templates/qa/bug-report-template.md +371 -0
- package/core/templates/qa/test-plan-template.md +57 -0
- package/core/templates/release/release-record-template.json +67 -0
- package/core/templates/requirements/PRD-template.md +58 -0
- package/core/templates/requirements/user-story-template.md +381 -0
- package/core/templates/technical/ADR-template.md +46 -0
- package/core/templates/technical/refactor-plan-template.md +84 -0
- package/core/templates/technical/tech-design-template.md +71 -0
- package/core/workflows/bug-lifecycle.md +56 -0
- package/core/workflows/feature-lifecycle.md +347 -0
- package/core/workflows/hotfix-lifecycle.md +65 -0
- package/core/workflows/sprint-lifecycle.md +56 -0
- package/lib/install-codex.js +85 -0
- package/package.json +36 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/validate-docs.sh
|
|
3
|
+
#
|
|
4
|
+
# Enforces documentation and DoD evidence gates from RULE 04 and RULE 08.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bash scripts/validate-docs.sh
|
|
8
|
+
# bash scripts/validate-docs.sh --base origin/main
|
|
9
|
+
# bash scripts/validate-docs.sh --staged
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
RED='\033[0;31m'
|
|
14
|
+
GREEN='\033[0;32m'
|
|
15
|
+
YELLOW='\033[1;33m'
|
|
16
|
+
BLUE='\033[0;34m'
|
|
17
|
+
NC='\033[0m'
|
|
18
|
+
|
|
19
|
+
log_info() { echo -e "${BLUE}i${NC} $1"; }
|
|
20
|
+
log_pass() { echo -e "${GREEN}+${NC} $1"; }
|
|
21
|
+
log_warn() { echo -e "${YELLOW}!${NC} $1"; }
|
|
22
|
+
log_fail() { echo -e "${RED}x${NC} $1"; }
|
|
23
|
+
|
|
24
|
+
BASE_REF=""
|
|
25
|
+
MODE="auto"
|
|
26
|
+
|
|
27
|
+
while [ $# -gt 0 ]; do
|
|
28
|
+
case "$1" in
|
|
29
|
+
--base)
|
|
30
|
+
BASE_REF="${2:-}"
|
|
31
|
+
shift 2
|
|
32
|
+
;;
|
|
33
|
+
--staged)
|
|
34
|
+
MODE="staged"
|
|
35
|
+
shift
|
|
36
|
+
;;
|
|
37
|
+
*)
|
|
38
|
+
log_fail "Unknown argument: $1"
|
|
39
|
+
exit 2
|
|
40
|
+
;;
|
|
41
|
+
esac
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
require_git() {
|
|
45
|
+
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
|
46
|
+
log_fail "Not in a git repository"
|
|
47
|
+
exit 2
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get_changed_files() {
|
|
52
|
+
if [ "$MODE" = "staged" ]; then
|
|
53
|
+
git diff --cached --name-only --diff-filter=ACMR
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [ -n "$BASE_REF" ]; then
|
|
58
|
+
git diff --name-only --diff-filter=ACMR "$BASE_REF"...HEAD
|
|
59
|
+
return 0
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
local staged_count
|
|
63
|
+
staged_count=$(git diff --cached --name-only --diff-filter=ACMR | grep -c . || true)
|
|
64
|
+
if [ "$staged_count" -gt 0 ]; then
|
|
65
|
+
git diff --cached --name-only --diff-filter=ACMR
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
local working_count
|
|
70
|
+
working_count=$(git diff --name-only --diff-filter=ACMR | grep -c . || true)
|
|
71
|
+
if [ "$working_count" -gt 0 ]; then
|
|
72
|
+
git diff --name-only --diff-filter=ACMR
|
|
73
|
+
git ls-files --others --exclude-standard
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
local default_ref=""
|
|
78
|
+
for ref in origin/main origin/develop main develop HEAD~1; do
|
|
79
|
+
if git rev-parse --verify "$ref" >/dev/null 2>&1; then
|
|
80
|
+
default_ref="$ref"
|
|
81
|
+
break
|
|
82
|
+
fi
|
|
83
|
+
done
|
|
84
|
+
|
|
85
|
+
if [ -n "$default_ref" ]; then
|
|
86
|
+
git diff --name-only --diff-filter=ACMR "$default_ref"...HEAD 2>/dev/null || git diff --name-only --diff-filter=ACMR "$default_ref" HEAD
|
|
87
|
+
else
|
|
88
|
+
git ls-files
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
contains_any() {
|
|
93
|
+
local content="$1"
|
|
94
|
+
local pattern="$2"
|
|
95
|
+
echo "$content" | grep -Eq "$pattern"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
has_changed_path() {
|
|
99
|
+
local files="$1"
|
|
100
|
+
local pattern="$2"
|
|
101
|
+
echo "$files" | grep -Eq "$pattern"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
load_docs_policy() {
|
|
105
|
+
DOCS_POLICY_FILE="config/docs-policy.json"
|
|
106
|
+
|
|
107
|
+
if [ -f "$DOCS_POLICY_FILE" ]; then
|
|
108
|
+
local extends
|
|
109
|
+
extends=$(jq -r '.extends // empty' "$DOCS_POLICY_FILE")
|
|
110
|
+
if [ -n "$extends" ] && [ -f "$extends" ]; then
|
|
111
|
+
DOCS_POLICY_JSON=$(jq -s '.[0] * .[1]' "$extends" "$DOCS_POLICY_FILE")
|
|
112
|
+
return 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
DOCS_POLICY_JSON=$(jq '.' "$DOCS_POLICY_FILE")
|
|
116
|
+
return 0
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
DOCS_POLICY_FILE="core/config/docs-policy.default.json"
|
|
120
|
+
DOCS_POLICY_JSON=$(jq '.' "$DOCS_POLICY_FILE")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
policy_prefix_regex() {
|
|
124
|
+
local key="$1"
|
|
125
|
+
local fallback="$2"
|
|
126
|
+
local values
|
|
127
|
+
values=$(printf '%s' "$DOCS_POLICY_JSON" | jq -r --arg key "$key" '.[$key][]? // empty' | sed 's/[.[\*^$()+?{}|]/\\&/g' | tr '\n' '|' | sed 's/|$//')
|
|
128
|
+
|
|
129
|
+
if [ -z "$values" ]; then
|
|
130
|
+
printf '%s' "$fallback"
|
|
131
|
+
return 0
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
printf '(^|/)(%s)(/|$)' "$values"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
policy_file_regex() {
|
|
138
|
+
local key="$1"
|
|
139
|
+
local fallback="$2"
|
|
140
|
+
local values
|
|
141
|
+
values=$(printf '%s' "$DOCS_POLICY_JSON" | jq -r --arg key "$key" '.[$key][]? // empty' | sed 's/[.[\*^$()+?{}|]/\\&/g' | tr '\n' '|' | sed 's/|$//')
|
|
142
|
+
|
|
143
|
+
if [ -z "$values" ]; then
|
|
144
|
+
printf '%s' "$fallback"
|
|
145
|
+
return 0
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
printf '(^|/)(%s)(/|$)' "$values"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
current_ticket_id() {
|
|
152
|
+
local branch
|
|
153
|
+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)
|
|
154
|
+
if [[ "$branch" =~ (TICKET-[0-9]{3,}) ]]; then
|
|
155
|
+
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
156
|
+
return 0
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
echo "$CHANGED_FILES" | grep -Eo 'TICKET-[0-9]{3,}' | head -1 || true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
validate_ticket_doc_paths() {
|
|
163
|
+
local errors=0
|
|
164
|
+
|
|
165
|
+
if [ ! -d "project/tickets" ]; then
|
|
166
|
+
return 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
while IFS= read -r -d '' ticket; do
|
|
170
|
+
local ticket_id
|
|
171
|
+
ticket_id=$(jq -r '.id // empty' "$ticket")
|
|
172
|
+
|
|
173
|
+
local spec_path plan_path
|
|
174
|
+
spec_path=$(jq -r '.spec_path // empty' "$ticket")
|
|
175
|
+
if [ -n "$spec_path" ] && [ ! -e "$spec_path" ]; then
|
|
176
|
+
log_fail "$ticket_id: spec_path does not exist: $spec_path"
|
|
177
|
+
errors=$((errors + 1))
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
plan_path=$(jq -r '.implementation_plan_path // empty' "$ticket")
|
|
181
|
+
if [ -n "$plan_path" ] && [ ! -e "$plan_path" ]; then
|
|
182
|
+
log_fail "$ticket_id: implementation_plan_path does not exist: $plan_path"
|
|
183
|
+
errors=$((errors + 1))
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
while IFS= read -r path; do
|
|
187
|
+
[ -z "$path" ] && continue
|
|
188
|
+
if [ ! -e "$path" ]; then
|
|
189
|
+
log_fail "$ticket_id: documentation path does not exist: $path"
|
|
190
|
+
errors=$((errors + 1))
|
|
191
|
+
fi
|
|
192
|
+
done < <(jq -r '.documentation.paths[]? // empty' "$ticket")
|
|
193
|
+
|
|
194
|
+
local adr_required adr_path
|
|
195
|
+
adr_required=$(jq -r '.adr.required // false' "$ticket")
|
|
196
|
+
adr_path=$(jq -r '.adr.path // empty' "$ticket")
|
|
197
|
+
if [ "$adr_required" = "true" ]; then
|
|
198
|
+
if [ -z "$adr_path" ] || [ ! -e "$adr_path" ]; then
|
|
199
|
+
log_fail "$ticket_id: ADR required but adr.path is missing or does not exist"
|
|
200
|
+
errors=$((errors + 1))
|
|
201
|
+
fi
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
local runbook_required runbook_path
|
|
205
|
+
runbook_required=$(jq -r '.runbook.required // false' "$ticket")
|
|
206
|
+
runbook_path=$(jq -r '.runbook.path // empty' "$ticket")
|
|
207
|
+
if [ "$runbook_required" = "true" ]; then
|
|
208
|
+
if [ -z "$runbook_path" ] || [ ! -e "$runbook_path" ]; then
|
|
209
|
+
log_fail "$ticket_id: runbook required but runbook.path is missing or does not exist"
|
|
210
|
+
errors=$((errors + 1))
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
done < <(find project/tickets -name '*.json' -type f -print0)
|
|
214
|
+
|
|
215
|
+
return "$errors"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
validate_done_tickets() {
|
|
219
|
+
local errors=0
|
|
220
|
+
|
|
221
|
+
if [ ! -d "project/tickets" ]; then
|
|
222
|
+
return 0
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
while IFS= read -r -d '' ticket; do
|
|
226
|
+
local status ticket_id
|
|
227
|
+
status=$(jq -r '.status // empty' "$ticket")
|
|
228
|
+
[ "$status" = "DONE" ] || continue
|
|
229
|
+
|
|
230
|
+
ticket_id=$(jq -r '.id // empty' "$ticket")
|
|
231
|
+
|
|
232
|
+
for field in code_complete tests_passed docs_updated review_approved qa_verified release_notes_updated security_checked; do
|
|
233
|
+
if [ "$(jq -r ".dod_checklist.$field // false" "$ticket")" != "true" ]; then
|
|
234
|
+
log_fail "$ticket_id: DONE requires dod_checklist.$field=true"
|
|
235
|
+
errors=$((errors + 1))
|
|
236
|
+
fi
|
|
237
|
+
done
|
|
238
|
+
|
|
239
|
+
if [ "$(jq -r '.documentation.required // false' "$ticket")" = "true" ] &&
|
|
240
|
+
[ "$(jq -r '.documentation.updated // false' "$ticket")" != "true" ]; then
|
|
241
|
+
log_fail "$ticket_id: documentation.required=true but documentation.updated is not true"
|
|
242
|
+
errors=$((errors + 1))
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
if [ "$(jq -r '.qa_evidence.required // true' "$ticket")" = "true" ]; then
|
|
246
|
+
local qa_path
|
|
247
|
+
qa_path=$(jq -r '.qa_evidence.path // empty' "$ticket")
|
|
248
|
+
if [ -z "$qa_path" ] || [ ! -e "$qa_path" ]; then
|
|
249
|
+
log_fail "$ticket_id: DONE requires qa_evidence.path pointing to an existing file"
|
|
250
|
+
errors=$((errors + 1))
|
|
251
|
+
fi
|
|
252
|
+
fi
|
|
253
|
+
done < <(find project/tickets -name '*.json' -type f -print0)
|
|
254
|
+
|
|
255
|
+
return "$errors"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main() {
|
|
259
|
+
require_git
|
|
260
|
+
|
|
261
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
262
|
+
log_fail "jq not installed. Install with: brew install jq"
|
|
263
|
+
exit 2
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
load_docs_policy
|
|
267
|
+
CHANGED_FILES="$(get_changed_files || true)"
|
|
268
|
+
|
|
269
|
+
echo ""
|
|
270
|
+
echo "========================================================"
|
|
271
|
+
echo " Validating documentation and DoD evidence"
|
|
272
|
+
echo "========================================================"
|
|
273
|
+
echo ""
|
|
274
|
+
|
|
275
|
+
if [ -z "$CHANGED_FILES" ]; then
|
|
276
|
+
log_info "No changed files detected; validating ticket evidence only."
|
|
277
|
+
else
|
|
278
|
+
log_info "Changed files:"
|
|
279
|
+
echo "$CHANGED_FILES" | sed 's/^/ - /'
|
|
280
|
+
echo ""
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
local errors=0
|
|
284
|
+
local ticket_id
|
|
285
|
+
ticket_id=$(current_ticket_id)
|
|
286
|
+
|
|
287
|
+
local code_roots_pattern api_paths_pattern migration_paths_pattern setup_paths_pattern architecture_paths_pattern
|
|
288
|
+
local documentation_paths_pattern api_doc_paths_pattern runbook_paths_pattern setup_doc_paths_pattern adr_paths_pattern
|
|
289
|
+
|
|
290
|
+
code_roots_pattern="$(policy_prefix_regex code_roots '(^src/|^lib/|^app/|^pages/|^packages/|^services/|^server/|^api/|^cmd/|^internal/|^pkg/)')"
|
|
291
|
+
api_paths_pattern="$(policy_prefix_regex api_paths '(^app/api/|^pages/api/|/routes?/|/controllers?/)')|openapi\.(yaml|yml|json)$"
|
|
292
|
+
migration_paths_pattern="$(policy_prefix_regex migration_paths '(^migrations/|/migrations/|^db/migrate/|^prisma/migrations/)')"
|
|
293
|
+
setup_paths_pattern="$(policy_file_regex setup_paths '(^package\.json$|^pnpm-lock\.yaml$|^package-lock\.json$|^yarn\.lock$|^Dockerfile$|^docker-compose|^\.env\.example$|^scripts/|^\.github/workflows/)')"
|
|
294
|
+
architecture_paths_pattern="$(policy_prefix_regex architecture_paths '(^src/(auth|cache|db|database|security)/|^lib/(auth|cache|db|database|security)/|^infra/|^terraform/|^k8s/|^prisma/schema\.prisma$)')"
|
|
295
|
+
documentation_paths_pattern="$(policy_file_regex documentation_paths '(^docs/|^README\.md$|^CHANGELOG\.md$|^RELEASE-NOTES\.md$|^openapi\.(yaml|yml|json)$|^docs/project/api/)')"
|
|
296
|
+
api_doc_paths_pattern="$(policy_file_regex api_doc_paths '(^docs/project/api/|^openapi\.(yaml|yml|json)$|^README\.md$)')"
|
|
297
|
+
runbook_paths_pattern="$(policy_file_regex runbook_paths '(^docs/runtime/runbooks/|^docs/runtime/technical/)')|migration.*\.(md|txt)$"
|
|
298
|
+
setup_doc_paths_pattern="$(policy_file_regex setup_doc_paths '(^README\.md$|^docs/runtime/technical/|^docs/runtime/runbooks/)')"
|
|
299
|
+
adr_paths_pattern="$(policy_file_regex adr_paths '^docs/runtime/adr/')"
|
|
300
|
+
|
|
301
|
+
local code_pattern="${code_roots_pattern}.*\.(ts|tsx|js|jsx|py|go|java|rb|rs|kt|swift|c|cpp|h)$"
|
|
302
|
+
local test_pattern='(\.test\.|\.spec\.|/__tests__/|^tests/)'
|
|
303
|
+
|
|
304
|
+
if has_changed_path "$CHANGED_FILES" "$code_pattern" && [ -z "$ticket_id" ]; then
|
|
305
|
+
log_fail "Code changed but no TICKET-XXX found in branch name or changed state files"
|
|
306
|
+
errors=$((errors + 1))
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
if has_changed_path "$CHANGED_FILES" "$code_pattern"; then
|
|
310
|
+
local non_test_code
|
|
311
|
+
non_test_code=$(echo "$CHANGED_FILES" | grep -E "$code_pattern" | grep -Ev "$test_pattern" || true)
|
|
312
|
+
if [ -n "$non_test_code" ] &&
|
|
313
|
+
! has_changed_path "$CHANGED_FILES" "$documentation_paths_pattern"; then
|
|
314
|
+
log_fail "Production code changed but no docs/release note path changed"
|
|
315
|
+
log_fail "Expected one of: docs/, README.md, CHANGELOG.md, RELEASE-NOTES.md, openapi.*, docs/project/api/"
|
|
316
|
+
errors=$((errors + 1))
|
|
317
|
+
fi
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
if has_changed_path "$CHANGED_FILES" "$api_paths_pattern" &&
|
|
321
|
+
! has_changed_path "$CHANGED_FILES" "$api_doc_paths_pattern"; then
|
|
322
|
+
log_fail "API route/interface changed but API docs were not updated"
|
|
323
|
+
errors=$((errors + 1))
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
if has_changed_path "$CHANGED_FILES" "$migration_paths_pattern" &&
|
|
327
|
+
! has_changed_path "$CHANGED_FILES" "$runbook_paths_pattern"; then
|
|
328
|
+
log_fail "Migration changed but no runbook or migration notes changed"
|
|
329
|
+
errors=$((errors + 1))
|
|
330
|
+
fi
|
|
331
|
+
|
|
332
|
+
if has_changed_path "$CHANGED_FILES" "$setup_paths_pattern" &&
|
|
333
|
+
! has_changed_path "$CHANGED_FILES" "$setup_doc_paths_pattern"; then
|
|
334
|
+
log_fail "Setup/workflow/dependency changed but README or technical docs were not updated"
|
|
335
|
+
errors=$((errors + 1))
|
|
336
|
+
fi
|
|
337
|
+
|
|
338
|
+
if has_changed_path "$CHANGED_FILES" "$architecture_paths_pattern" &&
|
|
339
|
+
! has_changed_path "$CHANGED_FILES" "$adr_paths_pattern"; then
|
|
340
|
+
log_fail "Architecture-sensitive area changed but no ADR changed"
|
|
341
|
+
errors=$((errors + 1))
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
if ! validate_ticket_doc_paths; then
|
|
345
|
+
errors=$((errors + 1))
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
if ! validate_done_tickets; then
|
|
349
|
+
errors=$((errors + 1))
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
echo ""
|
|
353
|
+
echo "========================================================"
|
|
354
|
+
if [ "$errors" -eq 0 ]; then
|
|
355
|
+
echo -e " ${GREEN}Documentation gates passed${NC}"
|
|
356
|
+
echo "========================================================"
|
|
357
|
+
exit 0
|
|
358
|
+
fi
|
|
359
|
+
|
|
360
|
+
echo -e " ${RED}Documentation gates failed: $errors issue group(s)${NC}"
|
|
361
|
+
echo "========================================================"
|
|
362
|
+
exit 1
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
main "$@"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/validate-permissions.sh
|
|
3
|
+
#
|
|
4
|
+
# Validates that state history commands were executed by an allowed agent role.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# bash scripts/validate-permissions.sh
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
RED='\033[0;31m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
BLUE='\033[0;34m'
|
|
14
|
+
NC='\033[0m'
|
|
15
|
+
|
|
16
|
+
log_info() { echo -e "${BLUE}i${NC} $1"; }
|
|
17
|
+
log_pass() { echo -e "${GREEN}+${NC} $1"; }
|
|
18
|
+
log_fail() { echo -e "${RED}x${NC} $1"; }
|
|
19
|
+
|
|
20
|
+
normalize_agent() {
|
|
21
|
+
printf '%s' "$1" | sed 's/-agent$//'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
command_file_for() {
|
|
25
|
+
local command="$1"
|
|
26
|
+
local name
|
|
27
|
+
name=$(printf '%s' "$command" | sed 's#^/##')
|
|
28
|
+
find core/commands -name "$name.md" -type f | head -1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
allowed_agents_for_command() {
|
|
32
|
+
local file="$1"
|
|
33
|
+
|
|
34
|
+
awk '
|
|
35
|
+
/^owner_agent:/ {
|
|
36
|
+
gsub(/"/, "", $2);
|
|
37
|
+
print $2;
|
|
38
|
+
}
|
|
39
|
+
/^requires_agents:/ {
|
|
40
|
+
in_requires=1;
|
|
41
|
+
next;
|
|
42
|
+
}
|
|
43
|
+
in_requires && /^ - / {
|
|
44
|
+
agent=$2;
|
|
45
|
+
gsub(/"/, "", agent);
|
|
46
|
+
print agent;
|
|
47
|
+
next;
|
|
48
|
+
}
|
|
49
|
+
in_requires && /^[^ ]/ {
|
|
50
|
+
in_requires=0;
|
|
51
|
+
}
|
|
52
|
+
' "$file" | sort -u
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
main() {
|
|
56
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
57
|
+
log_fail "jq not installed. Install with: brew install jq"
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo ""
|
|
62
|
+
echo "========================================================"
|
|
63
|
+
echo " Validating agent command permissions"
|
|
64
|
+
echo "========================================================"
|
|
65
|
+
echo ""
|
|
66
|
+
|
|
67
|
+
if [ ! -d "project/tickets" ]; then
|
|
68
|
+
log_info "No tickets directory; skipping"
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
local errors=0
|
|
73
|
+
|
|
74
|
+
while IFS= read -r -d '' ticket; do
|
|
75
|
+
local ticket_id
|
|
76
|
+
ticket_id=$(jq -r '.id // empty' "$ticket")
|
|
77
|
+
|
|
78
|
+
local history_count
|
|
79
|
+
history_count=$(jq '.state_history | length' "$ticket")
|
|
80
|
+
if [ "$history_count" -eq 0 ]; then
|
|
81
|
+
continue
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
for i in $(seq 0 $((history_count - 1))); do
|
|
85
|
+
local command agent role file allowed
|
|
86
|
+
command=$(jq -r ".state_history[$i].by_command // empty" "$ticket")
|
|
87
|
+
agent=$(jq -r ".state_history[$i].by_agent // empty" "$ticket")
|
|
88
|
+
role=$(normalize_agent "$agent")
|
|
89
|
+
|
|
90
|
+
[ -n "$command" ] || continue
|
|
91
|
+
|
|
92
|
+
file=$(command_file_for "$command")
|
|
93
|
+
if [ -z "$file" ]; then
|
|
94
|
+
log_fail "$ticket_id state_history[$i]: unknown command $command"
|
|
95
|
+
errors=$((errors + 1))
|
|
96
|
+
continue
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
allowed=$(allowed_agents_for_command "$file")
|
|
100
|
+
if ! printf '%s\n' "$allowed" | grep -qx "$role"; then
|
|
101
|
+
log_fail "$ticket_id state_history[$i]: $agent cannot execute $command"
|
|
102
|
+
log_fail " Allowed: $(printf '%s' "$allowed" | tr '\n' ' ')"
|
|
103
|
+
errors=$((errors + 1))
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
local from_state to_state assignee
|
|
107
|
+
from_state=$(jq -r ".state_history[$i].from_state // empty" "$ticket")
|
|
108
|
+
to_state=$(jq -r ".state_history[$i].to_state // empty" "$ticket")
|
|
109
|
+
assignee=$(jq -r ".assignee // empty" "$ticket")
|
|
110
|
+
|
|
111
|
+
if { [ "$to_state" = "DONE" ] || { [ "$from_state" = "IN_REVIEW" ] && [ "$to_state" = "QA" ]; }; } &&
|
|
112
|
+
[ -n "$assignee" ] && [ "$agent" = "$assignee" ]; then
|
|
113
|
+
log_fail "$ticket_id state_history[$i]: self-approval forbidden for $agent"
|
|
114
|
+
errors=$((errors + 1))
|
|
115
|
+
fi
|
|
116
|
+
done
|
|
117
|
+
done < <(find project/tickets -name '*.json' -type f -print0)
|
|
118
|
+
|
|
119
|
+
echo ""
|
|
120
|
+
echo "========================================================"
|
|
121
|
+
if [ "$errors" -eq 0 ]; then
|
|
122
|
+
echo -e " ${GREEN}Permission gates passed${NC}"
|
|
123
|
+
echo "========================================================"
|
|
124
|
+
exit 0
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
echo -e " ${RED}Permission gates failed: $errors issue(s)${NC}"
|
|
128
|
+
echo "========================================================"
|
|
129
|
+
exit 1
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main "$@"
|