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.
Files changed (103) hide show
  1. package/.claude-plugin/plugin.json +21 -0
  2. package/.codex-plugin/plugin.json +35 -0
  3. package/.cursor-plugin/plugin.json +22 -0
  4. package/README.md +173 -0
  5. package/bin/ai-core-framework.js +110 -0
  6. package/core/README.md +162 -0
  7. package/core/agents/README.md +32 -0
  8. package/core/agents/business-analyst.md +269 -0
  9. package/core/agents/developer.md +375 -0
  10. package/core/agents/qa-tester.md +477 -0
  11. package/core/agents/scrum-master.md +136 -0
  12. package/core/agents/tech-lead.md +345 -0
  13. package/core/config/backlog.schema.json +38 -0
  14. package/core/config/docs-policy.default.json +37 -0
  15. package/core/config/release.schema.json +120 -0
  16. package/core/config/ticket.schema.json +253 -0
  17. package/core/rules/00-global-rules.md +373 -0
  18. package/core/rules/01-git-workflow.md +388 -0
  19. package/core/rules/02-code-quality.md +77 -0
  20. package/core/rules/03-security.md +78 -0
  21. package/core/rules/04-documentation.md +72 -0
  22. package/core/rules/05-testing-mandatory.md +374 -0
  23. package/core/rules/06-approval-gates.md +388 -0
  24. package/core/rules/07-definition-of-ready.md +112 -0
  25. package/core/rules/08-definition-of-done.md +149 -0
  26. package/core/scripts/ai-core.sh +456 -0
  27. package/core/scripts/generate-views.sh +210 -0
  28. package/core/scripts/install-codex-prompts.sh +127 -0
  29. package/core/scripts/log-user-request.sh +113 -0
  30. package/core/scripts/setup-project.sh +183 -0
  31. package/core/scripts/sync-platforms.sh +322 -0
  32. package/core/scripts/validate-audit-log.sh +73 -0
  33. package/core/scripts/validate-docs.sh +365 -0
  34. package/core/scripts/validate-permissions.sh +132 -0
  35. package/core/scripts/validate-state.sh +611 -0
  36. package/core/scripts/workflow.sh +513 -0
  37. package/core/skills/README.md +21 -0
  38. package/core/skills/ai-core-commands/SKILL.md +86 -0
  39. package/core/skills/brainstorming/SKILL.md +40 -0
  40. package/core/skills/development-implement-task/SKILL.md +308 -0
  41. package/core/skills/executing-ticket/SKILL.md +28 -0
  42. package/core/skills/git-branch-status/SKILL.md +56 -0
  43. package/core/skills/git-cleanup-branches/SKILL.md +57 -0
  44. package/core/skills/git-scan-untracked/SKILL.md +50 -0
  45. package/core/skills/meta-generate-views/SKILL.md +54 -0
  46. package/core/skills/meta-request-log/SKILL.md +61 -0
  47. package/core/skills/meta-sprint-report/SKILL.md +59 -0
  48. package/core/skills/meta-sync-platforms/SKILL.md +53 -0
  49. package/core/skills/meta-ticket-health/SKILL.md +61 -0
  50. package/core/skills/meta-validate-audit-log/SKILL.md +42 -0
  51. package/core/skills/meta-validate-docs/SKILL.md +58 -0
  52. package/core/skills/meta-validate-permissions/SKILL.md +53 -0
  53. package/core/skills/meta-validate-state/SKILL.md +58 -0
  54. package/core/skills/planning-analyze-requirements/SKILL.md +471 -0
  55. package/core/skills/planning-backlog-status/SKILL.md +57 -0
  56. package/core/skills/planning-document-existing-requirements/SKILL.md +246 -0
  57. package/core/skills/planning-estimate-task/SKILL.md +60 -0
  58. package/core/skills/planning-groom-ticket/SKILL.md +442 -0
  59. package/core/skills/planning-mark-ready/SKILL.md +111 -0
  60. package/core/skills/planning-plan-refactor/SKILL.md +66 -0
  61. package/core/skills/planning-plan-sprint/SKILL.md +112 -0
  62. package/core/skills/planning-prioritize-backlog/SKILL.md +62 -0
  63. package/core/skills/planning-write-plan/SKILL.md +68 -0
  64. package/core/skills/project-detect-stack/SKILL.md +71 -0
  65. package/core/skills/project-discover-codebase/SKILL.md +74 -0
  66. package/core/skills/project-setup-project/SKILL.md +113 -0
  67. package/core/skills/qa-bug-status/SKILL.md +52 -0
  68. package/core/skills/qa-report-bug/SKILL.md +518 -0
  69. package/core/skills/qa-smoke-test/SKILL.md +387 -0
  70. package/core/skills/qa-triage-bug/SKILL.md +62 -0
  71. package/core/skills/qa-verify-fix/SKILL.md +446 -0
  72. package/core/skills/release-hotfix/SKILL.md +117 -0
  73. package/core/skills/release-release/SKILL.md +123 -0
  74. package/core/skills/release-rollback/SKILL.md +62 -0
  75. package/core/skills/review-create-pr/SKILL.md +418 -0
  76. package/core/skills/review-merge-pr/SKILL.md +425 -0
  77. package/core/skills/review-techlead-review/SKILL.md +547 -0
  78. package/core/skills/using-ai-core/SKILL.md +72 -0
  79. package/core/skills/verification-before-done/SKILL.md +35 -0
  80. package/core/skills/writing-implementation-plan/SKILL.md +45 -0
  81. package/core/templates/ci/ai-core-governance.yml +112 -0
  82. package/core/templates/ci/node-pnpm.yml +35 -0
  83. package/core/templates/pm/retrospective-template.md +47 -0
  84. package/core/templates/pm/sprint-plan-template.md +45 -0
  85. package/core/templates/pr/pull-request-template.md +247 -0
  86. package/core/templates/project/CODEOWNERS +11 -0
  87. package/core/templates/project/docs-policy.json +3 -0
  88. package/core/templates/project/project-config.yaml +137 -0
  89. package/core/templates/project/project-structure.yaml +76 -0
  90. package/core/templates/qa/bug-report-template.md +371 -0
  91. package/core/templates/qa/test-plan-template.md +57 -0
  92. package/core/templates/release/release-record-template.json +67 -0
  93. package/core/templates/requirements/PRD-template.md +58 -0
  94. package/core/templates/requirements/user-story-template.md +381 -0
  95. package/core/templates/technical/ADR-template.md +46 -0
  96. package/core/templates/technical/refactor-plan-template.md +84 -0
  97. package/core/templates/technical/tech-design-template.md +71 -0
  98. package/core/workflows/bug-lifecycle.md +56 -0
  99. package/core/workflows/feature-lifecycle.md +347 -0
  100. package/core/workflows/hotfix-lifecycle.md +65 -0
  101. package/core/workflows/sprint-lifecycle.md +56 -0
  102. package/lib/install-codex.js +85 -0
  103. 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 "$@"