ganbatte-os 0.2.1

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 (200) hide show
  1. package/AGENTS.md +46 -0
  2. package/CLAUDE.md +31 -0
  3. package/GEMINI.md +17 -0
  4. package/LICENSE +21 -0
  5. package/README.md +141 -0
  6. package/agents/profiles/architect.md +13 -0
  7. package/agents/profiles/dev.md +13 -0
  8. package/agents/profiles/devops.md +12 -0
  9. package/agents/profiles/ganbatte-os-master.md +444 -0
  10. package/agents/profiles/index.json +14 -0
  11. package/agents/profiles/po.md +7 -0
  12. package/agents/profiles/qa.md +456 -0
  13. package/agents/profiles/sm.md +7 -0
  14. package/agents/profiles/squad-creator.md +7 -0
  15. package/agents/profiles/ux-design-expert.md +14 -0
  16. package/config.json +15 -0
  17. package/docs/curation.md +60 -0
  18. package/docs/gos_installation_guide.md +114 -0
  19. package/docs/ide-compatibility.md +20 -0
  20. package/docs/plan/plan-git-operations.md +51 -0
  21. package/docs/plan-distribuicao-publica.md +360 -0
  22. package/docs/stacks/stack-git-operations.md +154 -0
  23. package/docs/toolchain-map.md +18 -0
  24. package/integrations/README.md +42 -0
  25. package/integrations/antigravity/README.md +29 -0
  26. package/integrations/antigravity/command-map.json +29 -0
  27. package/integrations/claude/README.md +35 -0
  28. package/integrations/claude/agent-map.json +46 -0
  29. package/integrations/claude/command-map.json +32 -0
  30. package/integrations/claude/litellm-proxy.md +93 -0
  31. package/integrations/claude/mcp-specifics.md +121 -0
  32. package/integrations/codex/README.md +29 -0
  33. package/integrations/codex/command-map.json +29 -0
  34. package/integrations/cursor/README.md +8 -0
  35. package/integrations/cursor/command-map.json +11 -0
  36. package/integrations/gemini/README.md +13 -0
  37. package/integrations/gemini/command-map.json +11 -0
  38. package/integrations/kilo-code/README.md +7 -0
  39. package/integrations/kilo-code/command-map.json +11 -0
  40. package/integrations/opencode/README.md +91 -0
  41. package/integrations/opencode/command-map.json +46 -0
  42. package/integrations/registry.json +20 -0
  43. package/manifests/g-os-runtime-manifest.json +39 -0
  44. package/manifests/gos-install-manifest.json +46 -0
  45. package/opencode.json +7 -0
  46. package/package.json +51 -0
  47. package/playbooks/feature-development-playbook.md +239 -0
  48. package/playbooks/sprint-planner-playbook.md +127 -0
  49. package/playbooks/squad-pipeline-runner.md +196 -0
  50. package/playbooks/ssh-multi-account-setup.md +185 -0
  51. package/prompts/01-search.md +18 -0
  52. package/prompts/02-spec.md +19 -0
  53. package/prompts/03-tasks.md +15 -0
  54. package/prompts/04-code.md +10 -0
  55. package/prompts/05-reviews.md +11 -0
  56. package/rules/plan-mode.md +60 -0
  57. package/scripts/cli/gos-cli.js +679 -0
  58. package/scripts/hooks/pre-commit-validate.js +201 -0
  59. package/scripts/integrations/check-ide-compat.js +44 -0
  60. package/scripts/integrations/setup-ide-adapters.js +87 -0
  61. package/scripts/tools/clickup-preprocess.js +218 -0
  62. package/scripts/tools/clickup.js +1058 -0
  63. package/skills/agent-teams/SKILL.md +78 -0
  64. package/skills/agent-teams/presets/team-all.yaml +14 -0
  65. package/skills/agent-teams/presets/team-fullstack.yaml +17 -0
  66. package/skills/agent-teams/presets/team-ide-minimal.yaml +9 -0
  67. package/skills/agent-teams/presets/team-no-ui.yaml +12 -0
  68. package/skills/agent-teams/presets/team-qa-focused.yaml +83 -0
  69. package/skills/clickup/SKILL.md +151 -0
  70. package/skills/component-dedup/SKILL.md +101 -0
  71. package/skills/design-to-code/SKILL.md +255 -0
  72. package/skills/figma-implement-design/SKILL.md +227 -0
  73. package/skills/figma-make-analyzer/SKILL.md +140 -0
  74. package/skills/frontend-dev/SKILL.md +271 -0
  75. package/skills/git-ssh-setup/SKILL.md +164 -0
  76. package/skills/interface-design/SKILL.md +350 -0
  77. package/skills/interface-design/references/audit.md +76 -0
  78. package/skills/interface-design/references/craft-examples.md +134 -0
  79. package/skills/interface-design/references/critique.md +92 -0
  80. package/skills/interface-design/references/extract.md +92 -0
  81. package/skills/interface-design/references/principles.md +348 -0
  82. package/skills/interface-design/references/templates/system-precision.md +73 -0
  83. package/skills/interface-design/references/templates/system-warmth.md +67 -0
  84. package/skills/interface-design/references/validation.md +137 -0
  85. package/skills/make-code-triage/SKILL.md +135 -0
  86. package/skills/make-version-diff/SKILL.md +87 -0
  87. package/skills/plan-to-tasks/SKILL.md +136 -0
  88. package/skills/react-best-practices/AGENTS.md +2975 -0
  89. package/skills/react-best-practices/SKILL.md +151 -0
  90. package/skills/react-best-practices/metadata.json +15 -0
  91. package/skills/react-best-practices/rules/_sections.md +46 -0
  92. package/skills/react-best-practices/rules/_template.md +28 -0
  93. package/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  94. package/skills/react-best-practices/rules/advanced-init-once.md +42 -0
  95. package/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
  96. package/skills/react-best-practices/rules/async-api-routes.md +38 -0
  97. package/skills/react-best-practices/rules/async-defer-await.md +80 -0
  98. package/skills/react-best-practices/rules/async-dependencies.md +51 -0
  99. package/skills/react-best-practices/rules/async-parallel.md +28 -0
  100. package/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
  101. package/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
  102. package/skills/react-best-practices/rules/bundle-conditional.md +31 -0
  103. package/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
  104. package/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  105. package/skills/react-best-practices/rules/bundle-preload.md +50 -0
  106. package/skills/react-best-practices/rules/client-event-listeners.md +74 -0
  107. package/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
  108. package/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
  109. package/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
  110. package/skills/react-best-practices/rules/js-batch-dom-css.md +107 -0
  111. package/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
  112. package/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
  113. package/skills/react-best-practices/rules/js-cache-storage.md +70 -0
  114. package/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
  115. package/skills/react-best-practices/rules/js-early-exit.md +50 -0
  116. package/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
  117. package/skills/react-best-practices/rules/js-index-maps.md +37 -0
  118. package/skills/react-best-practices/rules/js-length-check-first.md +49 -0
  119. package/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
  120. package/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
  121. package/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
  122. package/skills/react-best-practices/rules/rendering-activity.md +26 -0
  123. package/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  124. package/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
  125. package/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
  126. package/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  127. package/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  128. package/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  129. package/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
  130. package/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  131. package/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
  132. package/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
  133. package/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  134. package/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
  135. package/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
  136. package/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  137. package/skills/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  138. package/skills/react-best-practices/rules/rerender-memo.md +44 -0
  139. package/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  140. package/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  141. package/skills/react-best-practices/rules/rerender-transitions.md +40 -0
  142. package/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  143. package/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
  144. package/skills/react-best-practices/rules/server-auth-actions.md +96 -0
  145. package/skills/react-best-practices/rules/server-cache-lru.md +41 -0
  146. package/skills/react-best-practices/rules/server-cache-react.md +76 -0
  147. package/skills/react-best-practices/rules/server-dedup-props.md +65 -0
  148. package/skills/react-best-practices/rules/server-hoist-static-io.md +142 -0
  149. package/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
  150. package/skills/react-best-practices/rules/server-serialization.md +38 -0
  151. package/skills/react-doctor/SKILL.md +74 -0
  152. package/skills/registry.json +21 -0
  153. package/skills/sprint-planner/SKILL.md +434 -0
  154. package/squads/design-delivery/README.md +10 -0
  155. package/squads/design-delivery/squad.yaml +30 -0
  156. package/squads/design-delivery/workflows/wf-design-delivery.yaml +27 -0
  157. package/squads/design-squad/README.md +31 -0
  158. package/squads/design-squad/agents/brad-frost.md +185 -0
  159. package/squads/design-squad/agents/dan-mall.md +178 -0
  160. package/squads/design-squad/agents/dave-malouf.md +198 -0
  161. package/squads/design-squad/agents/design-chief.md +109 -0
  162. package/squads/design-squad/agents/design-system-architect.md +109 -0
  163. package/squads/design-squad/agents/ui-engineer.md +102 -0
  164. package/squads/design-squad/agents/ux-designer.md +105 -0
  165. package/squads/design-squad/agents/visual-generator.md +108 -0
  166. package/squads/design-squad/checklists/output-quality.md +76 -0
  167. package/squads/design-squad/config/config.yaml +65 -0
  168. package/squads/design-squad/data/design-patterns-catalog.yaml +276 -0
  169. package/squads/design-squad/data/routing-catalog.yaml +95 -0
  170. package/squads/design-squad/squad.yaml +88 -0
  171. package/squads/design-squad/tasks/audit-design.md +174 -0
  172. package/squads/design-squad/tasks/create-component-spec.md +185 -0
  173. package/squads/design-squad/tasks/create-design-system.md +179 -0
  174. package/squads/design-squad/tasks/design-ux-flow.md +184 -0
  175. package/squads/design-squad/tasks/diagnose.md +138 -0
  176. package/squads/design-squad/tasks/generate-handoff.md +186 -0
  177. package/squads/design-squad/tasks/review.md +133 -0
  178. package/squads/design-squad/tasks/setup-design-ops.md +177 -0
  179. package/squads/design-squad/workflows/wf-design-system-creation.yaml +131 -0
  180. package/squads/design-squad/workflows/wf-feature-design.yaml +114 -0
  181. package/squads/git-operations/README.md +30 -0
  182. package/squads/git-operations/squad.yaml +27 -0
  183. package/squads/git-operations/workflows/wf-safe-commit.yaml +27 -0
  184. package/squads/git-operations/workflows/wf-ssh-setup.yaml +27 -0
  185. package/squads/sprint-planning/agents/sprint-chief.md +47 -0
  186. package/squads/sprint-planning/agents/sprint-planner-agent.md +43 -0
  187. package/squads/sprint-planning/agents/sprint-tracker.md +43 -0
  188. package/squads/sprint-planning/agents/task-importer.md +44 -0
  189. package/squads/sprint-planning/checklists/sprint-readiness.md +27 -0
  190. package/squads/sprint-planning/config/config.yaml +65 -0
  191. package/squads/sprint-planning/data/clickup-field-mapping.yaml +94 -0
  192. package/squads/sprint-planning/squad.yaml +52 -0
  193. package/squads/sprint-planning/tasks/close-sprint.md +43 -0
  194. package/squads/sprint-planning/tasks/create-sprint.md +42 -0
  195. package/squads/sprint-planning/tasks/import-tasks.md +39 -0
  196. package/squads/sprint-planning/tasks/sync-status.md +31 -0
  197. package/squads/sprint-planning/workflows/wf-sprint-creation.yaml +59 -0
  198. package/squads/sprint-planning/workflows/wf-sprint-sync.yaml +35 -0
  199. package/templates/adr-tmpl.yaml +76 -0
  200. package/templates/sprint-clickup.template.md +80 -0
