ganbatte-os 0.2.6 → 0.2.8

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.
@@ -20,7 +20,10 @@ Ativar **PLAN MODE** automaticamente sempre que o input do usuário envolver **q
20
20
  - Git operations: `commit`, `push`, `pull`, `branch`, `merge`, `status`, `log`
21
21
  - Leitura e explicação: "o que é", "como funciona", "onde está", "mostre", "explique"
22
22
  - Edições simples: 1 arquivo, 1 mudança isolada (ex: corrigir typo, ajustar 1 linha)
23
+ - Correções de lint, typo, formatação, ortografia
23
24
  - Quando o usuário inclui: `"só faça"`, `"execute direto"`, `"sem plano"`, `"direto ao ponto"`
25
+ - Comandos de retomada ou continuação: `"continue"`, `"continuar"`, `"retomar"`, `"resume"`, `"prosseguir"`, `"segue"`
26
+ - Aprovações após um plano já apresentado: `"ok"`, `"aprovado"`, `"go"`, `"execute"`, `"pode ir"`, `"sim"`, `"proceed"`
24
27
  - Quando plan mode já foi ativado ou aprovado na sessão atual
25
28
 
26
29
  ---
@@ -37,7 +40,7 @@ Ativar **PLAN MODE** automaticamente sempre que o input do usuário envolver **q
37
40
  - Mudanças propostas agrupadas por componente
38
41
  - Arquivos a criar `[NEW]`, modificar `[MODIFY]`, deletar `[DELETE]`
39
42
  - Plano de verificação
40
- - **Apresentar o plano ao usuário e PARAR**
43
+ - **Apresentar o plano ao usuário e aguardar aprovação na resposta principal, sem bloquear via hook**
41
44
 
42
45
  ### Fase 3 — AWAIT APPROVAL
