@tinygem/soda-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +40 -0
- package/dist/auth.js.map +1 -0
- package/dist/circuit-breaker.d.ts +5 -0
- package/dist/circuit-breaker.js +23 -0
- package/dist/circuit-breaker.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +224 -0
- package/dist/cli.js.map +1 -0
- package/dist/executor.d.ts +3 -0
- package/dist/executor.js +146 -0
- package/dist/executor.js.map +1 -0
- package/dist/external-help.d.ts +5 -0
- package/dist/external-help.js +78 -0
- package/dist/external-help.js.map +1 -0
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +49 -0
- package/dist/logger.js.map +1 -0
- package/dist/orchestrator.d.ts +11 -0
- package/dist/orchestrator.js +204 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/phases.d.ts +4 -0
- package/dist/phases.js +139 -0
- package/dist/phases.js.map +1 -0
- package/dist/prompts.d.ts +7 -0
- package/dist/prompts.js +140 -0
- package/dist/prompts.js.map +1 -0
- package/dist/state.d.ts +12 -0
- package/dist/state.js +209 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/package.json +31 -0
- package/plugin/.claude-plugin/plugin.json +11 -0
- package/plugin/commands/soda-cancel.md +23 -0
- package/plugin/commands/soda-status.md +31 -0
- package/plugin/skills/soda/SKILL.md +72 -0
package/dist/state.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// 상태 저장/복원 - .soda/state.json
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Phase, SCHEMA_VERSION, getErrorMessage } from "./types.js";
|
|
5
|
+
import { logWarn } from "./logger.js";
|
|
6
|
+
const STATE_DIR = ".soda";
|
|
7
|
+
const STATE_FILE = "state.json";
|
|
8
|
+
const PID_FILE = "engine.pid";
|
|
9
|
+
const CANCEL_FILE = "cancel";
|
|
10
|
+
const CLARIFY_PENDING_FILE = "clarify-pending.json";
|
|
11
|
+
const CLARIFY_ANSWER_FILE = "clarify-answer.txt";
|
|
12
|
+
const MAX_FAILURE_HISTORY = 10;
|
|
13
|
+
export function createInitialState(goal) {
|
|
14
|
+
return {
|
|
15
|
+
schemaVersion: SCHEMA_VERSION,
|
|
16
|
+
goal,
|
|
17
|
+
goalConfirmed: null,
|
|
18
|
+
phase: Phase.CLARIFY,
|
|
19
|
+
verifyPass: 0,
|
|
20
|
+
iterations: 0,
|
|
21
|
+
consecutiveFailures: 0,
|
|
22
|
+
failureHistory: [],
|
|
23
|
+
startTime: Date.now(),
|
|
24
|
+
implementationSummary: null,
|
|
25
|
+
lastVerifyResult: null,
|
|
26
|
+
clarifyRetries: 0,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// #7: 런타임 필드 검증 — unsafe cast 대신 모든 필드를 확인
|
|
30
|
+
function isValidGoalConfirmed(gc) {
|
|
31
|
+
if (gc === null)
|
|
32
|
+
return true; // null은 유효 (CLARIFY 전)
|
|
33
|
+
if (typeof gc !== "object" || gc === null)
|
|
34
|
+
return false;
|
|
35
|
+
const obj = gc;
|
|
36
|
+
return (typeof obj.summary === "string" &&
|
|
37
|
+
Array.isArray(obj.tasks) &&
|
|
38
|
+
obj.tasks.length > 0 &&
|
|
39
|
+
obj.tasks.every((t) => typeof t === "string") &&
|
|
40
|
+
Array.isArray(obj.completionCriteria) &&
|
|
41
|
+
obj.completionCriteria.length > 0 &&
|
|
42
|
+
obj.completionCriteria.every((c) => typeof c === "string"));
|
|
43
|
+
}
|
|
44
|
+
function isValidClarifyMessages(msgs) {
|
|
45
|
+
if (msgs === undefined)
|
|
46
|
+
return true; // optional 필드
|
|
47
|
+
if (!Array.isArray(msgs))
|
|
48
|
+
return false;
|
|
49
|
+
return msgs.every((m) => {
|
|
50
|
+
if (typeof m !== "object" || m === null)
|
|
51
|
+
return false;
|
|
52
|
+
const msg = m;
|
|
53
|
+
return (msg.role === "assistant" || msg.role === "user") && typeof msg.content === "string";
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function isValidRunState(data) {
|
|
57
|
+
return (data.schemaVersion === SCHEMA_VERSION &&
|
|
58
|
+
typeof data.goal === "string" &&
|
|
59
|
+
typeof data.phase === "string" &&
|
|
60
|
+
Object.values(Phase).includes(data.phase) &&
|
|
61
|
+
typeof data.verifyPass === "number" &&
|
|
62
|
+
typeof data.iterations === "number" &&
|
|
63
|
+
typeof data.consecutiveFailures === "number" &&
|
|
64
|
+
Array.isArray(data.failureHistory) &&
|
|
65
|
+
typeof data.startTime === "number" &&
|
|
66
|
+
isValidGoalConfirmed(data.goalConfirmed) &&
|
|
67
|
+
isValidClarifyMessages(data.clarifyMessages));
|
|
68
|
+
}
|
|
69
|
+
export function loadState(workingDir) {
|
|
70
|
+
const path = join(workingDir, STATE_DIR, STATE_FILE);
|
|
71
|
+
if (!existsSync(path))
|
|
72
|
+
return null;
|
|
73
|
+
try {
|
|
74
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
75
|
+
if (data.schemaVersion !== undefined && data.schemaVersion !== SCHEMA_VERSION) {
|
|
76
|
+
logWarn(`상태 파일 버전 불일치 (파일: ${data.schemaVersion}, 현재: ${SCHEMA_VERSION}). 이전 상태를 사용할 수 없습니다.`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (!isValidRunState(data))
|
|
80
|
+
return null;
|
|
81
|
+
return data;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// #11: saveState는 입력 객체를 변이하지 않는다
|
|
88
|
+
// #19: existsSync + mkdirSync 중복 제거
|
|
89
|
+
export function saveState(workingDir, state) {
|
|
90
|
+
const dir = join(workingDir, STATE_DIR);
|
|
91
|
+
mkdirSync(dir, { recursive: true });
|
|
92
|
+
// failureHistory 길이 제한 — 원본을 변이하지 않고 직렬화 시점에 자름
|
|
93
|
+
const trimmedHistory = state.failureHistory.length > MAX_FAILURE_HISTORY
|
|
94
|
+
? state.failureHistory.slice(-MAX_FAILURE_HISTORY)
|
|
95
|
+
: state.failureHistory;
|
|
96
|
+
const json = JSON.stringify({ ...state, failureHistory: trimmedHistory }, null, 2);
|
|
97
|
+
// 원자적 쓰기: temp 파일에 쓴 후 rename
|
|
98
|
+
const filePath = join(dir, STATE_FILE);
|
|
99
|
+
const tmpPath = filePath + ".tmp";
|
|
100
|
+
writeFileSync(tmpPath, json);
|
|
101
|
+
renameSync(tmpPath, filePath);
|
|
102
|
+
}
|
|
103
|
+
export function clearState(workingDir) {
|
|
104
|
+
const path = join(workingDir, STATE_DIR, STATE_FILE);
|
|
105
|
+
if (existsSync(path)) {
|
|
106
|
+
unlinkSync(path);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function writePidFile(workingDir) {
|
|
110
|
+
const dir = join(workingDir, STATE_DIR);
|
|
111
|
+
mkdirSync(dir, { recursive: true });
|
|
112
|
+
const pidPath = join(dir, PID_FILE);
|
|
113
|
+
try {
|
|
114
|
+
writeFileSync(pidPath, process.pid.toString(), { flag: "wx" });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// 파일이 이미 존재하면 덮어쓰기 (stale PID 파일 처리)
|
|
118
|
+
writeFileSync(pidPath, process.pid.toString());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function cleanupPidFile(workingDir) {
|
|
122
|
+
const path = join(workingDir, STATE_DIR, PID_FILE);
|
|
123
|
+
try {
|
|
124
|
+
unlinkSync(path);
|
|
125
|
+
}
|
|
126
|
+
catch { /* 파일 없으면 무시 */ }
|
|
127
|
+
}
|
|
128
|
+
// 동시 실행 보호 — PID 파일로 다른 인스턴스가 실행 중인지 확인
|
|
129
|
+
export function isLocked(workingDir) {
|
|
130
|
+
const pidPath = join(workingDir, STATE_DIR, PID_FILE);
|
|
131
|
+
if (!existsSync(pidPath))
|
|
132
|
+
return false;
|
|
133
|
+
try {
|
|
134
|
+
const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
135
|
+
if (!Number.isFinite(pid))
|
|
136
|
+
return false;
|
|
137
|
+
if (pid === process.pid)
|
|
138
|
+
return false; // 자기 자신은 잠금 대상 아님
|
|
139
|
+
process.kill(pid, 0);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
// EPERM: 프로세스 존재하지만 다른 사용자 소유 → 잠금
|
|
144
|
+
if (e && typeof e === "object" && e.code === "EPERM") {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
// ESRCH: 프로세스 없음 → 잠금 해제
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export function isCancelled(workingDir) {
|
|
152
|
+
const path = join(workingDir, STATE_DIR, CANCEL_FILE);
|
|
153
|
+
try {
|
|
154
|
+
unlinkSync(path); // 존재하면 삭제 후 true, ENOENT면 catch
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// CLARIFY 대화: 답변 파일 읽기 + 정리
|
|
162
|
+
export function readClarifyAnswer(workingDir) {
|
|
163
|
+
const answerPath = join(workingDir, STATE_DIR, CLARIFY_ANSWER_FILE);
|
|
164
|
+
if (!existsSync(answerPath))
|
|
165
|
+
return null;
|
|
166
|
+
try {
|
|
167
|
+
const answer = readFileSync(answerPath, "utf-8").trim();
|
|
168
|
+
unlinkSync(answerPath);
|
|
169
|
+
const pendingPath = join(workingDir, STATE_DIR, CLARIFY_PENDING_FILE);
|
|
170
|
+
try {
|
|
171
|
+
unlinkSync(pendingPath);
|
|
172
|
+
}
|
|
173
|
+
catch { /* 없으면 무시 */ }
|
|
174
|
+
return answer || null;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// CLARIFY 대화: 질문 파일 작성 (plugin 모드)
|
|
181
|
+
export function writeClarifyPending(workingDir, question) {
|
|
182
|
+
const dir = join(workingDir, STATE_DIR);
|
|
183
|
+
mkdirSync(dir, { recursive: true });
|
|
184
|
+
const pendingPath = join(dir, CLARIFY_PENDING_FILE);
|
|
185
|
+
writeFileSync(pendingPath, JSON.stringify({ question, timestamp: Date.now() }, null, 2));
|
|
186
|
+
}
|
|
187
|
+
// .soda/가 .gitignore에 없으면 추가
|
|
188
|
+
export function ensureSodaIgnored(workingDir) {
|
|
189
|
+
const gitignorePath = join(workingDir, ".gitignore");
|
|
190
|
+
if (!existsSync(gitignorePath)) {
|
|
191
|
+
logWarn(".gitignore가 없습니다. .soda/ 디렉토리가 버전 관리에 포함될 수 있습니다.");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
196
|
+
const lines = content.split("\n");
|
|
197
|
+
const hasSodaEntry = lines.some((line) => {
|
|
198
|
+
const trimmed = line.trim();
|
|
199
|
+
return trimmed === ".soda" || trimmed === ".soda/" || trimmed === "/.soda" || trimmed === "/.soda/";
|
|
200
|
+
});
|
|
201
|
+
if (hasSodaEntry)
|
|
202
|
+
return;
|
|
203
|
+
appendFileSync(gitignorePath, "\n# soda agent state\n.soda/\n");
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
logWarn(`gitignore 업데이트 실패: ${getErrorMessage(e)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export declare const SCHEMA_VERSION = 4;
|
|
2
|
+
export declare enum Phase {
|
|
3
|
+
CLARIFY = "clarify",
|
|
4
|
+
IMPLEMENT = "implement",
|
|
5
|
+
VERIFY = "verify",
|
|
6
|
+
DONE = "done"
|
|
7
|
+
}
|
|
8
|
+
export interface GoalConfirmed {
|
|
9
|
+
summary: string;
|
|
10
|
+
tasks: string[];
|
|
11
|
+
completionCriteria: string[];
|
|
12
|
+
isRefactoring?: boolean;
|
|
13
|
+
isUIWork?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ClarifyMessage {
|
|
16
|
+
role: "assistant" | "user";
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
export interface RunState {
|
|
20
|
+
schemaVersion: number;
|
|
21
|
+
goal: string;
|
|
22
|
+
goalConfirmed: GoalConfirmed | null;
|
|
23
|
+
phase: Phase;
|
|
24
|
+
verifyPass: number;
|
|
25
|
+
iterations: number;
|
|
26
|
+
consecutiveFailures: number;
|
|
27
|
+
failureHistory: string[];
|
|
28
|
+
startTime: number;
|
|
29
|
+
implementationSummary: string | null;
|
|
30
|
+
lastVerifyResult: string | null;
|
|
31
|
+
clarifyRetries?: number;
|
|
32
|
+
clarifyMessages?: ClarifyMessage[];
|
|
33
|
+
}
|
|
34
|
+
export interface Limits {
|
|
35
|
+
maxIterations: number;
|
|
36
|
+
maxTimeMinutes: number;
|
|
37
|
+
maxConsecutiveFails: number;
|
|
38
|
+
}
|
|
39
|
+
export declare const DEFAULT_LIMITS: Limits;
|
|
40
|
+
export interface PhaseConfig {
|
|
41
|
+
phase: Phase;
|
|
42
|
+
prompt: string;
|
|
43
|
+
systemPromptAppend: string;
|
|
44
|
+
allowedTools?: string[];
|
|
45
|
+
skipPermissions: boolean;
|
|
46
|
+
cwd: string;
|
|
47
|
+
}
|
|
48
|
+
export interface PhaseResult {
|
|
49
|
+
text: string;
|
|
50
|
+
sessionId: string;
|
|
51
|
+
}
|
|
52
|
+
export interface PhaseTransition {
|
|
53
|
+
nextPhase: Phase;
|
|
54
|
+
verdict: {
|
|
55
|
+
pass: boolean;
|
|
56
|
+
reason: string;
|
|
57
|
+
} | null;
|
|
58
|
+
}
|
|
59
|
+
export declare function getErrorMessage(e: unknown): string;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// soda 전체에서 사용되는 타입 정의
|
|
2
|
+
export const SCHEMA_VERSION = 4;
|
|
3
|
+
export var Phase;
|
|
4
|
+
(function (Phase) {
|
|
5
|
+
Phase["CLARIFY"] = "clarify";
|
|
6
|
+
Phase["IMPLEMENT"] = "implement";
|
|
7
|
+
Phase["VERIFY"] = "verify";
|
|
8
|
+
Phase["DONE"] = "done";
|
|
9
|
+
})(Phase || (Phase = {}));
|
|
10
|
+
export const DEFAULT_LIMITS = {
|
|
11
|
+
maxIterations: 50,
|
|
12
|
+
maxTimeMinutes: 180,
|
|
13
|
+
maxConsecutiveFails: 10,
|
|
14
|
+
};
|
|
15
|
+
export function getErrorMessage(e) {
|
|
16
|
+
return e instanceof Error ? e.message : String(e);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,uBAAuB;AAEvB,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC,MAAM,CAAN,IAAY,KAKX;AALD,WAAY,KAAK;IACf,4BAAmB,CAAA;IACnB,gCAAuB,CAAA;IACvB,0BAAiB,CAAA;IACjB,sBAAa,CAAA;AACf,CAAC,EALW,KAAK,KAAL,KAAK,QAKhB;AAqCD,MAAM,CAAC,MAAM,cAAc,GAAW;IACpC,aAAa,EAAE,EAAE;IACjB,cAAc,EAAE,GAAG;IACnB,mBAAmB,EAAE,EAAE;CACxB,CAAC;AAqBF,MAAM,UAAU,eAAe,CAAC,CAAU;IACxC,OAAO,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tinygem/soda-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "자율 개발 에이전트 - Claude Code 기반으로 태스크를 끝까지 완수한다",
|
|
5
|
+
"bin": {
|
|
6
|
+
"soda-agent": "dist/cli.js",
|
|
7
|
+
"soda": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"plugin/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"pretest": "npm run build",
|
|
16
|
+
"test": "node --test test/*.test.js",
|
|
17
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.7.0",
|
|
21
|
+
"@types/node": "^22.0.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"license": "UNLICENSED",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "soda",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Autonomous development agent - completes tasks end-to-end via fresh Claude sessions per phase",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "soda"
|
|
7
|
+
},
|
|
8
|
+
"commands": "./commands/",
|
|
9
|
+
"skills": "./skills/",
|
|
10
|
+
"keywords": ["autonomous", "agent", "development", "verification"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: soda-cancel
|
|
3
|
+
description: Cancel a running soda agent gracefully
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Cancel the running soda engine by creating a cancel sentinel file.
|
|
7
|
+
|
|
8
|
+
Run this command via Bash:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
if [ -f .soda/engine.pid ]; then
|
|
12
|
+
PID=$(cat .soda/engine.pid)
|
|
13
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
14
|
+
touch .soda/cancel
|
|
15
|
+
echo "Cancel signal sent to PID $PID. soda will stop after the current phase completes."
|
|
16
|
+
echo "State is saved — you can resume later with: npx @tinygem/soda-agent --resume"
|
|
17
|
+
else
|
|
18
|
+
echo "soda engine (PID $PID) is no longer running. No cancel needed."
|
|
19
|
+
fi
|
|
20
|
+
else
|
|
21
|
+
echo "No running soda session found."
|
|
22
|
+
fi
|
|
23
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: soda-status
|
|
3
|
+
description: Check the current status of a running soda agent
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Check the soda engine status by reading its state and process information.
|
|
7
|
+
|
|
8
|
+
Run this command via Bash:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
if [ -f .soda/state.json ]; then
|
|
12
|
+
echo "=== soda state ==="
|
|
13
|
+
cat .soda/state.json
|
|
14
|
+
echo ""
|
|
15
|
+
if [ -f .soda/engine.pid ]; then
|
|
16
|
+
PID=$(cat .soda/engine.pid)
|
|
17
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
18
|
+
echo "STATUS: running (PID $PID)"
|
|
19
|
+
else
|
|
20
|
+
echo "STATUS: finished"
|
|
21
|
+
fi
|
|
22
|
+
else
|
|
23
|
+
echo "STATUS: no PID file (engine may have completed)"
|
|
24
|
+
fi
|
|
25
|
+
else
|
|
26
|
+
echo "STATUS: no soda session found"
|
|
27
|
+
fi
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Report to the user: current phase, iteration count, consecutive failures, and whether
|
|
31
|
+
the engine is still running.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: soda
|
|
3
|
+
description: Launch the soda autonomous development agent to complete a complex task end-to-end with multi-phase verification. Use when the user wants fully autonomous implementation with quality guarantees.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# soda Autonomous Agent
|
|
8
|
+
|
|
9
|
+
soda runs as a background process that spawns fresh `claude -p` sessions for each phase
|
|
10
|
+
(CLARIFY -> IMPLEMENT -> VERIFY), preventing context degradation.
|
|
11
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
- Claude Code가 설치되어 있어야 합니다.
|
|
15
|
+
- `claude --dangerously-skip-permissions` 를 1회 실행하여 약관에 동의해야 합니다. soda는 내부적으로 이 플래그를 사용하여 자율 실행합니다.
|
|
16
|
+
- root 사용자로는 실행할 수 없습니다 (Claude Code 제한).
|
|
17
|
+
|
|
18
|
+
## Instructions
|
|
19
|
+
|
|
20
|
+
1. If the user provides no task description, ask them what they want soda to do before launching.
|
|
21
|
+
|
|
22
|
+
2. Launch soda in the background using the Bash tool:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @tinygem/soda-agent --plugin-mode "$ARGUMENTS" > .soda/output.log 2>&1
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use `run_in_background: true` on the Bash tool call.
|
|
29
|
+
|
|
30
|
+
3. Tell the user that soda has been launched and is running in the background.
|
|
31
|
+
|
|
32
|
+
4. To check progress, read the state file:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
if [ -f .soda/state.json ]; then
|
|
36
|
+
cat .soda/state.json
|
|
37
|
+
echo ""
|
|
38
|
+
if [ -f .soda/engine.pid ]; then
|
|
39
|
+
PID=$(cat .soda/engine.pid 2>/dev/null)
|
|
40
|
+
[ -n "$PID" ] && kill -0 "$PID" 2>/dev/null && echo "STATUS: running" || echo "STATUS: finished"
|
|
41
|
+
fi
|
|
42
|
+
if [ -f .soda/clarify-pending.json ]; then
|
|
43
|
+
echo "CLARIFY_PENDING: true"
|
|
44
|
+
cat .soda/clarify-pending.json
|
|
45
|
+
fi
|
|
46
|
+
else
|
|
47
|
+
echo "No soda session found."
|
|
48
|
+
fi
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
5. Report the current phase, iteration count, and status to the user.
|
|
52
|
+
|
|
53
|
+
6. **CLARIFY 대화 처리**: soda가 종료되고 `.soda/clarify-pending.json`이 존재하면:
|
|
54
|
+
- 파일의 `question` 내용을 사용자에게 보여준다.
|
|
55
|
+
- 사용자의 답변을 받는다.
|
|
56
|
+
- 답변을 `.soda/clarify-answer.txt`에 기록한다:
|
|
57
|
+
```bash
|
|
58
|
+
echo "사용자의 답변 내용" > .soda/clarify-answer.txt
|
|
59
|
+
```
|
|
60
|
+
- soda를 재개한다:
|
|
61
|
+
```bash
|
|
62
|
+
npx @tinygem/soda-agent --plugin-mode --resume > .soda/output.log 2>&1
|
|
63
|
+
```
|
|
64
|
+
Use `run_in_background: true` on the Bash tool call.
|
|
65
|
+
|
|
66
|
+
## Important Rules
|
|
67
|
+
|
|
68
|
+
- Always run soda in the background. It can take a long time.
|
|
69
|
+
- The engine writes progress to `.soda/state.json` after each phase.
|
|
70
|
+
- If the user asks to stop, use `/soda-cancel`.
|
|
71
|
+
- When finished, `.soda/state.json` will show `"phase": "done"`.
|
|
72
|
+
- To resume a stopped session: `npx @tinygem/soda-agent --plugin-mode --resume`
|