@@ -0,0 +1,1058 @@
1
+ #!/usr/bin/env node
2
+
3
+ // clickup.js — ClickUp API v2 CLI tool (zero-dep)
4
+ // Usage: node clickup.js <resource> <action> [--options]
5
+ // Auth: CLICKUP_API_KEY env var (personal token pk_*)
6
+
7
+ const API_KEY = process.env.CLICKUP_API_KEY
8
+ const BASE_URL = 'https://api.clickup.com/api/v2'
9
+
10
+ if (!API_KEY) {
11
+ console.error(JSON.stringify({ error: 'CLICKUP_API_KEY environment variable required' }))
12
+ process.exit(1)
13
+ }
14
+
15
+ function parseArgs(argv) {
16
+ const result = { _: [] }
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const arg = argv[i]
19
+ if (arg.startsWith('--')) {
20
+ const key = arg.slice(2)
21
+ const next = argv[i + 1]
22
+ if (next && !next.startsWith('--')) {
23
+ result[key] = next
24
+ i++
25
+ } else {
26
+ result[key] = true
27
+ }
28
+ } else {
29
+ result._.push(arg)
30
+ }
31
+ }
32
+ return result
33
+ }
34
+
35
+ const args = parseArgs(process.argv.slice(2))
36
+ const [cmd, sub, ...rest] = args._
37
+
38
+ let rateLimitRemaining = 100
39
+
40
+ async function api(method, path, body) {
41
+ if (args['dry-run']) {
42
+ return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined }
43
+ }
44
+
45
+ const maxRetries = 3
46
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
47
+ if (rateLimitRemaining <= 2 && attempt === 0) {
48
+ await new Promise(r => setTimeout(r, 1000))
49
+ }
50
+
51
+ const res = await fetch(`${BASE_URL}${path}`, {
52
+ method,
53
+ headers: {
54
+ 'Authorization': API_KEY,
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body: body ? JSON.stringify(body) : undefined,
58
+ })
59
+
60
+ const remaining = res.headers.get('x-ratelimit-remaining')
61
+ if (remaining) rateLimitRemaining = parseInt(remaining, 10)
62
+
63
+ if (res.status === 429 && attempt < maxRetries) {
64
+ const reset = res.headers.get('x-ratelimit-reset')
65
+ const waitMs = reset ? Math.max(0, parseInt(reset, 10) * 1000 - Date.now()) : (attempt + 1) * 2000
66
+ await new Promise(r => setTimeout(r, Math.min(waitMs, 10000)))
67
+ continue
68
+ }
69
+
70
+ const text = await res.text()
71
+ try {
72
+ return JSON.parse(text)
73
+ } catch {
74
+ return { status: res.status, body: text }
75
+ }
76
+ }
77
+ }
78
+
79
+ async function apiWithDelay(method, path, body, delayMs = 200) {
80
+ const result = await api(method, path, body)
81
+ if (!args['dry-run']) await new Promise(r => setTimeout(r, delayMs))
82
+ return result
83
+ }
84
+
85
+ // --- Text Quality: Sanitize + AI Pattern Detection ---
86
+
87
+ const ACCENT_FIXES = {
88
+ 'nao': 'não', 'entao': 'então', 'tambem': 'também', 'codigo': 'código',
89
+ 'pagina': 'página', 'unico': 'único', 'analise': 'análise', 'modulo': 'módulo',
90
+ 'numero': 'número', 'especifico': 'específico', 'diretorio': 'diretório',
91
+ 'padrao': 'padrão', 'configuracao': 'configuração', 'validacao': 'validação',
92
+ 'implementacao': 'implementação', 'descricao': 'descrição', 'opcao': 'opção',
93
+ 'sessao': 'sessão', 'secao': 'seção', 'funcao': 'função', 'acao': 'ação',
94
+ 'informacao': 'informação', 'versao': 'versão', 'conexao': 'conexão',
95
+ 'excecao': 'exceção', 'condicao': 'condição', 'operacao': 'operação',
96
+ 'autenticacao': 'autenticação', 'migracao': 'migração', 'integracao': 'integração',
97
+ }
98
+
99
+ const AI_PATTERNS = [
100
+ /\bvale ressaltar\b/gi, /\bé importante destacar\b/gi,
101
+ /\bnesse sentido\b/gi, /\bdiante disso\b/gi,
102
+ /\bem suma\b/gi, /\bpor fim\b/gi,
103
+ /\brobusto\b/gi, /\babrangente\b/gi,
104
+ /\binovador\b/gi, /\bestratégico\b/gi,
105
+ /\bholístic[oa]\b/gi, /\balém disso\b/gi,
106
+ /\badicionalmente\b/gi, /\bpor outro lado\b/gi,
107
+ ]
108
+
109
+ function fixAccents(text) {
110
+ if (!text) return text
111
+ for (const [wrong, right] of Object.entries(ACCENT_FIXES)) {
112
+ text = text.replace(new RegExp(`\\b${wrong}\\b`, 'gi'), (match) => {
113
+ // Preserve capitalization: Nao → Não, NAO → NÃO, nao → não
114
+ if (match === match.toUpperCase()) return right.toUpperCase()
115
+ if (match[0] === match[0].toUpperCase()) return right[0].toUpperCase() + right.slice(1)
116
+ return right
117
+ })
118
+ }
119
+ return text
120
+ }
121
+
122
+ function countAiPatterns(text) {
123
+ if (!text) return 0
124
+ let count = 0
125
+ for (const p of AI_PATTERNS) {
126
+ const m = text.match(p)
127
+ if (m) count += m.length
128
+ }
129
+ return count
130
+ }
131
+
132
+ function sanitizeTaskTexts(task) {
133
+ const textFields = ['description', 'context', 'technicalNotes', 'dod']
134
+ const arrayFields = ['acceptanceCriteria', 'steps']
135
+ let aiPatternCount = 0
136
+
137
+ for (const f of textFields) {
138
+ if (task[f]) {
139
+ task[f] = fixAccents(task[f])
140
+ aiPatternCount += countAiPatterns(task[f])
141
+ }
142
+ }
143
+ for (const f of arrayFields) {
144
+ if (task[f]?.length) {
145
+ task[f] = task[f].map(item => {
146
+ item = fixAccents(item)
147
+ aiPatternCount += countAiPatterns(item)
148
+ return item
149
+ })
150
+ }
151
+ }
152
+ return { task, aiPatternCount }
153
+ }
154
+
155
+ // --- Rich Description Builder ---
156
+
157
+ function buildTaskDescription(task) {
158
+ const sections = []
159
+
160
+ if (task.description) {
161
+ sections.push(`## O que\n${task.description}`)
162
+ }
163
+ if (task.context) {
164
+ sections.push(`## Contexto\n${task.context}`)
165
+ }
166
+ if (task.steps?.length) {
167
+ sections.push(`## Como fazer\n${task.steps.map((s, i) => `${i + 1}. ${s}`).join('\n')}`)
168
+ }
169
+ if (task.acceptanceCriteria?.length) {
170
+ sections.push(`## Critérios de Aceite\n${task.acceptanceCriteria.map(a => `- ${a}`).join('\n')}`)
171
+ }
172
+ if (task.businessRules?.length) {
173
+ sections.push(`## Regras de Negócio\n${task.businessRules.map(r => `- ${r}`).join('\n')}`)
174
+ }
175
+ if (task.dependencies?.length) {
176
+ sections.push(`## Dependências\n${task.dependencies.map(d => `- ${d}`).join('\n')}`)
177
+ }
178
+ if (task.files?.length) {
179
+ sections.push(`## Arquivos\n${task.files.map(f => `- ${f}`).join('\n')}`)
180
+ }
181
+ if (task.ref) {
182
+ sections.push(`## Referência\n${task.ref}`)
183
+ }
184
+ if (task.technicalNotes) {
185
+ sections.push(`## Notas Técnicas\n${task.technicalNotes}`)
186
+ }
187
+ if (task.dod) {
188
+ sections.push(`## Definition of Done\n${task.dod}`)
189
+ }
190
+
191
+ return sections.join('\n\n---\n\n')
192
+ }
193
+
194
+ // --- Task Completeness Validation ---
195
+
196
+ function validateTaskCompleteness(task) {
197
+ let score = 0
198
+ const suggestions = []
199
+
200
+ // description (20 pts)
201
+ if (task.description && task.description.length > 50) {
202
+ score += 20
203
+ } else if (task.description) {
204
+ score += 5
205
+ suggestions.push(`Descrição muito curta (${task.description.length} chars). Expandir com contexto e motivação.`)
206
+ } else {
207
+ suggestions.push('Sem descrição. Adicionar O QUE fazer e POR QUE.')
208
+ }
209
+
210
+ // acceptanceCriteria (25 pts)
211
+ if (task.acceptanceCriteria?.length >= 2 && task.acceptanceCriteria.every(a => a.length > 20)) {
212
+ score += 25
213
+ if (task.acceptanceCriteria.some(a => /\b(dado|quando|então|given|when|then)\b/i.test(a))) {
214
+ // bonus: using Given/When/Then
215
+ } else {
216
+ suggestions.push('ACs sem formato DADO/QUANDO/ENTÃO. Reescrever para verificabilidade.')
217
+ }
218
+ } else if (task.acceptanceCriteria?.length) {
219
+ score += 10
220
+ suggestions.push(`ACs insuficientes (${task.acceptanceCriteria.length} item(ns), min 2 com >20 chars).`)
221
+ } else {
222
+ suggestions.push('Sem critérios de aceite. Adicionar mínimo 2 ACs verificáveis.')
223
+ }
224
+
225
+ // steps or subtasks (15 pts)
226
+ if (task.steps?.length || task.subtasks?.length) {
227
+ score += 15
228
+ } else {
229
+ suggestions.push('Sem steps de implementação. Adicionar passo a passo (3-7 steps).')
230
+ }
231
+
232
+ // files (10 pts)
233
+ if (task.files?.length) {
234
+ score += 10
235
+ if (task.files.some(f => f.endsWith('/'))) {
236
+ suggestions.push('Files contém diretórios genéricos. Preferir paths específicos (ex: app/page.tsx).')
237
+ }
238
+ } else {
239
+ suggestions.push('Sem arquivos listados. Adicionar paths dos arquivos envolvidos.')
240
+ }
241
+
242
+ // points (10 pts)
243
+ if (task.points && task.points >= 1 && task.points <= 13) {
244
+ score += 10
245
+ } else {
246
+ suggestions.push('Sem story points. Adicionar estimativa (1, 2, 3, 5, 8, 13).')
247
+ }
248
+
249
+ // dependencies (5 pts)
250
+ if (task.dependencies?.length >= 0) {
251
+ score += 5 // present even if empty means it was considered
252
+ }
253
+
254
+ // context or businessRules (10 pts)
255
+ if (task.context || task.businessRules?.length) {
256
+ score += 10
257
+ } else {
258
+ suggestions.push('Sem contexto de negócio. Adicionar motivação para leitor não-técnico.')
259
+ }
260
+
261
+ // dod or AC with Given/When/Then (5 pts)
262
+ if (task.dod || task.acceptanceCriteria?.some(a => /\b(dado|quando|então|given|when|then)\b/i.test(a))) {
263
+ score += 5
264
+ } else {
265
+ suggestions.push('Sem Definition of Done explícito.')
266
+ }
267
+
268
+ return { score, suggestions }
269
+ }
270
+
271
+ function validateAllTasks(tasks, skipValidation) {
272
+ if (skipValidation) return true
273
+ let hasErrors = false
274
+ for (const task of tasks) {
275
+ const { score, suggestions } = validateTaskCompleteness(task)
276
+ if (score < 30) {
277
+ console.error(`❌ ${task.id} "${task.title}" (score: ${score}/100) — requer enriquecimento`)
278
+ suggestions.forEach(s => console.error(` - ${s}`))
279
+ hasErrors = true
280
+ } else if (score < 50) {
281
+ console.warn(`⚠ ${task.id} "${task.title}" (score: ${score}/100) — detalhamento insuficiente`)
282
+ suggestions.forEach(s => console.warn(` - ${s}`))
283
+ } else if (suggestions.length > 0) {
284
+ console.warn(`ℹ ${task.id} "${task.title}" (score: ${score}/100)`)
285
+ suggestions.forEach(s => console.warn(` - ${s}`))
286
+ }
287
+ }
288
+ return !hasErrors
289
+ }
290
+
291
+ // --- Sprint Abstraction Layer ---
292
+
293
+ function sprintFolderName(id, name, start, end) {
294
+ return `Sprint ${id} — ${name} (${start} to ${end})`
295
+ }
296
+
297
+ function parseSprintMeta(description) {
298
+ try {
299
+ const match = description?.match(/^---\n([\s\S]*?)\n---/)
300
+ if (!match) return {}
301
+ const lines = match[1].split('\n')
302
+ const meta = {}
303
+ for (const line of lines) {
304
+ const [k, ...v] = line.split(':')
305
+ if (k) meta[k.trim()] = v.join(':').trim()
306
+ }
307
+ return meta
308
+ } catch { return {} }
309
+ }
310
+
311
+ function buildSprintDescription(meta) {
312
+ const lines = Object.entries(meta).map(([k, v]) => `${k}: ${v}`)
313
+ return `---\n${lines.join('\n')}\n---`
314
+ }
315
+
316
+ // --- Main ---
317
+
318
+ async function main() {
319
+ let result
320
+
321
+ switch (cmd) {
322
+ // === WORKSPACE ===
323
+ case 'workspace':
324
+ switch (sub) {
325
+ case 'list':
326
+ result = await api('GET', '/team')
327
+ break
328
+ case 'members': {
329
+ if (!args['team-id']) { result = { error: '--team-id required' }; break }
330
+ const teamRes = await api('GET', `/team/${args['team-id']}`)
331
+ const members = (teamRes.team?.members || []).map(m => ({
332
+ id: m.user?.id,
333
+ username: m.user?.username,
334
+ email: m.user?.email,
335
+ initials: m.user?.initials,
336
+ role: m.role,
337
+ roleName: m.role === 1 ? 'owner' : m.role === 2 ? 'admin' : m.role === 3 ? 'member' : 'guest',
338
+ }))
339
+ result = { teamId: args['team-id'], members, count: members.length }
340
+ break
341
+ }
342
+ default:
343
+ result = { error: 'Unknown workspace subcommand. Use: list, members' }
344
+ }
345
+ break
346
+
347
+ // === SPACE ===
348
+ case 'space':
349
+ switch (sub) {
350
+ case 'list': {
351
+ if (!args['team-id']) { result = { error: '--team-id required' }; break }
352
+ result = await api('GET', `/team/${args['team-id']}/space?archived=false`)
353
+ break
354
+ }
355
+ case 'get': {
356
+ if (!rest[0]) { result = { error: 'Space ID required' }; break }
357
+ result = await api('GET', `/space/${rest[0]}`)
358
+ break
359
+ }
360
+ default:
361
+ result = { error: 'Unknown space subcommand. Use: list, get' }
362
+ }
363
+ break
364
+
365
+ // === FOLDER ===
366
+ case 'folder':
367
+ switch (sub) {
368
+ case 'list': {
369
+ if (!args['space-id']) { result = { error: '--space-id required' }; break }
370
+ result = await api('GET', `/space/${args['space-id']}/folder?archived=false`)
371
+ break
372
+ }
373
+ case 'get': {
374
+ if (!rest[0]) { result = { error: 'Folder ID required' }; break }
375
+ result = await api('GET', `/folder/${rest[0]}`)
376
+ break
377
+ }
378
+ case 'create': {
379
+ if (!args['space-id'] || !args.name) { result = { error: '--space-id and --name required' }; break }
380
+ result = await api('POST', `/space/${args['space-id']}/folder`, { name: args.name })
381
+ break
382
+ }
383
+ default:
384
+ result = { error: 'Unknown folder subcommand. Use: list, get, create' }
385
+ }
386
+ break
387
+
388
+ // === LIST ===
389
+ case 'list':
390
+ switch (sub) {
391
+ case 'list': {
392
+ if (!args['folder-id']) { result = { error: '--folder-id required' }; break }
393
+ result = await api('GET', `/folder/${args['folder-id']}/list?archived=false`)
394
+ break
395
+ }
396
+ case 'create': {
397
+ if (!args['folder-id'] || !args.name) { result = { error: '--folder-id and --name required' }; break }
398
+ const body = { name: args.name }
399
+ if (args.content) body.content = args.content
400
+ if (args['due-date']) body.due_date = new Date(args['due-date']).getTime()
401
+ result = await api('POST', `/folder/${args['folder-id']}/list`, body)
402
+ break
403
+ }
404
+ default:
405
+ result = { error: 'Unknown list subcommand. Use: list, create' }
406
+ }
407
+ break
408
+
409
+ // === TASK ===
410
+ case 'task':
411
+ switch (sub) {
412
+ case 'list': {
413
+ if (!args['list-id']) { result = { error: '--list-id required' }; break }
414
+ const params = new URLSearchParams()
415
+ if (args.page) params.set('page', args.page)
416
+ if (args.subtasks) params.set('subtasks', 'true')
417
+ if (args.statuses) args.statuses.split(',').forEach(s => params.append('statuses[]', s))
418
+ result = await api('GET', `/list/${args['list-id']}/task?${params}`)
419
+ break
420
+ }
421
+ case 'get': {
422
+ if (!rest[0]) { result = { error: 'Task ID required' }; break }
423
+ const params = new URLSearchParams()
424
+ if (args.subtasks) params.set('include_subtasks', 'true')
425
+ result = await api('GET', `/task/${rest[0]}?${params}`)
426
+ break
427
+ }
428
+ case 'create': {
429
+ if (!args['list-id'] || !args.name) { result = { error: '--list-id and --name required' }; break }
430
+ const body = { name: args.name }
431
+ if (args.description) body.description = args.description
432
+ if (args.status) body.status = args.status
433
+ if (args.priority) body.priority = parseInt(args.priority, 10)
434
+ if (args['due-date']) body.due_date = new Date(args['due-date']).getTime()
435
+ if (args['start-date']) body.start_date = new Date(args['start-date']).getTime()
436
+ if (args['time-estimate']) body.time_estimate = parseInt(args['time-estimate'], 10)
437
+ if (args.parent) body.parent = args.parent
438
+ if (args.tags) body.tags = args.tags.split(',')
439
+ if (args.points) body.points = parseFloat(args.points)
440
+ if (args.assignees) body.assignees = args.assignees.split(',').map(Number)
441
+ result = await api('POST', `/list/${args['list-id']}/task`, body)
442
+ break
443
+ }
444
+ case 'update': {
445
+ if (!args['task-id']) { result = { error: '--task-id required' }; break }
446
+ const body = {}
447
+ if (args.name) body.name = args.name
448
+ if (args.description) body.description = args.description
449
+ if (args.status) body.status = args.status
450
+ if (args.priority) body.priority = parseInt(args.priority, 10)
451
+ if (args['due-date']) body.due_date = new Date(args['due-date']).getTime()
452
+ if (args.parent) body.parent = args.parent
453
+ if (args.points) body.points = parseFloat(args.points)
454
+ if (args.assignees) body.assignees = args.assignees.split(',').map(Number)
455
+ result = await api('PUT', `/task/${args['task-id']}`, body)
456
+ break
457
+ }
458
+ case 'delete': {
459
+ if (!rest[0]) { result = { error: 'Task ID required' }; break }
460
+ result = await api('DELETE', `/task/${rest[0]}`)
461
+ break
462
+ }
463
+ case 'enrich': {
464
+ // Enrich existing ClickUp tasks with rich descriptions from a JSON file
465
+ if (!args.file) { result = { error: '--file (enriched JSON path) required. Optionally --task-id for single task.' }; break }
466
+
467
+ const { readFileSync } = require('node:fs')
468
+ let enrichData
469
+ try {
470
+ enrichData = JSON.parse(readFileSync(args.file, 'utf-8'))
471
+ } catch (e) {
472
+ result = { error: `Failed to read/parse ${args.file}: ${e.message}` }; break
473
+ }
474
+
475
+ // Load registry for ID mapping
476
+ const { resolve, join } = require('node:path')
477
+ const { existsSync } = require('node:fs')
478
+ const repoRoot = process.cwd()
479
+ const registryPaths = [
480
+ resolve(repoRoot, 'data/sprints/registry.json'),
481
+ resolve(repoRoot, '.a8z/data/sprints/registry.json'),
482
+ ]
483
+ const registryPath = registryPaths.find(p => existsSync(p))
484
+ let taskMap = {}
485
+ if (registryPath) {
486
+ try { taskMap = JSON.parse(readFileSync(registryPath, 'utf-8')).taskMap || {} } catch {}
487
+ }
488
+
489
+ const tasks = enrichData.tasks || [enrichData]
490
+ const updated = []
491
+ const enrichFailed = []
492
+
493
+ for (const task of tasks) {
494
+ // Resolve ClickUp task ID
495
+ let clickupId = task.clickupTaskId || taskMap[task.id] || null
496
+ if (args['task-id'] && tasks.length === 1) clickupId = args['task-id']
497
+ if (!clickupId) {
498
+ enrichFailed.push({ task: task.id, error: `No ClickUp ID found. Add clickupTaskId or ensure registry has mapping.` })
499
+ continue
500
+ }
501
+
502
+ // Sanitize
503
+ const { task: sanitized, aiPatternCount } = sanitizeTaskTexts({ ...task })
504
+ if (aiPatternCount >= 3) {
505
+ console.warn(`⚠ Task "${sanitized.title}": ${aiPatternCount} padrões de IA detectados.`)
506
+ }
507
+
508
+ // Build rich description and update
509
+ const description = buildTaskDescription(sanitized)
510
+ const updateBody = { description }
511
+ if (sanitized.points) updateBody.points = sanitized.points
512
+ if (sanitized.estimatedHours) updateBody.time_estimate = sanitized.estimatedHours * 3600000
513
+
514
+ const res = await apiWithDelay('PUT', `/task/${clickupId}`, updateBody)
515
+ if (res.id) {
516
+ updated.push({ localId: sanitized.id, clickupId, name: sanitized.title })
517
+
518
+ // Create Implementation Steps checklist if steps present
519
+ if (sanitized.steps?.length > 0) {
520
+ const stepsRes = await apiWithDelay('POST', `/task/${clickupId}/checklist`, { name: 'Implementation Steps' })
521
+ if (stepsRes.checklist?.id) {
522
+ for (const step of sanitized.steps) {
523
+ await apiWithDelay('POST', `/checklist/${stepsRes.checklist.id}/checklist_item`, { name: step })
524
+ }
525
+ }
526
+ }
527
+
528
+ // Create subtasks if present
529
+ if (sanitized.subtasks?.length > 0) {
530
+ // Need a list ID — get it from the task
531
+ const taskDetail = await api('GET', `/task/${clickupId}`)
532
+ const listId = taskDetail.list?.id
533
+ if (listId) {
534
+ for (const st of sanitized.subtasks) {
535
+ const stBody = { name: st.title || st.name, parent: clickupId, description: st.description || '' }
536
+ await apiWithDelay('POST', `/list/${listId}/task`, stBody)
537
+ }
538
+ }
539
+ }
540
+ } else {
541
+ enrichFailed.push({ task: sanitized.id, error: res.error || res.err || 'unknown' })
542
+ }
543
+ }
544
+
545
+ result = { enriched: updated.length, failed: enrichFailed.length, updated, errors: enrichFailed }
546
+ break
547
+ }
548
+ default:
549
+ result = { error: 'Unknown task subcommand. Use: list, get, create, update, delete, enrich' }
550
+ }
551
+ break
552
+
553
+ // === SUBTASK (alias) ===
554
+ case 'subtask':
555
+ switch (sub) {
556
+ case 'create': {
557
+ if (!args['list-id'] || !args.name || !args.parent) { result = { error: '--list-id, --name, and --parent required' }; break }
558
+ const body = { name: args.name, parent: args.parent }
559
+ if (args.description) body.description = args.description
560
+ if (args.status) body.status = args.status
561
+ if (args.priority) body.priority = parseInt(args.priority, 10)
562
+ if (args.points) body.points = parseFloat(args.points)
563
+ if (args.assignees) body.assignees = args.assignees.split(',').map(Number)
564
+ result = await api('POST', `/list/${args['list-id']}/task`, body)
565
+ break
566
+ }
567
+ default:
568
+ result = { error: 'Unknown subtask subcommand. Use: create' }
569
+ }
570
+ break
571
+
572
+ // === FIELD ===
573
+ case 'field':
574
+ switch (sub) {
575
+ case 'list': {
576
+ if (!args['list-id']) { result = { error: '--list-id required' }; break }
577
+ result = await api('GET', `/list/${args['list-id']}/field`)
578
+ break
579
+ }
580
+ case 'set': {
581
+ if (!args['task-id'] || !args['field-id'] || args.value === undefined) {
582
+ result = { error: '--task-id, --field-id, and --value required' }; break
583
+ }
584
+ let value = args.value
585
+ try { value = JSON.parse(value) } catch {}
586
+ result = await api('POST', `/task/${args['task-id']}/field/${args['field-id']}`, { value })
587
+ break
588
+ }
589
+ default:
590
+ result = { error: 'Unknown field subcommand. Use: list, set' }
591
+ }
592
+ break
593
+
594
+ // === TAG ===
595
+ case 'tag':
596
+ switch (sub) {
597
+ case 'add': {
598
+ if (!args['task-id'] || !args.tag) { result = { error: '--task-id and --tag required' }; break }
599
+ result = await api('POST', `/task/${args['task-id']}/tag/${encodeURIComponent(args.tag)}`, {})
600
+ break
601
+ }
602
+ default:
603
+ result = { error: 'Unknown tag subcommand. Use: add' }
604
+ }
605
+ break
606
+
607
+ // === COMMENT ===
608
+ case 'comment':
609
+ switch (sub) {
610
+ case 'create': {
611
+ if (!args['task-id'] || !args.text) { result = { error: '--task-id and --text required' }; break }
612
+ result = await api('POST', `/task/${args['task-id']}/comment`, { comment_text: args.text })
613
+ break
614
+ }
615
+ case 'list': {
616
+ if (!args['task-id']) { result = { error: '--task-id required' }; break }
617
+ result = await api('GET', `/task/${args['task-id']}/comment`)
618
+ break
619
+ }
620
+ default:
621
+ result = { error: 'Unknown comment subcommand. Use: create, list' }
622
+ }
623
+ break
624
+
625
+ // === SPRINT (composite) ===
626
+ case 'sprint':
627
+ switch (sub) {
628
+ case 'create': {
629
+ if (!args['space-id'] || !args.name || !args.start || !args.end) {
630
+ result = { error: '--space-id, --name, --start (YYYY-MM-DD), and --end (YYYY-MM-DD) required' }; break
631
+ }
632
+ const sprintId = args.id || `S${String(args.number || '01').padStart(2, '0')}`
633
+ const tracks = (args.tracks || 'backend,frontend').split(',')
634
+ const folderName = sprintFolderName(sprintId, args.name, args.start, args.end)
635
+ const meta = { sprintId, name: args.name, start: args.start, end: args.end, tracks: tracks.join(','), status: 'planning' }
636
+ const description = buildSprintDescription(meta)
637
+
638
+ // Create folder
639
+ const folder = await apiWithDelay('POST', `/space/${args['space-id']}/folder`, { name: folderName })
640
+ if (folder.error || folder.err) { result = { error: 'Failed to create folder', details: folder }; break }
641
+
642
+ // Create lists for each track
643
+ const lists = []
644
+ for (let i = 0; i < tracks.length; i++) {
645
+ const listName = `${i + 1}-${tracks[i].charAt(0).toUpperCase() + tracks[i].slice(1)}`
646
+ const list = await apiWithDelay('POST', `/folder/${folder.id}/list`, { name: listName, content: `Track: ${tracks[i]}` })
647
+ lists.push({ track: tracks[i], id: list.id, name: listName })
648
+ }
649
+
650
+ result = {
651
+ sprint: { id: sprintId, name: args.name, start: args.start, end: args.end },
652
+ folder: { id: folder.id, name: folderName },
653
+ lists,
654
+ description
655
+ }
656
+ break
657
+ }
658
+
659
+ case 'status': {
660
+ if (!args['folder-id']) { result = { error: '--folder-id required' }; break }
661
+
662
+ // Get folder info
663
+ const folder = await api('GET', `/folder/${args['folder-id']}`)
664
+ if (folder.error || folder.err) { result = { error: 'Failed to get folder', details: folder }; break }
665
+
666
+ // Get lists in folder
667
+ const listsRes = await api('GET', `/folder/${args['folder-id']}/list?archived=false`)
668
+ const lists = listsRes.lists || []
669
+
670
+ // Aggregate tasks from all lists
671
+ let totalTasks = 0, done = 0, inProgress = 0, blocked = 0, todo = 0
672
+ const trackStats = []
673
+
674
+ for (const list of lists) {
675
+ const tasksRes = await apiWithDelay('GET', `/list/${list.id}/task?subtasks=true`)
676
+ const tasks = tasksRes.tasks || []
677
+ let lDone = 0, lInProgress = 0, lBlocked = 0, lTodo = 0
678
+
679
+ for (const t of tasks) {
680
+ const s = (t.status?.status || '').toLowerCase()
681
+ if (s === 'complete' || s === 'closed' || s === 'done') lDone++
682
+ else if (s === 'in progress' || s === 'in review') lInProgress++
683
+ else if (s === 'blocked') lBlocked++
684
+ else lTodo++
685
+ }
686
+
687
+ totalTasks += tasks.length
688
+ done += lDone; inProgress += lInProgress; blocked += lBlocked; todo += lTodo
689
+ trackStats.push({ list: list.name, listId: list.id, total: tasks.length, done: lDone, inProgress: lInProgress, blocked: lBlocked, todo: lTodo })
690
+ }
691
+
692
+ const meta = parseSprintMeta(folder.description || folder.content || '')
693
+ const pct = totalTasks > 0 ? Math.round((done / totalTasks) * 100) : 0
694
+
695
+ result = {
696
+ sprint: { folderId: folder.id, name: folder.name, ...meta },
697
+ summary: { totalTasks, done, inProgress, blocked, todo, completionPct: pct },
698
+ tracks: trackStats
699
+ }
700
+ break
701
+ }
702
+
703
+ case 'get': {
704
+ if (!args['folder-id']) { result = { error: '--folder-id required' }; break }
705
+
706
+ // Get folder info
707
+ const folder = await api('GET', `/folder/${args['folder-id']}`)
708
+ if (folder.error || folder.err) { result = { error: 'Failed to get folder', details: folder }; break }
709
+
710
+ // Get lists in folder
711
+ const listsRes = await api('GET', `/folder/${args['folder-id']}/list?archived=false`)
712
+ const sprintLists = listsRes.lists || []
713
+ const meta = parseSprintMeta(folder.description || folder.content || '')
714
+
715
+ const assigneeFilter = args.assignee ? args.assignee.toLowerCase() : null
716
+ const statusFilter = args.status ? args.status.toLowerCase() : null
717
+
718
+ let totalTasks = 0, totalDone = 0, totalInProgress = 0, totalBlocked = 0, totalTodo = 0
719
+ const trackResults = []
720
+ const assigneeSummary = {}
721
+
722
+ for (const list of sprintLists) {
723
+ const tasksRes = await apiWithDelay('GET', `/list/${list.id}/task?subtasks=true&include_closed=true`)
724
+ const allTasks = tasksRes.tasks || []
725
+
726
+ const trackTasks = []
727
+ for (const t of allTasks) {
728
+ const status = (t.status?.status || 'to do').toLowerCase()
729
+
730
+ // Status filter
731
+ if (statusFilter && !status.includes(statusFilter)) continue
732
+
733
+ // Assignee info
734
+ const assignees = (t.assignees || []).map(a => ({
735
+ id: a.id,
736
+ username: a.username,
737
+ email: a.email,
738
+ initials: a.initials,
739
+ }))
740
+
741
+ // Assignee filter
742
+ if (assigneeFilter) {
743
+ const match = assignees.some(a =>
744
+ (a.username || '').toLowerCase().includes(assigneeFilter) ||
745
+ (a.email || '').toLowerCase().includes(assigneeFilter)
746
+ )
747
+ if (!match && assignees.length > 0) continue
748
+ // Include unassigned tasks too (they need assignment)
749
+ }
750
+
751
+ // Count stats
752
+ if (status === 'complete' || status === 'closed' || status === 'done') totalDone++
753
+ else if (status === 'in progress' || status === 'in review') totalInProgress++
754
+ else if (status === 'blocked') totalBlocked++
755
+ else totalTodo++
756
+ totalTasks++
757
+
758
+ // Track per-assignee stats
759
+ const assigneeKey = assignees.length > 0 ? assignees.map(a => a.username || a.email).join(', ') : 'unassigned'
760
+ if (!assigneeSummary[assigneeKey]) assigneeSummary[assigneeKey] = { total: 0, done: 0, inProgress: 0, blocked: 0, todo: 0 }
761
+ assigneeSummary[assigneeKey].total++
762
+ if (status === 'complete' || status === 'closed' || status === 'done') assigneeSummary[assigneeKey].done++
763
+ else if (status === 'in progress' || status === 'in review') assigneeSummary[assigneeKey].inProgress++
764
+ else if (status === 'blocked') assigneeSummary[assigneeKey].blocked++
765
+ else assigneeSummary[assigneeKey].todo++
766
+
767
+ // Subtasks
768
+ const subtasks = (t.subtasks || []).map(st => ({
769
+ id: st.id,
770
+ name: st.name,
771
+ status: st.status?.status,
772
+ }))
773
+
774
+ // Tags
775
+ const tags = (t.tags || []).map(tg => tg.name)
776
+
777
+ trackTasks.push({
778
+ id: t.id,
779
+ customId: t.custom_id,
780
+ name: t.name,
781
+ status: t.status?.status,
782
+ priority: t.priority?.priority,
783
+ points: t.points,
784
+ assignees,
785
+ tags,
786
+ dueDate: t.due_date ? new Date(parseInt(t.due_date)).toISOString().slice(0, 10) : null,
787
+ startDate: t.start_date ? new Date(parseInt(t.start_date)).toISOString().slice(0, 10) : null,
788
+ subtaskCount: subtasks.length,
789
+ subtasks: subtasks.length > 0 ? subtasks : undefined,
790
+ url: t.url,
791
+ })
792
+ }
793
+
794
+ trackResults.push({
795
+ track: list.name,
796
+ listId: list.id,
797
+ taskCount: trackTasks.length,
798
+ tasks: trackTasks,
799
+ })
800
+ }
801
+
802
+ const pct = totalTasks > 0 ? Math.round((totalDone / totalTasks) * 100) : 0
803
+
804
+ result = {
805
+ sprint: { folderId: folder.id, name: folder.name, ...meta },
806
+ summary: { totalTasks, done: totalDone, inProgress: totalInProgress, blocked: totalBlocked, todo: totalTodo, completionPct: pct },
807
+ tracks: trackResults,
808
+ byAssignee: assigneeSummary,
809
+ filters: { assignee: assigneeFilter, status: statusFilter },
810
+ }
811
+ break
812
+ }
813
+
814
+ case 'import': {
815
+ if (!args['folder-id'] || !args.file) { result = { error: '--folder-id and --file (JSON path) required' }; break }
816
+
817
+ const { readFileSync, writeFileSync, existsSync } = require('node:fs')
818
+ const { resolve } = require('node:path')
819
+ let plan
820
+ try {
821
+ plan = JSON.parse(readFileSync(args.file, 'utf-8'))
822
+ } catch (e) {
823
+ result = { error: `Failed to read/parse ${args.file}: ${e.message}` }; break
824
+ }
825
+
826
+ // Resolve member map for assignees (name/email → userId)
827
+ let memberMap = {}
828
+ if (args['team-id']) {
829
+ const teamRes = await api('GET', `/team/${args['team-id']}`)
830
+ for (const m of (teamRes.team?.members || [])) {
831
+ const u = m.user || {}
832
+ if (u.username) memberMap[u.username.toLowerCase()] = u.id
833
+ if (u.email) memberMap[u.email.toLowerCase()] = u.id
834
+ if (u.initials) memberMap[u.initials.toLowerCase()] = u.id
835
+ }
836
+ }
837
+
838
+ // Get lists in folder
839
+ const listsRes = await api('GET', `/folder/${args['folder-id']}/list?archived=false`)
840
+ const lists = listsRes.lists || []
841
+ const listByTrack = {}
842
+ for (const l of lists) {
843
+ const trackMatch = l.name.match(/^\d+-(\w+)/)
844
+ if (trackMatch) listByTrack[trackMatch[1].toLowerCase()] = l.id
845
+ }
846
+
847
+ const created = []
848
+ const failed = []
849
+ const tasks = plan.tasks || plan
850
+
851
+ // Pre-import validation
852
+ if (!validateAllTasks(tasks, args['skip-validation'])) {
853
+ result = { error: 'Validação falhou. Tasks com score < 30 precisam de enriquecimento. Use --skip-validation para bypass.' }
854
+ break
855
+ }
856
+
857
+ for (const task of tasks) {
858
+ const track = (task.area || task.track || 'backend').toLowerCase()
859
+ const listId = listByTrack[track] || lists[0]?.id
860
+ if (!listId) { failed.push({ task: task.id, error: `No list for track: ${track}` }); continue }
861
+
862
+ // Sanitize texts (accent fixes + AI pattern detection)
863
+ const { task: sanitized, aiPatternCount } = sanitizeTaskTexts({ ...task })
864
+ if (aiPatternCount >= 3) {
865
+ console.warn(`⚠ Task "${sanitized.title}": ${aiPatternCount} padrões de IA detectados. Considere revisar com /humanizer.`)
866
+ }
867
+
868
+ const body = {
869
+ name: sanitized.title || sanitized.name,
870
+ description: buildTaskDescription(sanitized),
871
+ tags: [sanitized.id, `track:${track}`].filter(Boolean),
872
+ }
873
+ if (sanitized.priority) {
874
+ const pMap = { P0: 1, P1: 2, P2: 3 }
875
+ body.priority = pMap[sanitized.priority] || 3
876
+ }
877
+ if (sanitized.points) body.points = sanitized.points
878
+ if (sanitized.status) body.status = sanitized.status
879
+ if (sanitized.estimatedHours) body.time_estimate = sanitized.estimatedHours * 3600000
880
+
881
+ // Resolve assignees from task.assignee or task.assignees
882
+ const rawAssignees = sanitized.assignees || (sanitized.assignee ? [sanitized.assignee] : [])
883
+ if (rawAssignees.length > 0) {
884
+ const ids = rawAssignees.map(a => {
885
+ if (typeof a === 'number') return a
886
+ return memberMap[String(a).toLowerCase()] || null
887
+ }).filter(Boolean)
888
+ if (ids.length > 0) body.assignees = ids
889
+ }
890
+
891
+ const res = await apiWithDelay('POST', `/list/${listId}/task`, body)
892
+ if (res.id) {
893
+ created.push({ localId: sanitized.id, clickupId: res.id, name: body.name, track })
894
+
895
+ // Create subtasks if present
896
+ if (sanitized.subtasks?.length) {
897
+ for (const st of sanitized.subtasks) {
898
+ const stBody = {
899
+ name: st.title || st.name,
900
+ parent: res.id,
901
+ description: st.description || '',
902
+ }
903
+ const stAssignees = st.assignees || (st.assignee ? [st.assignee] : [])
904
+ if (stAssignees.length > 0) {
905
+ const stIds = stAssignees.map(a => typeof a === 'number' ? a : memberMap[String(a).toLowerCase()] || null).filter(Boolean)
906
+ if (stIds.length > 0) stBody.assignees = stIds
907
+ }
908
+ const stRes = await apiWithDelay('POST', `/list/${listId}/task`, stBody)
909
+ if (stRes.id) created.push({ localId: st.id, clickupId: stRes.id, name: stBody.name, parent: res.id })
910
+ }
911
+ }
912
+
913
+ // Create checklist from acceptanceCriteria if present
914
+ if (sanitized.acceptanceCriteria?.length > 0) {
915
+ const clRes = await apiWithDelay('POST', `/task/${res.id}/checklist`, { name: 'Acceptance Criteria' })
916
+ if (clRes.checklist?.id) {
917
+ for (const ac of sanitized.acceptanceCriteria) {
918
+ await apiWithDelay('POST', `/checklist/${clRes.checklist.id}/checklist_item`, { name: ac })
919
+ }
920
+ }
921
+ }
922
+
923
+ // Create checklist from steps if present
924
+ if (sanitized.steps?.length > 0) {
925
+ const stepsRes = await apiWithDelay('POST', `/task/${res.id}/checklist`, { name: 'Implementation Steps' })
926
+ if (stepsRes.checklist?.id) {
927
+ for (const step of sanitized.steps) {
928
+ await apiWithDelay('POST', `/checklist/${stepsRes.checklist.id}/checklist_item`, { name: step })
929
+ }
930
+ }
931
+ }
932
+ } else {
933
+ failed.push({ task: sanitized.id, error: res.error || res.err || 'unknown' })
934
+ }
935
+ }
936
+
937
+ // Auto-populate sprint registry (G1)
938
+ if (created.length > 0) {
939
+ try {
940
+ const repoRoot = process.cwd()
941
+ const registryPaths = [
942
+ resolve(repoRoot, 'data/sprints/registry.json'),
943
+ resolve(repoRoot, '.a8z/data/sprints/registry.json'),
944
+ ]
945
+ const registryPath = registryPaths.find(p => existsSync(p)) || registryPaths[0]
946
+ let registry = { version: '1.0', sprints: [], taskMap: {} }
947
+ if (existsSync(registryPath)) {
948
+ try { registry = JSON.parse(readFileSync(registryPath, 'utf-8')) } catch {}
949
+ }
950
+
951
+ // Update taskMap
952
+ for (const c of created) {
953
+ if (c.localId && !c.parent) registry.taskMap[c.localId] = c.clickupId
954
+ }
955
+
956
+ // Add sprint entry if plan has sprint metadata
957
+ const sprintMeta = plan.sprint || {}
958
+ if (sprintMeta.id || args['sprint-id']) {
959
+ const sprintId = sprintMeta.id || args['sprint-id']
960
+ const existing = registry.sprints.findIndex(s => s.id === sprintId)
961
+ const entry = {
962
+ id: sprintId,
963
+ folderId: args['folder-id'],
964
+ name: sprintMeta.name || '',
965
+ start: sprintMeta.startDate || sprintMeta.start || '',
966
+ end: sprintMeta.endDate || sprintMeta.end || '',
967
+ importedAt: new Date().toISOString(),
968
+ }
969
+ if (existing >= 0) registry.sprints[existing] = entry
970
+ else registry.sprints.push(entry)
971
+ }
972
+
973
+ writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n')
974
+ } catch (regErr) {
975
+ // Registry save is best-effort, don't fail the import
976
+ result = { imported: created.length, failed: failed.length, created, errors: failed, registryWarning: regErr.message }
977
+ break
978
+ }
979
+ }
980
+
981
+ result = { imported: created.length, failed: failed.length, created, errors: failed, registrySaved: created.length > 0 }
982
+ break
983
+ }
984
+
985
+ default:
986
+ result = { error: 'Unknown sprint subcommand. Use: create, status, import' }
987
+ }
988
+ break
989
+
990
+ // === CHECKLIST ===
991
+ case 'checklist':
992
+ switch (sub) {
993
+ case 'create': {
994
+ if (!args['task-id'] || !args.name) { result = { error: '--task-id and --name required' }; break }
995
+ result = await api('POST', `/task/${args['task-id']}/checklist`, { name: args.name })
996
+ break
997
+ }
998
+ case 'add-item': {
999
+ if (!args['checklist-id'] || !args.name) { result = { error: '--checklist-id and --name required' }; break }
1000
+ result = await api('POST', `/checklist/${args['checklist-id']}/checklist_item`, { name: args.name })
1001
+ break
1002
+ }
1003
+ case 'list': {
1004
+ if (!args['task-id']) { result = { error: '--task-id required' }; break }
1005
+ const taskRes = await api('GET', `/task/${args['task-id']}`)
1006
+ const checklists = (taskRes.checklists || []).map(cl => ({
1007
+ id: cl.id,
1008
+ name: cl.name,
1009
+ items: (cl.items || []).map(it => ({
1010
+ id: it.id,
1011
+ name: it.name,
1012
+ resolved: it.resolved,
1013
+ })),
1014
+ }))
1015
+ result = { taskId: args['task-id'], checklists, count: checklists.length }
1016
+ break
1017
+ }
1018
+ default:
1019
+ result = { error: 'Unknown checklist subcommand. Use: create, add-item, list' }
1020
+ }
1021
+ break
1022
+
1023
+ // === DEFAULT (help) ===
1024
+ default:
1025
+ result = {
1026
+ error: cmd ? `Unknown command: ${cmd}` : 'No command provided',
1027
+ usage: {
1028
+ workspace: 'workspace [list|members] --team-id <id>',
1029
+ space: 'space [list|get] [id] --team-id <id>',
1030
+ folder: 'folder [list|get|create] [id] --space-id <id> --name <name>',
1031
+ list: 'list [list|create] --folder-id <id> --name <name>',
1032
+ task: 'task [list|get|create|update|delete|enrich] [id] --list-id <id> --task-id <id> --name <name> [--assignees id1,id2] [--file enriched.json]',
1033
+ subtask: 'subtask create --list-id <id> --parent <id> --name <name>',
1034
+ field: 'field [list|set] --list-id <id> --task-id <id> --field-id <id> --value <val>',
1035
+ tag: 'tag add --task-id <id> --tag <name>',
1036
+ comment: 'comment [create|list] --task-id <id> --text <text>',
1037
+ checklist: 'checklist [create|add-item|list] --task-id <id> --checklist-id <id> --name <name>',
1038
+ sprint: {
1039
+ create: 'sprint create --space-id <id> --name <name> --start YYYY-MM-DD --end YYYY-MM-DD [--tracks backend,frontend] [--id S01]',
1040
+ get: 'sprint get --folder-id <id> [--assignee <name/email>] [--status <status>]',
1041
+ status: 'sprint status --folder-id <id>',
1042
+ import: 'sprint import --folder-id <id> --file <plan.json> [--team-id <id>] [--sprint-id S01] [--skip-validation]',
1043
+ },
1044
+ },
1045
+ flags: {
1046
+ '--dry-run': 'Show request without executing',
1047
+ '--skip-validation': 'Skip task completeness validation on sprint import',
1048
+ }
1049
+ }
1050
+ }
1051
+
1052
+ console.log(JSON.stringify(result, null, 2))
1053
+ }
1054
+
1055
+ main().catch(err => {
1056
+ console.error(JSON.stringify({ error: err.message }))
1057
+ process.exit(1)
1058
+ })