ganbatte-os 0.2.8 → 0.2.9

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.
@@ -58,3 +58,21 @@ Foram mantidos apenas os suportes exigidos pelas skills copiadas:
58
58
  - memoria completa do framework
59
59
  - CLI legada do framework anterior
60
60
  - runtime multi-IDE integral
61
+
62
+ ## Decisoes de Curadoria
63
+
64
+ ### Lessons Loop / `tasks/lessons.md`
65
+
66
+ O `G-OS` NAO ativa o loop de `tasks/lessons.md` e NAO replica o `Livro de Ouro` do framework principal.
67
+
68
+ Motivos:
69
+
70
+ - esta distribuicao nao inclui o subsistema completo de memoria do `.a8z-framework`
71
+ - o custo de manter memoria graduada localmente aqui seria desproporcional ao escopo enxuto do `G-OS`
72
+ - o ciclo canonico desta distribuicao permanece: `implementation_plan.md` -> `task.md` -> `walkthrough.md`
73
+
74
+ Decisao operacional:
75
+
76
+ - nao criar `tasks/lessons.md` neste repo
77
+ - nao adicionar hooks de captura automatica de lessons
78
+ - registrar aprendizados duraveis por curadoria manual em `docs/curation.md` ou promover direto para `rules/` quando houver padrao estavel
@@ -0,0 +1,20 @@
1
+ {
2
+ "ide": "qwen-code",
3
+ "version": "1.0.0",
4
+ "status": "active",
5
+ "notes": {
6
+ "settingsFile": ".qwen/settings.json",
7
+ "skillsDir": ".qwen/skills/gos-{slug}/SKILL.md",
8
+ "agentsDir": ".qwen/agents/{id}.md",
9
+ "commandsDir": ".qwen/commands/",
10
+ "mcpConfig": "mcpServers object in .qwen/settings.json",
11
+ "ignoreFile": ".qwenignore"
12
+ },
13
+ "mappings": [
14
+ { "canonicalCommand": "gos.search", "coreArtifact": ".gos/prompts/01-search.md" },
15
+ { "canonicalCommand": "gos.spec", "coreArtifact": ".gos/prompts/02-spec.md" },
16
+ { "canonicalCommand": "gos.tasks", "coreArtifact": ".gos/prompts/03-tasks.md" },
17
+ { "canonicalCommand": "gos.code", "coreArtifact": ".gos/prompts/04-code.md" },
18
+ { "canonicalCommand": "gos.review", "coreArtifact": ".gos/prompts/05-reviews.md" }
19
+ ]
20
+ }
@@ -15,6 +15,7 @@
15
15
  "cursor": { "workspaceDir": ".cursor", "ruleFile": ".cursor/rules/g-os.mdc", "status": "active" },
16
16
  "codex": { "workspaceDir": ".codex", "adapter": "integrations/codex", "status": "active" },
17
17
  "opencode": { "workspaceDir": ".opencode", "adapter": "integrations/opencode", "status": "active" },
18
- "kilo-code": { "workspaceDir": ".kilocode", "ruleFile": ".kilocode/rules/g-os.md", "status": "active" }
18
+ "kilo-code": { "workspaceDir": ".kilocode", "ruleFile": ".kilocode/rules/g-os.md", "status": "active" },
19
+ "qwen-code": { "workspaceDir": ".qwen", "adapter": "integrations/qwen", "status": "active" }
19
20
  }
20
21
  }