43
46
  - Aguardar aprovação explícita do usuário (`"ok"`, `"aprovado"`, `"go"`, etc.)
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
6
+ const ROOT = path.resolve(__dirname, "../../..");
7
+ const STATE_DIR = path.join(ROOT, ".claude", ".hook-state");
8
+
9
+ function readStdin() {
10
+ try {
11
+ return fs.readFileSync(0, "utf8");
12
+ } catch {
13
+ return "";
14
+ }
15
+ }
16
+
17
+ function parsePayload() {
18
+ const raw = readStdin().trim();
19
+ if (!raw) return {};
20
+ try {
21
+ return JSON.parse(raw);
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
27
+ function getSessionId(payload) {
28
+ return (
29
+ payload.session_id ||
30
+ payload.sessionId ||
31
+ payload.conversation_id ||
32
+ payload.conversationId ||
33
+ "default"
34
+ );
35
+ }
36
+
37
+ function ensureDir(dirPath) {
38
+ fs.mkdirSync(dirPath, { recursive: true });
39
+ }
40
+
41
+ function statePathFor(sessionId) {
42
+ return path.join(STATE_DIR, `${sessionId}.json`);
43
+ }
44
+
45
+ function readState(statePath, sessionId) {
46
+ if (!fs.existsSync(statePath)) {
47
+ return {
48
+ sessionId,
49
+ touchedFiles: [],
50
+ commands: [],
51
+ significantAction: false,
52
+ updatedAt: new Date().toISOString(),
53
+ };
54
+ }
55
+
56
+ try {
57
+ return JSON.parse(fs.readFileSync(statePath, "utf8"));
58
+ } catch {
59
+ return {
60
+ sessionId,
61
+ touchedFiles: [],
62
+ commands: [],
63
+ significantAction: false,
64
+ updatedAt: new Date().toISOString(),
65
+ };
66
+ }
67
+ }
68
+
69
+ function uniquePush(list, value) {
70
+ if (!value || list.includes(value)) return;
71
+ list.push(value);
72
+ }
73
+
74
+ function normalizePath(candidate) {
75
+ if (typeof candidate !== "string" || !candidate.trim()) return null;
76
+ const trimmed = candidate.trim();
77
+ const absolute = path.isAbsolute(trimmed) ? trimmed : path.join(ROOT, trimmed);
78
+ const normalized = path.normalize(absolute);
79
+ if (!normalized.startsWith(ROOT)) return null;
80
+ return path.relative(ROOT, normalized).replace(/\\/g, "/");
81
+ }
82
+
83
+ function collectPaths(payload) {
84
+ const args = payload.args || payload.arguments || {};
85
+ const candidates = [
86
+ args.file_path,
87
+ args.path,
88
+ args.target_file,
89
+ args.new_file_path,
90
+ payload.file_path,
91
+ payload.path,
92
+ ];
93
+
94
+ const result = [];
95
+ for (const candidate of candidates) {
96
+ const normalized = normalizePath(candidate);
97
+ if (normalized) uniquePush(result, normalized);
98
+ }
99
+ return result;
100
+ }
101
+
102
+ function extractCommand(payload) {
103
+ const args = payload.args || payload.arguments || {};
104
+ return (
105
+ args.command ||
106
+ args.cmd ||
107
+ payload.command ||
108
+ payload.cmd ||
109
+ ""
110
+ ).trim();
111
+ }
112
+
113
+ function main() {
114
+ const payload = parsePayload();
115
+ const sessionId = getSessionId(payload);
116
+ const statePath = statePathFor(sessionId);
117
+
118
+ ensureDir(STATE_DIR);
119
+ const state = readState(statePath, sessionId);
120
+
121
+ for (const filePath of collectPaths(payload)) {
122
+ uniquePush(state.touchedFiles, filePath);
123
+ }
124
+
125
+ const toolName = String(payload.tool || payload.tool_name || payload.matcher || "");
126
+ if (/Bash/i.test(toolName)) {
127
+ const command = extractCommand(payload);
128
+ if (command) uniquePush(state.commands, command);
129
+ }
130
+
131
+ if (state.touchedFiles.length > 0 || state.commands.length > 0) {
132
+ state.significantAction = true;
133
+ }
134
+
135
+ state.updatedAt = new Date().toISOString();
136
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
137
+ }
138
+
139
+ try {
140
+ main();
141
+ } catch {
142
+ // observation hook: never block
143
+ }
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { execFileSync } = require("node:child_process");
6
+
7
+ const ROOT = path.resolve(__dirname, "../../..");
8
+ const STATE_DIR = path.join(ROOT, ".claude", ".hook-state");
9
+
10
+ function readStdin() {
11
+ try {
12
+ return fs.readFileSync(0, "utf8");
13
+ } catch {
14
+ return "";
15
+ }
16
+ }
17
+
18
+ function parsePayload() {
19
+ const raw = readStdin().trim();
20
+ if (!raw) return {};
21
+ try {
22
+ return JSON.parse(raw);
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+
28
+ function getSessionId(payload) {
29
+ return (
30
+ payload.session_id ||
31
+ payload.sessionId ||
32
+ payload.conversation_id ||
33
+ payload.conversationId ||
34
+ "default"
35
+ );
36
+ }
37
+
38
+ function readState(sessionId) {
39
+ const statePath = path.join(STATE_DIR, `${sessionId}.json`);
40
+ if (!fs.existsSync(statePath)) {
41
+ return { statePath, state: null };
42
+ }
43
+ try {
44
+ return {
45
+ statePath,
46
+ state: JSON.parse(fs.readFileSync(statePath, "utf8")),
47
+ };
48
+ } catch {
49
+ return { statePath, state: null };
50
+ }
51
+ }
52
+
53
+ function safeUnlink(filePath) {
54
+ try {
55
+ fs.unlinkSync(filePath);
56
+ } catch {
57
+ // ignore cleanup issues
58
+ }
59
+ }
60
+
61
+ function runNpm(args, options = {}) {
62
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
63
+ return execFileSync(npmCmd, args, {
64
+ cwd: ROOT,
65
+ encoding: "utf8",
66
+ stdio: ["pipe", "pipe", "pipe"],
67
+ timeout: options.timeout || 120000,
68
+ });
69
+ }
70
+
71
+ function getGitStatusMap(paths) {
72
+ const map = new Map();
73
+ if (!Array.isArray(paths) || paths.length === 0) return map;
74
+
75
+ try {
76
+ const output = execFileSync("git", ["status", "--porcelain", "--", ...paths], {
77
+ cwd: ROOT,
78
+ encoding: "utf8",
79
+ stdio: ["pipe", "pipe", "pipe"],
80
+ timeout: 15000,
81
+ });
82
+
83
+ for (const line of output.split(/\r?\n/)) {
84
+ if (!line.trim()) continue;
85
+ const status = line.slice(0, 2);
86
+ const filePath = line.slice(3).trim().replace(/\\/g, "/");
87
+ map.set(filePath, status);
88
+ }
89
+ } catch {
90
+ // ignore git issues
91
+ }
92
+
93
+ return map;
94
+ }
95
+
96
+ function anyPathMatches(paths, matcher) {
97
+ return paths.some((filePath) => matcher.test(filePath));
98
+ }
99
+
100
+ function anyCreatedOrRemoved(statusMap, matcher) {
101
+ for (const [filePath, status] of statusMap.entries()) {
102
+ if (!matcher.test(filePath)) continue;
103
+ if (status.includes("A") || status.includes("D") || status.includes("R") || status === "??") {
104
+ return true;
105
+ }
106
+ }
107
+ return false;
108
+ }
109
+
110
+ function main() {
111
+ const payload = parsePayload();
112
+ const sessionId = getSessionId(payload);
113
+ const { statePath, state } = readState(sessionId);
114
+
115
+ if (!state || !state.significantAction) {
116
+ safeUnlink(statePath);
117
+ return;
118
+ }
119
+
120
+ const touchedFiles = Array.isArray(state.touchedFiles) ? state.touchedFiles : [];
121
+ const statusMap = getGitStatusMap(touchedFiles);
122
+ const summary = [];
123
+ const syncMatcher = /^(\.gos|\.claude|data|README\.md|CLAUDE\.md|AGENTS\.md|GEMINI\.md)/;
124
+
125
+ if (anyPathMatches(touchedFiles, syncMatcher)) {
126
+ try {
127
+ runNpm(["run", "sync:ides"], { timeout: 180000 });
128
+ summary.push("sync:ides OK");
129
+
130
+ if (anyCreatedOrRemoved(statusMap, /^(\.gos|\.claude)\//)) {
131
+ runNpm(["run", "doctor"], { timeout: 180000 });
132
+ summary.push("doctor OK");
133
+ }
134
+ } catch (error) {
135
+ const message = error.stderr || error.stdout || error.message || "falha no sync";
136
+ summary.push(`sync:ides falhou (${String(message).split(/\r?\n/)[0]})`);
137
+ }
138
+ }
139
+
140
+ safeUnlink(statePath);
141
+
142
+ if (summary.length > 0) {
143
+ process.stdout.write(`${summary.join("; ")}\n`);
144
+ }
145
+ }
146
+
147
+ try {
148
+ main();
149
+ } catch {
150
+ // observation hook: never block
151
+ }
package/AGENTS.md CHANGED
@@ -39,8 +39,8 @@ Antes de executar qualquer tarefa complexa, SEMPRE entre em plan mode.
39
39
 
40
40
  **Protocolo:**
41
41
  1. **RESEARCH** — leia arquivos relevantes sem alterar nada
42
- 2. **PLAN** — crie `implementation_plan.md` com `[NEW]`/`[MODIFY]`/`[DELETE]`, perguntas abertas, plano de verificação. Apresente e **PARE**
42
+ 2. **PLAN** — crie `implementation_plan.md` com `[NEW]`/`[MODIFY]`/`[DELETE]`, perguntas abertas, plano de verificação. Apresente e aguarde aprovação na resposta principal, sem bloquear via hook
43
43
  3. **AWAIT** — aguarde aprovação: "ok", "aprovado", "go", "execute", "pode ir"
44
44
  4. **EXECUTE + TRACK** — crie `task.md`, execute, finalize com `walkthrough.md`
45
45
 
46
- **Exceções — NÃO ativar:** git operations, leitura/explicação, edições simples de 1 arquivo, lint/typo, "execute direto", "sem plano", quando já aprovado na sessão.
46
+ **Exceções — NÃO ativar:** git operations, leitura/explicação, edições simples de 1 arquivo, lint/typo, "execute direto", "sem plano", comandos de retomada como `continue`/`continuar`/`resume`, aprovações como `ok`/`aprovado`/`pode ir`, quando já aprovado na sessão.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ganbatte-os",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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",