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.
- package/AGENTS.md +46 -0
- package/CLAUDE.md +31 -0
- package/GEMINI.md +17 -0
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/agents/profiles/architect.md +13 -0
- package/agents/profiles/dev.md +13 -0
- package/agents/profiles/devops.md +12 -0
- package/agents/profiles/ganbatte-os-master.md +444 -0
- package/agents/profiles/index.json +14 -0
- package/agents/profiles/po.md +7 -0
- package/agents/profiles/qa.md +456 -0
- package/agents/profiles/sm.md +7 -0
- package/agents/profiles/squad-creator.md +7 -0
- package/agents/profiles/ux-design-expert.md +14 -0
- package/config.json +15 -0
- package/docs/curation.md +60 -0
- package/docs/gos_installation_guide.md +114 -0
- package/docs/ide-compatibility.md +20 -0
- package/docs/plan/plan-git-operations.md +51 -0
- package/docs/plan-distribuicao-publica.md +360 -0
- package/docs/stacks/stack-git-operations.md +154 -0
- package/docs/toolchain-map.md +18 -0
- package/integrations/README.md +42 -0
- package/integrations/antigravity/README.md +29 -0
- package/integrations/antigravity/command-map.json +29 -0
- package/integrations/claude/README.md +35 -0
- package/integrations/claude/agent-map.json +46 -0
- package/integrations/claude/command-map.json +32 -0
- package/integrations/claude/litellm-proxy.md +93 -0
- package/integrations/claude/mcp-specifics.md +121 -0
- package/integrations/codex/README.md +29 -0
- package/integrations/codex/command-map.json +29 -0
- package/integrations/cursor/README.md +8 -0
- package/integrations/cursor/command-map.json +11 -0
- package/integrations/gemini/README.md +13 -0
- package/integrations/gemini/command-map.json +11 -0
- package/integrations/kilo-code/README.md +7 -0
- package/integrations/kilo-code/command-map.json +11 -0
- package/integrations/opencode/README.md +91 -0
- package/integrations/opencode/command-map.json +46 -0
- package/integrations/registry.json +20 -0
- package/manifests/g-os-runtime-manifest.json +39 -0
- package/manifests/gos-install-manifest.json +46 -0
- package/opencode.json +7 -0
- package/package.json +51 -0
- package/playbooks/feature-development-playbook.md +239 -0
- package/playbooks/sprint-planner-playbook.md +127 -0
- package/playbooks/squad-pipeline-runner.md +196 -0
- package/playbooks/ssh-multi-account-setup.md +185 -0
- package/prompts/01-search.md +18 -0
- package/prompts/02-spec.md +19 -0
- package/prompts/03-tasks.md +15 -0
- package/prompts/04-code.md +10 -0
- package/prompts/05-reviews.md +11 -0
- package/rules/plan-mode.md +60 -0
- package/scripts/cli/gos-cli.js +679 -0
- package/scripts/hooks/pre-commit-validate.js +201 -0
- package/scripts/integrations/check-ide-compat.js +44 -0
- package/scripts/integrations/setup-ide-adapters.js +87 -0
- package/scripts/tools/clickup-preprocess.js +218 -0
- package/scripts/tools/clickup.js +1058 -0
- package/skills/agent-teams/SKILL.md +78 -0
- package/skills/agent-teams/presets/team-all.yaml +14 -0
- package/skills/agent-teams/presets/team-fullstack.yaml +17 -0
- package/skills/agent-teams/presets/team-ide-minimal.yaml +9 -0
- package/skills/agent-teams/presets/team-no-ui.yaml +12 -0
- package/skills/agent-teams/presets/team-qa-focused.yaml +83 -0
- package/skills/clickup/SKILL.md +151 -0
- package/skills/component-dedup/SKILL.md +101 -0
- package/skills/design-to-code/SKILL.md +255 -0
- package/skills/figma-implement-design/SKILL.md +227 -0
- package/skills/figma-make-analyzer/SKILL.md +140 -0
- package/skills/frontend-dev/SKILL.md +271 -0
- package/skills/git-ssh-setup/SKILL.md +164 -0
- package/skills/interface-design/SKILL.md +350 -0
- package/skills/interface-design/references/audit.md +76 -0
- package/skills/interface-design/references/craft-examples.md +134 -0
- package/skills/interface-design/references/critique.md +92 -0
- package/skills/interface-design/references/extract.md +92 -0
- package/skills/interface-design/references/principles.md +348 -0
- package/skills/interface-design/references/templates/system-precision.md +73 -0
- package/skills/interface-design/references/templates/system-warmth.md +67 -0
- package/skills/interface-design/references/validation.md +137 -0
- package/skills/make-code-triage/SKILL.md +135 -0
- package/skills/make-version-diff/SKILL.md +87 -0
- package/skills/plan-to-tasks/SKILL.md +136 -0
- package/skills/react-best-practices/AGENTS.md +2975 -0
- package/skills/react-best-practices/SKILL.md +151 -0
- package/skills/react-best-practices/metadata.json +15 -0
- package/skills/react-best-practices/rules/_sections.md +46 -0
- package/skills/react-best-practices/rules/_template.md +28 -0
- package/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/skills/react-best-practices/rules/advanced-init-once.md +42 -0
- package/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
- package/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/skills/react-best-practices/rules/async-dependencies.md +51 -0
- package/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/skills/react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/skills/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/skills/react-best-practices/rules/server-auth-actions.md +96 -0
- package/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/skills/react-best-practices/rules/server-dedup-props.md +65 -0
- package/skills/react-best-practices/rules/server-hoist-static-io.md +142 -0
- package/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/skills/react-doctor/SKILL.md +74 -0
- package/skills/registry.json +21 -0
- package/skills/sprint-planner/SKILL.md +434 -0
- package/squads/design-delivery/README.md +10 -0
- package/squads/design-delivery/squad.yaml +30 -0
- package/squads/design-delivery/workflows/wf-design-delivery.yaml +27 -0
- package/squads/design-squad/README.md +31 -0
- package/squads/design-squad/agents/brad-frost.md +185 -0
- package/squads/design-squad/agents/dan-mall.md +178 -0
- package/squads/design-squad/agents/dave-malouf.md +198 -0
- package/squads/design-squad/agents/design-chief.md +109 -0
- package/squads/design-squad/agents/design-system-architect.md +109 -0
- package/squads/design-squad/agents/ui-engineer.md +102 -0
- package/squads/design-squad/agents/ux-designer.md +105 -0
- package/squads/design-squad/agents/visual-generator.md +108 -0
- package/squads/design-squad/checklists/output-quality.md +76 -0
- package/squads/design-squad/config/config.yaml +65 -0
- package/squads/design-squad/data/design-patterns-catalog.yaml +276 -0
- package/squads/design-squad/data/routing-catalog.yaml +95 -0
- package/squads/design-squad/squad.yaml +88 -0
- package/squads/design-squad/tasks/audit-design.md +174 -0
- package/squads/design-squad/tasks/create-component-spec.md +185 -0
- package/squads/design-squad/tasks/create-design-system.md +179 -0
- package/squads/design-squad/tasks/design-ux-flow.md +184 -0
- package/squads/design-squad/tasks/diagnose.md +138 -0
- package/squads/design-squad/tasks/generate-handoff.md +186 -0
- package/squads/design-squad/tasks/review.md +133 -0
- package/squads/design-squad/tasks/setup-design-ops.md +177 -0
- package/squads/design-squad/workflows/wf-design-system-creation.yaml +131 -0
- package/squads/design-squad/workflows/wf-feature-design.yaml +114 -0
- package/squads/git-operations/README.md +30 -0
- package/squads/git-operations/squad.yaml +27 -0
- package/squads/git-operations/workflows/wf-safe-commit.yaml +27 -0
- package/squads/git-operations/workflows/wf-ssh-setup.yaml +27 -0
- package/squads/sprint-planning/agents/sprint-chief.md +47 -0
- package/squads/sprint-planning/agents/sprint-planner-agent.md +43 -0
- package/squads/sprint-planning/agents/sprint-tracker.md +43 -0
- package/squads/sprint-planning/agents/task-importer.md +44 -0
- package/squads/sprint-planning/checklists/sprint-readiness.md +27 -0
- package/squads/sprint-planning/config/config.yaml +65 -0
- package/squads/sprint-planning/data/clickup-field-mapping.yaml +94 -0
- package/squads/sprint-planning/squad.yaml +52 -0
- package/squads/sprint-planning/tasks/close-sprint.md +43 -0
- package/squads/sprint-planning/tasks/create-sprint.md +42 -0
- package/squads/sprint-planning/tasks/import-tasks.md +39 -0
- package/squads/sprint-planning/tasks/sync-status.md +31 -0
- package/squads/sprint-planning/workflows/wf-sprint-creation.yaml +59 -0
- package/squads/sprint-planning/workflows/wf-sprint-sync.yaml +35 -0
- package/templates/adr-tmpl.yaml +76 -0
- 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
|
+
})
|