@@ -0,0 +1,38 @@
1
+ ---
2
+ paths: "**"
3
+ severity: INFO
4
+ ---
5
+
6
+ # Demand Elegance
7
+
8
+ ## Resumo
9
+
10
+ Agentes de IA devem buscar a solução mais elegante para problemas não-triviais, evitando soluções "hacky" ou excessivamente complexas. No entanto, deve haver um equilíbrio: para correções simples e óbvias, a velocidade e a simplicidade técnica prevalecem sobre o over-engineering.
11
+
12
+ ## Princípios
13
+
14
+ 1. **The Elegance Pause**: Para qualquer mudança que envolva decisões arquiteturais ou lógica complexa, o agente deve pausar internamente e perguntar: "Existe uma forma mais elegante de resolver isso?".
15
+ 2. **Hacky vs. Elegant**: Se um fix parece "remendado" (hacky), o agente deve propor a implementação da solução robusta e elegante, mesmo que exija um pouco mais de esforço inicial.
16
+ 3. **Over-engineering Guardrail**: Não crie abstrações desnecessárias para problemas simples. Se a solução óbvia é direta, use-a.
17
+ 4. **Refactoring Mindset**: Trate cada tarefa como uma oportunidade de deixar o código ligeiramente mais limpo do que o encontrou (Regra do Escoteiro).
18
+
19
+ ## Quando aplicar (The Elegance Gate)
20
+
21
+ | Contexto | Ação |
22
+ |----------|------|
23
+ | **Bug Fix Crítico** | Direto ao ponto, correção robusta mas sem abstrações complexas. |
24
+ | **Nova Feature** | Design elegante, modular e extensível por padrão. |
25
+ | **Refatoração** | Máxima elegância, seguindo padrões de projeto e princípios SOLID. |
26
+ | **Scripts Rápidos** | Funcionalidade sobre estética de código, desde que seguro. |
27
+
28
+ ## Exemplos
29
+
30
+ ### ❌ Inelegante (Hacky)
31
+ Usar `setTimeout` arbitrário para esperar uma condição de rede sem retry-policy ou tratamento de erro adequado.
32
+
33
+ ### ✅ Elegante
34
+ Implementar um padrão de `backoff exponencial` ou um `subscription mechanism` que reaja ao estado real do recurso.
35
+
36
+ ---
37
+
38
+ *Baseado nos Core Principles do .a8z-OS.*
@@ -48,6 +48,9 @@ function readState(statePath, sessionId) {
48
48
  sessionId,
49
49
  touchedFiles: [],
50
50
  commands: [],
51
+ gitCommit: false,
52
+ lastCommitMessage: "",
53
+ taskRefs: [],
51
54
  significantAction: false,
52
55
  updatedAt: new Date().toISOString(),
53
56
  };
@@ -60,6 +63,9 @@ function readState(statePath, sessionId) {
60
63
  sessionId,
61
64
  touchedFiles: [],
62
65
  commands: [],
66
+ gitCommit: false,
67
+ lastCommitMessage: "",
68
+ taskRefs: [],
63
69
  significantAction: false,
64
70
  updatedAt: new Date().toISOString(),
65
71
  };
@@ -125,7 +131,19 @@ function main() {
125
131
  const toolName = String(payload.tool || payload.tool_name || payload.matcher || "");
126
132
  if (/Bash/i.test(toolName)) {
127
133
  const command = extractCommand(payload);
128
- if (command) uniquePush(state.commands, command);
134
+ if (command) {
135
+ uniquePush(state.commands, command);
136
+ if (/\bgit\s+commit\b/i.test(command)) {
137
+ state.gitCommit = true;
138
+ const msgMatch = command.match(/-m\s+["']([^"']+)["']/);
139
+ if (msgMatch) {
140
+ const message = msgMatch[1].trim();
141
+ state.lastCommitMessage = message;
142
+ const refs = message.match(/\bT-\d{3}\b/g);
143
+ if (refs) state.taskRefs = [...new Set(refs)];
144
+ }
145
+ }
146
+ }
129
147
  }
130
148
 
131
149
  if (state.touchedFiles.length > 0 || state.commands.length > 0) {
@@ -122,6 +122,22 @@ function main() {
122
122
  const summary = [];
123
123
  const syncMatcher = /^(\.gos|\.claude|data|README\.md|CLAUDE\.md|AGENTS\.md|GEMINI\.md)/;
124
124
 
125
+ // Trigger post-commit notification if git commit was made
126
+ if (state.gitCommit) {
127
+ try {
128
+ execFileSync('node', [path.join(ROOT, '.gos', 'scripts', 'hooks', 'post-commit-notify.js')], {
129
+ cwd: ROOT,
130
+ encoding: 'utf8',
131
+ stdio: ['pipe', 'pipe', 'pipe'],
132
+ timeout: 30000,
133
+ })
134
+ summary.push('post-commit-notify OK')
135
+ } catch (error) {
136
+ const message = error.stderr || error.stdout || error.message || 'falha no post-commit'
137
+ summary.push(`post-commit-notify falhou (${String(message).split(/\r?\n/)[0]})`)
138
+ }
139
+ }
140
+
125
141
  if (anyPathMatches(touchedFiles, syncMatcher)) {
126
142
  try {
127
143
  runNpm(["run", "sync:ides"], { timeout: 180000 });
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+
3
+ // post-commit-notify.js — Post-commit hook: marks tasks done in ClickUp + notifies Slack
4
+ // Trigger: Claude Code Stop hook or git post-commit
5
+ // Rule: ALWAYS exit 0 (observation hook, never blocks)
6
+
7
+ const { execFileSync } = require('node:child_process')
8
+ const { existsSync, readFileSync } = require('node:fs')
9
+ const { resolve } = require('node:path')
10
+
11
+ // Find repo root
12
+ function findRepoRoot() {
13
+ try {
14
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf-8' }).trim()
15
+ } catch {
16
+ return process.cwd()
17
+ }
18
+ }
19
+
20
+ // Parse last commit
21
+ function getLastCommit() {
22
+ try {
23
+ const log = execFileSync('git', ['log', '-1', '--format=%H|%s|%an'], { encoding: 'utf-8' }).trim()
24
+ const sepIdx = log.indexOf('|')
25
+ const hash = log.slice(0, sepIdx)
26
+ const rest = log.slice(sepIdx + 1)
27
+ const lastSep = rest.lastIndexOf('|')
28
+ const message = rest.slice(0, lastSep)
29
+ const author = rest.slice(lastSep + 1)
30
+ return { hash, message, author }
31
+ } catch {
32
+ return null
33
+ }
34
+ }
35
+
36
+ // Extract task refs from commit message (T-NNN pattern)
37
+ function extractTaskRefs(message) {
38
+ const matches = message.match(/\bT-(\d{3})\b/g)
39
+ return matches ? [...new Set(matches)] : []
40
+ }
41
+
42
+ // Load sprint registry to resolve local IDs -> ClickUp IDs
43
+ function loadRegistry(repoRoot) {
44
+ const paths = [
45
+ resolve(repoRoot, 'data/sprints/registry.json'),
46
+ resolve(repoRoot, '.gos/data/sprints/registry.json'),
47
+ ]
48
+ for (const p of paths) {
49
+ if (existsSync(p)) {
50
+ try {
51
+ return { data: JSON.parse(readFileSync(p, 'utf-8')), path: p }
52
+ } catch { continue }
53
+ }
54
+ }
55
+ return { data: { version: '1.0', sprints: [], taskMap: {}, taskSprintMap: {} }, path: paths[0] }
56
+ }
57
+
58
+ // Run a CLI tool safely (no shell injection)
59
+ function runTool(repoRoot, tool, toolArgs) {
60
+ const toolPaths = [
61
+ resolve(repoRoot, `.gos/scripts/tools/${tool}`),
62
+ resolve(repoRoot, `scripts/tools/${tool}`),
63
+ ]
64
+ const toolPath = toolPaths.find(p => existsSync(p))
65
+ if (!toolPath) return null
66
+
67
+ try {
68
+ const result = execFileSync('node', [toolPath, ...toolArgs], {
69
+ encoding: 'utf-8',
70
+ timeout: 15000,
71
+ env: process.env,
72
+ })
73
+ return JSON.parse(result)
74
+ } catch (e) {
75
+ return { error: e.message }
76
+ }
77
+ }
78
+
79
+ // Detect if commit implies task completion
80
+ function isCompletionCommit(message) {
81
+ const completionPatterns = [
82
+ /^feat[:(]/i,
83
+ /^fix[:(]/i,
84
+ /\b(close[sd]?|complete[sd]?|finish|done|resolve[sd]?)\b/i,
85
+ ]
86
+ return completionPatterns.some(p => p.test(message))
87
+ }
88
+
89
+ // Detect if commit is a start/WIP commit
90
+ function isStartCommit(message) {
91
+ const startPatterns = [
92
+ /^(wip|chore|progress|draft|start|refactor|test|docs|style)[:(]/i,
93
+ /\b(wip|in.?progress|draft|start)\b/i,
94
+ ]
95
+ return startPatterns.some(p => p.test(message))
96
+ }
97
+
98
+ // Find sprint for a task reference
99
+ function findSprintForTask(registry, taskRef) {
100
+ if (registry.taskSprintMap?.[taskRef]) {
101
+ return registry.sprints.find(s => s.id === registry.taskSprintMap[taskRef]) || null
102
+ }
103
+ return registry.sprints.filter(s => !s.end || new Date(s.end) >= new Date())
104
+ .sort((a, b) => new Date(b.start) - new Date(a.start))[0] || null
105
+ }
106
+
107
+ async function main() {
108
+ const repoRoot = findRepoRoot()
109
+ const commit = getLastCommit()
110
+ if (!commit) return
111
+
112
+ const taskRefs = extractTaskRefs(commit.message)
113
+ if (taskRefs.length === 0) return
114
+
115
+ const hasClickUp = !!process.env.CLICKUP_API_KEY
116
+ const hasSlack = !!process.env.SLACK_WEBHOOK_URL
117
+ if (!hasClickUp && !hasSlack) return
118
+
119
+ const { data: registry } = loadRegistry(repoRoot)
120
+ const shouldComplete = isCompletionCommit(commit.message)
121
+ const shouldStart = isStartCommit(commit.message)
122
+
123
+ for (const ref of taskRefs) {
124
+ const clickupId = registry.taskMap?.[ref]
125
+
126
+ // Update ClickUp task status
127
+ if (hasClickUp && clickupId) {
128
+ if (shouldComplete) {
129
+ runTool(repoRoot, 'clickup.js', ['task', 'update', '--task-id', clickupId, '--status', 'complete'])
130
+ } else if (shouldStart) {
131
+ runTool(repoRoot, 'clickup.js', ['task', 'update', '--task-id', clickupId, '--status', 'in progress'])
132
+ }
133
+ }
134
+
135
+ // Notify Slack
136
+ if (hasSlack) {
137
+ const status = shouldComplete ? 'concluida' : shouldStart ? 'em andamento' : 'atualizada'
138
+ runTool(repoRoot, 'slack-notify.js', [
139
+ 'task-update',
140
+ '--task', ref,
141
+ '--status', status,
142
+ '--commit', commit.hash,
143
+ '--author', commit.author,
144
+ '--message', commit.message,
145
+ ])
146
+ }
147
+
148
+ // Sprint completion check (if task completed)
149
+ if (shouldComplete) {
150
+ const sprint = findSprintForTask(registry, ref)
151
+ if (sprint) {
152
+ const tasksInSprint = Object.entries(registry.taskSprintMap || {})
153
+ .filter(([, sid]) => sid === sprint.id)
154
+ .map(([tid]) => tid)
155
+ const completedTasks = tasksInSprint.filter(tid => {
156
+ const cuId = registry.taskMap?.[tid]
157
+ if (!cuId) return false
158
+ return true
159
+ })
160
+ if (tasksInSprint.length > 0 && completedTasks.length === tasksInSprint.length) {
161
+ if (hasSlack) {
162
+ runTool(repoRoot, 'slack-notify.js', [
163
+ 'sprint-complete',
164
+ '--sprint', sprint.id,
165
+ '--name', sprint.name,
166
+ '--tasks', tasksInSprint.join(', '),
167
+ ])
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ main().catch(() => {}).finally(() => process.exit(0))
@@ -20,7 +20,8 @@ const required = [
20
20
  '.gos/integrations/antigravity/command-map.json',
21
21
  '.gos/integrations/gemini/command-map.json',
22
22
  '.gos/integrations/cursor/command-map.json',
23
- '.gos/integrations/kilo-code/command-map.json'
23
+ '.gos/integrations/kilo-code/command-map.json',
24
+ '.gos/integrations/qwen/command-map.json'
24
25
  ];
25
26
 
26
27
  const missing = required.filter((entry) => !fs.existsSync(path.join(root, entry)));
@@ -46,11 +46,14 @@ function main() {
46
46
  const codexSkill = path.join(root, '.codex', 'skills', `gos-${skill.slug}.md`);
47
47
  const geminiSkill = path.join(root, '.gemini', 'skills', `gos-${skill.slug}`, 'SKILL.md');
48
48
  const opencodeSkill = path.join(root, '.opencode', 'skills', `gos-${skill.slug}`, 'SKILL.md');
49
+ const qwenSkill = path.join(root, '.qwen', 'skills', `gos-${skill.slug}`, 'SKILL.md');
49
50
 
50
51
  writeFile(claudeSkill, skillWrapper(skill.slug, relativeTarget(claudeSkill, path.join(root, '.gos', skillTargetPath))));
51
52
  writeFile(codexSkill, skillWrapper(skill.slug, relativeTarget(codexSkill, path.join(root, '.gos', skillTargetPath))));
52
53
  writeFile(geminiSkill, skillWrapper(skill.slug, relativeTarget(geminiSkill, path.join(root, '.gos', skillTargetPath))));
53
54
  writeFile(opencodeSkill, skillWrapper(skill.slug, relativeTarget(opencodeSkill, path.join(root, '.gos', skillTargetPath))));
55
+ ensureDir(path.dirname(qwenSkill));
56
+ writeFile(qwenSkill, skillWrapper(skill.slug, relativeTarget(qwenSkill, path.join(root, '.gos', skillTargetPath))));
54
57
  }
55
58
 
56
59
  const antigravityInstructions = [
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+
3
+ // slack-notify.js — Slack notification tool via Incoming Webhooks (zero-dep)
4
+ // Usage: node slack-notify.js <command> [--options]
5
+ // Auth: SLACK_WEBHOOK_URL env var (Incoming Webhook URL)
6
+
7
+ const WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL
8
+
9
+ if (!WEBHOOK_URL) {
10
+ // Graceful skip — no webhook configured is not an error
11
+ console.log(JSON.stringify({ skipped: true, reason: 'SLACK_WEBHOOK_URL not set' }))
12
+ process.exit(0)
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
+ async function sendWebhook(payload) {
39
+ if (args['dry-run']) {
40
+ return { _dry_run: true, url: WEBHOOK_URL.replace(/\/[^/]{6,}$/, '/***'), payload }
41
+ }
42
+
43
+ const res = await fetch(WEBHOOK_URL, {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify(payload),
47
+ })
48
+
49
+ const text = await res.text()
50
+ if (res.ok) {
51
+ return { sent: true, status: res.status }
52
+ }
53
+ return { sent: false, status: res.status, body: text }
54
+ }
55
+
56
+ function escapeSlack(text) {
57
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
58
+ }
59
+
60
+ function buildTaskDonePayload(taskId, commit, author, extra = {}) {
61
+ const sprint = extra.sprint || ''
62
+ const track = extra.track || ''
63
+ const message = extra.message || ''
64
+
65
+ const lines = [`*:white_check_mark: Task Concluída*`]
66
+ lines.push(`>*Task:* ${escapeSlack(taskId)}${message ? ` — ${escapeSlack(message)}` : ''}`)
67
+ if (sprint) lines.push(`>*Sprint:* ${escapeSlack(sprint)}`)
68
+ if (track) lines.push(`>*Track:* ${escapeSlack(track)}`)
69
+ lines.push(`>*Commit:* \`${escapeSlack(commit.slice(0, 7))}\`${message ? ` — ${escapeSlack(message)}` : ''}`)
70
+ lines.push(`>*Author:* ${escapeSlack(author)}`)
71
+
72
+ return { text: lines.join('\n') }
73
+ }
74
+
75
+ function buildSprintSummaryPayload(data) {
76
+ const summary = data.summary || data
77
+ const name = data.sprint?.name || data.name || 'Sprint'
78
+ const now = Math.floor(Date.now() / 1000)
79
+
80
+ return {
81
+ blocks: [
82
+ {
83
+ type: 'header',
84
+ text: { type: 'plain_text', text: `:bar_chart: ${name} — Status Update` }
85
+ },
86
+ {
87
+ type: 'section',
88
+ fields: [
89
+ { type: 'mrkdwn', text: `*Total:*\n${summary.totalTasks || 0} tasks` },
90
+ { type: 'mrkdwn', text: `*Done:*\n${summary.done || 0} (${summary.completionPct || 0}%)` }
91
+ ]
92
+ },
93
+ {
94
+ type: 'section',
95
+ fields: [
96
+ { type: 'mrkdwn', text: `*In Progress:*\n${summary.inProgress || 0}` },
97
+ { type: 'mrkdwn', text: `*Blocked:*\n${summary.blocked || 0}` }
98
+ ]
99
+ },
100
+ { type: 'divider' },
101
+ {
102
+ type: 'context',
103
+ elements: [
104
+ { type: 'mrkdwn', text: `Updated: <!date^${now}^{date_short} {time}|${new Date().toISOString()}>` }
105
+ ]
106
+ }
107
+ ]
108
+ }
109
+ }
110
+
111
+ async function main() {
112
+ let result
113
+
114
+ switch (cmd) {
115
+ case 'send': {
116
+ if (args.text) {
117
+ result = await sendWebhook({ text: args.text })
118
+ } else if (args['blocks-file']) {
119
+ const { readFileSync } = require('node:fs')
120
+ try {
121
+ const payload = JSON.parse(readFileSync(args['blocks-file'], 'utf-8'))
122
+ result = await sendWebhook(payload)
123
+ } catch (e) {
124
+ result = { error: `Failed to read ${args['blocks-file']}: ${e.message}` }
125
+ }
126
+ } else {
127
+ result = { error: '--text or --blocks-file required' }
128
+ }
129
+ break
130
+ }
131
+
132
+ case 'task-done': {
133
+ if (!args.task || !args.commit || !args.author) {
134
+ result = { error: '--task, --commit, and --author required' }; break
135
+ }
136
+ const payload = buildTaskDonePayload(args.task, args.commit, args.author, {
137
+ sprint: args.sprint,
138
+ track: args.track,
139
+ message: args.message,
140
+ })
141
+ result = await sendWebhook(payload)
142
+ break
143
+ }
144
+
145
+ case 'sprint-summary': {
146
+ if (!args.file) { result = { error: '--file (sprint status JSON) required' }; break }
147
+ const { readFileSync } = require('node:fs')
148
+ try {
149
+ const data = JSON.parse(readFileSync(args.file, 'utf-8'))
150
+ const payload = buildSprintSummaryPayload(data)
151
+ result = await sendWebhook(payload)
152
+ } catch (e) {
153
+ result = { error: `Failed to read ${args.file}: ${e.message}` }
154
+ }
155
+ break
156
+ }
157
+
158
+ default:
159
+ result = {
160
+ error: cmd ? `Unknown command: ${cmd}` : 'No command provided',
161
+ usage: {
162
+ send: 'send --text "Hello *bold* _italic_" | send --blocks-file payload.json',
163
+ 'task-done': 'task-done --task T-001 --commit abc1234 --author "Name" [--sprint "S01"] [--track backend] [--message "feat: ..."]',
164
+ 'sprint-summary': 'sprint-summary --file sprint-status.json',
165
+ },
166
+ formatting: {
167
+ bold: '*text*',
168
+ italic: '_text_',
169
+ strike: '~text~',
170
+ code: '`code`',
171
+ codeBlock: '```multi-line```',
172
+ quote: '>quoted text',
173
+ link: '<url|display text>',
174
+ mention: '<@USER_ID>',
175
+ channel: '<#CHANNEL_ID>',
176
+ date: '<!date^UNIX_TS^{date_short} {time}|fallback>',
177
+ },
178
+ flags: {
179
+ '--dry-run': 'Show payload without sending',
180
+ }
181
+ }
182
+ }
183
+
184
+ console.log(JSON.stringify(result, null, 2))
185
+ }
186
+
187
+ main().catch(err => {
188
+ console.error(JSON.stringify({ error: err.message }))
189
+ process.exit(0) // Always exit 0 — notification failure should never block
190
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ganbatte-os",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Framework operacional para design-to-code, squads de entrega e sprint sync com ClickUp.",
5
5
  "bin": {
6
6
  "ganbatte-os": ".gos/scripts/cli/gos-cli.js",