agentic-loop 3.13.0 → 3.14.2
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/.claude/skills/idea/SKILL.md +56 -0
- package/.claude/skills/loopgram/SKILL.md +19 -0
- package/.claude/skills/prd/SKILL.md +2 -0
- package/README.md +1 -0
- package/bin/ralph.sh +17 -11
- package/dist/loopgram/claude.d.ts +18 -0
- package/dist/loopgram/claude.d.ts.map +1 -0
- package/dist/loopgram/claude.js +89 -0
- package/dist/loopgram/claude.js.map +1 -0
- package/dist/loopgram/context-search.d.ts +26 -0
- package/dist/loopgram/context-search.d.ts.map +1 -0
- package/dist/loopgram/context-search.js +175 -0
- package/dist/loopgram/context-search.js.map +1 -0
- package/dist/loopgram/conversation.d.ts +39 -0
- package/dist/loopgram/conversation.d.ts.map +1 -0
- package/dist/loopgram/conversation.js +158 -0
- package/dist/loopgram/conversation.js.map +1 -0
- package/dist/loopgram/index.d.ts +3 -0
- package/dist/loopgram/index.d.ts.map +1 -0
- package/dist/loopgram/index.js +246 -0
- package/dist/loopgram/index.js.map +1 -0
- package/dist/loopgram/loop-monitor.d.ts +16 -0
- package/dist/loopgram/loop-monitor.d.ts.map +1 -0
- package/dist/loopgram/loop-monitor.js +149 -0
- package/dist/loopgram/loop-monitor.js.map +1 -0
- package/dist/loopgram/loop-runner.d.ts +28 -0
- package/dist/loopgram/loop-runner.d.ts.map +1 -0
- package/dist/loopgram/loop-runner.js +157 -0
- package/dist/loopgram/loop-runner.js.map +1 -0
- package/dist/loopgram/prd-generator.d.ts +37 -0
- package/dist/loopgram/prd-generator.d.ts.map +1 -0
- package/dist/loopgram/prd-generator.js +134 -0
- package/dist/loopgram/prd-generator.js.map +1 -0
- package/dist/loopgram/saver.d.ts +9 -0
- package/dist/loopgram/saver.d.ts.map +1 -0
- package/dist/loopgram/saver.js +35 -0
- package/dist/loopgram/saver.js.map +1 -0
- package/dist/loopgram/types.d.ts +37 -0
- package/dist/loopgram/types.d.ts.map +1 -0
- package/dist/loopgram/types.js +5 -0
- package/dist/loopgram/types.js.map +1 -0
- package/package.json +6 -2
- package/ralph/hooks/common.sh +89 -0
- package/ralph/hooks/warn-debug.sh +14 -32
- package/ralph/hooks/warn-empty-catch.sh +13 -29
- package/ralph/hooks/warn-secrets.sh +19 -37
- package/ralph/hooks/warn-urls.sh +17 -33
- package/ralph/loop.sh +5 -2
- package/ralph/prd-check.sh +35 -8
- package/ralph/setup/quick-setup.sh +25 -12
- package/ralph/setup/ui.sh +0 -42
- package/ralph/setup.sh +71 -46
- package/ralph/utils.sh +167 -31
- package/templates/config/fastmcp.json +6 -1
- package/templates/config/fullstack.json +8 -0
- package/templates/config/node.json +8 -0
- package/templates/config/python.json +8 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { existsSync, writeFileSync, readFileSync, unlinkSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
const PID_FILE = '.ralph/loop.pid';
|
|
5
|
+
/**
|
|
6
|
+
* Check if a Ralph loop is currently running for a project
|
|
7
|
+
*/
|
|
8
|
+
export function isLoopRunning(projectPath) {
|
|
9
|
+
const pidFile = join(projectPath, PID_FILE);
|
|
10
|
+
if (!existsSync(pidFile)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const pid = parseInt(readFileSync(pidFile, 'utf-8').trim());
|
|
15
|
+
// Check if process is still running
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Process not running, clean up stale PID file
|
|
21
|
+
try {
|
|
22
|
+
unlinkSync(pidFile);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Ignore cleanup errors
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Start a Ralph loop for a project
|
|
32
|
+
*/
|
|
33
|
+
export function startLoop(projectPath) {
|
|
34
|
+
// Check if PRD exists
|
|
35
|
+
const prdPath = join(projectPath, '.ralph/prd.json');
|
|
36
|
+
if (!existsSync(prdPath)) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
message: 'No PRD found. Use /prd to create stories first.',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Check if already running
|
|
43
|
+
if (isLoopRunning(projectPath)) {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
message: 'Loop already running. Use /loop to check status.',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
// Start Ralph loop in background
|
|
51
|
+
const logFile = join(projectPath, '.ralph/loop.log');
|
|
52
|
+
// Use npx agentic-loop run
|
|
53
|
+
const child = spawn('npx', ['agentic-loop', 'run'], {
|
|
54
|
+
cwd: projectPath,
|
|
55
|
+
detached: true,
|
|
56
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
// Ensure Claude runs non-interactively
|
|
60
|
+
CLAUDE_CODE_ENTRYPOINT: 'cli',
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
// Write output to log file
|
|
64
|
+
const logStream = require('fs').createWriteStream(logFile, { flags: 'a' });
|
|
65
|
+
child.stdout?.pipe(logStream);
|
|
66
|
+
child.stderr?.pipe(logStream);
|
|
67
|
+
// Save PID
|
|
68
|
+
const pidFile = join(projectPath, PID_FILE);
|
|
69
|
+
writeFileSync(pidFile, child.pid.toString());
|
|
70
|
+
// Detach child process
|
|
71
|
+
child.unref();
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
message: `Loop started (PID: ${child.pid}). Use /loop to check progress.`,
|
|
75
|
+
pid: child.pid,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
message: `Failed to start loop: ${error}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Stop a running Ralph loop
|
|
87
|
+
*/
|
|
88
|
+
export function stopLoop(projectPath) {
|
|
89
|
+
const pidFile = join(projectPath, PID_FILE);
|
|
90
|
+
if (!existsSync(pidFile)) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
message: 'No loop running.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const pid = parseInt(readFileSync(pidFile, 'utf-8').trim());
|
|
98
|
+
// Kill the process and its children
|
|
99
|
+
try {
|
|
100
|
+
// Kill process group (negative PID)
|
|
101
|
+
process.kill(-pid, 'SIGTERM');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Try killing just the process
|
|
105
|
+
process.kill(pid, 'SIGTERM');
|
|
106
|
+
}
|
|
107
|
+
// Clean up PID file
|
|
108
|
+
unlinkSync(pidFile);
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
message: 'Loop stopped.',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
// Clean up PID file even if kill failed
|
|
116
|
+
try {
|
|
117
|
+
unlinkSync(pidFile);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Ignore
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
message: `Error stopping loop: ${error}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get loop process info
|
|
130
|
+
*/
|
|
131
|
+
export function getLoopInfo(projectPath) {
|
|
132
|
+
const pidFile = join(projectPath, PID_FILE);
|
|
133
|
+
const logFile = join(projectPath, '.ralph/loop.log');
|
|
134
|
+
const running = isLoopRunning(projectPath);
|
|
135
|
+
let pid;
|
|
136
|
+
let logTail;
|
|
137
|
+
if (existsSync(pidFile)) {
|
|
138
|
+
try {
|
|
139
|
+
pid = parseInt(readFileSync(pidFile, 'utf-8').trim());
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Ignore
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (existsSync(logFile)) {
|
|
146
|
+
try {
|
|
147
|
+
// Get last 500 chars of log
|
|
148
|
+
const content = readFileSync(logFile, 'utf-8');
|
|
149
|
+
logTail = content.slice(-500);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Ignore
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { running, pid, logTail };
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=loop-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop-runner.js","sourceRoot":"","sources":["../../src/loopgram/loop-runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Message } from './types.js';
|
|
2
|
+
interface Story {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
acceptanceCriteria: string[];
|
|
7
|
+
testSteps: string[];
|
|
8
|
+
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generate PRD stories from a conversation
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateStories(conversation: Message[], context: string | undefined, model: string): Promise<{
|
|
14
|
+
featureName: string;
|
|
15
|
+
featureDescription: string;
|
|
16
|
+
stories: Story[];
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* Append stories to existing PRD or create new one
|
|
20
|
+
*/
|
|
21
|
+
export declare function appendToPRD(projectPath: string, featureName: string, featureDescription: string, newStories: Story[]): {
|
|
22
|
+
added: number;
|
|
23
|
+
total: number;
|
|
24
|
+
prdPath: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Get current PRD status
|
|
28
|
+
*/
|
|
29
|
+
export declare function getPRDStatus(projectPath: string): {
|
|
30
|
+
exists: boolean;
|
|
31
|
+
featureName?: string;
|
|
32
|
+
total: number;
|
|
33
|
+
pending: number;
|
|
34
|
+
completed: number;
|
|
35
|
+
} | null;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=prd-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prd-generator.d.ts","sourceRoot":"","sources":["../../src/loopgram/prd-generator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAI1C,UAAU,KAAK;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,CAAC;CAC5D;AAmCD;;GAEG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,OAAO,EAAE,EACvB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,KAAK,EAAE,CAAA;CAAE,CAAC,CAwChF;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,kBAAkB,EAAE,MAAM,EAC1B,UAAU,EAAE,KAAK,EAAE,GAClB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA4CnD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG;IACjD,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,IAAI,CAwBP"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
4
|
+
const anthropic = new Anthropic();
|
|
5
|
+
const PRD_PROMPT = `You are generating stories for a PRD (Product Requirements Document) that will be executed by an autonomous coding agent called Ralph.
|
|
6
|
+
|
|
7
|
+
Based on the conversation, generate 1-3 small, focused stories. Each story should be completable in a single coding session.
|
|
8
|
+
|
|
9
|
+
Output ONLY valid JSON in this exact format:
|
|
10
|
+
{
|
|
11
|
+
"featureName": "Brief feature name",
|
|
12
|
+
"featureDescription": "One sentence description",
|
|
13
|
+
"stories": [
|
|
14
|
+
{
|
|
15
|
+
"id": "kebab-case-id",
|
|
16
|
+
"title": "Short title",
|
|
17
|
+
"description": "What needs to be built",
|
|
18
|
+
"acceptanceCriteria": ["Criterion 1", "Criterion 2"],
|
|
19
|
+
"testSteps": ["npm test", "npm run lint"]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Rules:
|
|
25
|
+
- Story IDs must be unique kebab-case (e.g., "add-oauth-config")
|
|
26
|
+
- Keep stories small and focused
|
|
27
|
+
- Include realistic test commands in testSteps
|
|
28
|
+
- Output ONLY the JSON, no markdown or explanation`;
|
|
29
|
+
/**
|
|
30
|
+
* Generate PRD stories from a conversation
|
|
31
|
+
*/
|
|
32
|
+
export async function generateStories(conversation, context, model) {
|
|
33
|
+
let prompt = 'Generate PRD stories from this conversation:\n\n';
|
|
34
|
+
if (context) {
|
|
35
|
+
prompt += `## Codebase Context\n${context}\n\n`;
|
|
36
|
+
}
|
|
37
|
+
prompt += '## Conversation\n';
|
|
38
|
+
for (const msg of conversation) {
|
|
39
|
+
prompt += `${msg.role.toUpperCase()}: ${msg.content}\n\n`;
|
|
40
|
+
}
|
|
41
|
+
const response = await anthropic.messages.create({
|
|
42
|
+
model,
|
|
43
|
+
max_tokens: 2000,
|
|
44
|
+
system: PRD_PROMPT,
|
|
45
|
+
messages: [{ role: 'user', content: prompt }],
|
|
46
|
+
});
|
|
47
|
+
const text = response.content[0];
|
|
48
|
+
if (text.type !== 'text') {
|
|
49
|
+
throw new Error('Unexpected response type');
|
|
50
|
+
}
|
|
51
|
+
// Parse JSON from response (handle markdown code blocks)
|
|
52
|
+
let jsonStr = text.text.trim();
|
|
53
|
+
if (jsonStr.startsWith('```')) {
|
|
54
|
+
jsonStr = jsonStr.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
|
|
55
|
+
}
|
|
56
|
+
const result = JSON.parse(jsonStr);
|
|
57
|
+
return {
|
|
58
|
+
featureName: result.featureName,
|
|
59
|
+
featureDescription: result.featureDescription,
|
|
60
|
+
stories: result.stories.map((s) => ({
|
|
61
|
+
...s,
|
|
62
|
+
status: 'pending',
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Append stories to existing PRD or create new one
|
|
68
|
+
*/
|
|
69
|
+
export function appendToPRD(projectPath, featureName, featureDescription, newStories) {
|
|
70
|
+
const prdPath = join(projectPath, '.ralph/prd.json');
|
|
71
|
+
let prd;
|
|
72
|
+
if (existsSync(prdPath)) {
|
|
73
|
+
// Load existing PRD
|
|
74
|
+
const content = readFileSync(prdPath, 'utf-8');
|
|
75
|
+
prd = JSON.parse(content);
|
|
76
|
+
// Check for duplicate story IDs and make unique
|
|
77
|
+
const existingIds = new Set(prd.stories.map((s) => s.id));
|
|
78
|
+
for (const story of newStories) {
|
|
79
|
+
let id = story.id;
|
|
80
|
+
let counter = 1;
|
|
81
|
+
while (existingIds.has(id)) {
|
|
82
|
+
id = `${story.id}-${counter}`;
|
|
83
|
+
counter++;
|
|
84
|
+
}
|
|
85
|
+
story.id = id;
|
|
86
|
+
existingIds.add(id);
|
|
87
|
+
}
|
|
88
|
+
// Append new stories
|
|
89
|
+
prd.stories.push(...newStories);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Create new PRD
|
|
93
|
+
prd = {
|
|
94
|
+
feature: {
|
|
95
|
+
name: featureName,
|
|
96
|
+
description: featureDescription,
|
|
97
|
+
},
|
|
98
|
+
stories: newStories,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Write PRD
|
|
102
|
+
writeFileSync(prdPath, JSON.stringify(prd, null, 2));
|
|
103
|
+
return {
|
|
104
|
+
added: newStories.length,
|
|
105
|
+
total: prd.stories.length,
|
|
106
|
+
prdPath,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get current PRD status
|
|
111
|
+
*/
|
|
112
|
+
export function getPRDStatus(projectPath) {
|
|
113
|
+
const prdPath = join(projectPath, '.ralph/prd.json');
|
|
114
|
+
if (!existsSync(prdPath)) {
|
|
115
|
+
return { exists: false, total: 0, pending: 0, completed: 0 };
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const content = readFileSync(prdPath, 'utf-8');
|
|
119
|
+
const prd = JSON.parse(content);
|
|
120
|
+
const pending = prd.stories.filter((s) => s.status === 'pending').length;
|
|
121
|
+
const completed = prd.stories.filter((s) => s.status === 'completed').length;
|
|
122
|
+
return {
|
|
123
|
+
exists: true,
|
|
124
|
+
featureName: prd.feature.name,
|
|
125
|
+
total: prd.stories.length,
|
|
126
|
+
pending,
|
|
127
|
+
completed,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=prd-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prd-generator.js","sourceRoot":"","sources":["../../src/loopgram/prd-generator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAG1C,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC;AAmBlC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;mDAuBgC,CAAC;AAEpD;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,YAAuB,EACvB,OAA2B,EAC3B,KAAa;IAEb,IAAI,MAAM,GAAG,kDAAkD,CAAC;IAEhE,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,IAAI,wBAAwB,OAAO,MAAM,CAAC;IAClD,CAAC;IAED,MAAM,IAAI,mBAAmB,CAAC;IAC9B,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,CAAC,OAAO,MAAM,CAAC;IAC5D,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC/C,KAAK;QACL,UAAU,EAAE,IAAI;QAChB,MAAM,EAAE,UAAU;QAClB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;KAC9C,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC9C,CAAC;IAED,yDAAyD;IACzD,IAAI,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAC/B,IAAI,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAEnC,OAAO;QACL,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,kBAAkB,EAAE,MAAM,CAAC,kBAAkB;QAC7C,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;YACvC,GAAG,CAAC;YACJ,MAAM,EAAE,SAAS;SAClB,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CACzB,WAAmB,EACnB,WAAmB,EACnB,kBAA0B,EAC1B,UAAmB;IAEnB,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC;IAErD,IAAI,GAAQ,CAAC;IAEb,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACxB,oBAAoB;QACpB,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC/C,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAE1B,gDAAgD;QAChD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1D,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;YAC/B,IAAI,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC;YAClB,IAAI,OAAO,GAAG,CAAC,CAAC;YAChB,OAAO,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC3B,EAAE,GAAG,GAAG,KAAK,CAAC,EAAE,IAAI,OAAO,EAAE,CAAC;gBAC9B,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC;YACd,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACtB,CAAC;QAED,qBAAqB;QACrB,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;IAClC,CAAC;SAAM,CAAC;QACN,iBAAiB;QACjB,GAAG,GAAG;YACJ,OAAO,EAAE;gBACP,IAAI,EAAE,WAAW;gBACjB,WAAW,EAAE,kBAAkB;aAChC;YACD,OAAO,EAAE,UAAU;SACpB,CAAC;IACJ,CAAC;IAED,YAAY;IACZ,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAErD,OAAO;QACL,KAAK,EAAE,UAAU,CAAC,MAAM;QACxB,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM;QACzB,OAAO;KACR,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,WAAmB;IAO9C,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC;IAErD,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IAC/D,CAAC;IAED,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC/C,MAAM,GAAG,GAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAErC,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,MAAM,CAAC;QACzE,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,MAAM,CAAC;QAE7E,OAAO;YACL,MAAM,EAAE,IAAI;YACZ,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI;YAC7B,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM;YACzB,OAAO;YACP,SAAS;SACV,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save a brainstorming idea to docs/ideas/ in the specified project
|
|
3
|
+
*/
|
|
4
|
+
export declare function saveIdea(projectPath: string, filename: string, content: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Generate a filename-safe slug from a title
|
|
7
|
+
*/
|
|
8
|
+
export declare function slugify(title: string): string;
|
|
9
|
+
//# sourceMappingURL=saver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saver.d.ts","sourceRoot":"","sources":["../../src/loopgram/saver.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAgB,QAAQ,CACtB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,MAAM,CAsBR;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Save a brainstorming idea to docs/ideas/ in the specified project
|
|
5
|
+
*/
|
|
6
|
+
export function saveIdea(projectPath, filename, content) {
|
|
7
|
+
const ideasDir = join(projectPath, 'docs/ideas');
|
|
8
|
+
// Ensure directory exists
|
|
9
|
+
if (!existsSync(ideasDir)) {
|
|
10
|
+
mkdirSync(ideasDir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
const filepath = join(ideasDir, `${filename}.md`);
|
|
13
|
+
// Add metadata header
|
|
14
|
+
const fullContent = `---
|
|
15
|
+
created: ${new Date().toISOString()}
|
|
16
|
+
source: telegram-brainstorm
|
|
17
|
+
status: idea
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
${content}
|
|
21
|
+
`;
|
|
22
|
+
writeFileSync(filepath, fullContent);
|
|
23
|
+
return filepath;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generate a filename-safe slug from a title
|
|
27
|
+
*/
|
|
28
|
+
export function slugify(title) {
|
|
29
|
+
return title
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
32
|
+
.replace(/^-|-$/g, '')
|
|
33
|
+
.slice(0, 50);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=saver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saver.js","sourceRoot":"","sources":["../../src/loopgram/saver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B;;GAEG;AACH,MAAM,UAAU,QAAQ,CACtB,WAAmB,EACnB,QAAgB,EAChB,OAAe;IAEf,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IAEjD,0BAA0B;IAC1B,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,QAAQ,KAAK,CAAC,CAAC;IAElD,sBAAsB;IACtB,MAAM,WAAW,GAAG;WACX,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;;;;;EAKjC,OAAO;CACR,CAAC;IAEA,aAAa,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACrC,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,KAAK;SACT,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript interfaces for the Telegram Brainstorm Bot
|
|
3
|
+
*/
|
|
4
|
+
export interface Message {
|
|
5
|
+
role: 'user' | 'assistant';
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ProjectConfig {
|
|
9
|
+
name: string;
|
|
10
|
+
path: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface TelegramConfig {
|
|
14
|
+
allowedUserIds: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface AnthropicConfig {
|
|
17
|
+
model: string;
|
|
18
|
+
}
|
|
19
|
+
export interface BrainstormConfig {
|
|
20
|
+
telegram: TelegramConfig;
|
|
21
|
+
anthropic: AnthropicConfig;
|
|
22
|
+
projects: Record<string, ProjectConfig>;
|
|
23
|
+
}
|
|
24
|
+
export interface SaveResult {
|
|
25
|
+
filepath: string;
|
|
26
|
+
summary: string;
|
|
27
|
+
title: string;
|
|
28
|
+
}
|
|
29
|
+
export interface LoopStatus {
|
|
30
|
+
isRunning: boolean;
|
|
31
|
+
currentStory: string | null;
|
|
32
|
+
completedStories: number;
|
|
33
|
+
totalStories: number;
|
|
34
|
+
lastUpdate: string;
|
|
35
|
+
errors: string[];
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/loopgram/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,eAAe,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/loopgram/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-loop",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.2",
|
|
4
4
|
"description": "Autonomous AI coding loop - PRD-driven development with Claude Code",
|
|
5
5
|
"author": "Allie Jones <allie@allthrive.ai>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,7 +58,9 @@
|
|
|
58
58
|
"postinstall": "./bin/postinstall.sh",
|
|
59
59
|
"release": "npm version patch && git push && git push --tags && npm publish",
|
|
60
60
|
"release:minor": "npm version minor && git push && git push --tags && npm publish",
|
|
61
|
-
"release:major": "npm version major && git push && git push --tags && npm publish"
|
|
61
|
+
"release:major": "npm version major && git push && git push --tags && npm publish",
|
|
62
|
+
"loopgram": "tsx src/loopgram/index.ts",
|
|
63
|
+
"loopgram:bg": "nohup npm run loopgram > .ralph/loopgram.log 2>&1 &"
|
|
62
64
|
},
|
|
63
65
|
"engines": {
|
|
64
66
|
"node": ">=18.0.0"
|
|
@@ -72,6 +74,8 @@
|
|
|
72
74
|
}
|
|
73
75
|
},
|
|
74
76
|
"dependencies": {
|
|
77
|
+
"@anthropic-ai/sdk": "^0.32.0",
|
|
78
|
+
"telegraf": "^4.16.0",
|
|
75
79
|
"tsx": "^4.0.0"
|
|
76
80
|
},
|
|
77
81
|
"devDependencies": {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# shellcheck shell=bash
|
|
3
|
+
# common.sh - Shared utilities for Claude Code hooks
|
|
4
|
+
#
|
|
5
|
+
# Source this file at the start of PostToolUse hooks:
|
|
6
|
+
# source "$(dirname "$0")/common.sh"
|
|
7
|
+
#
|
|
8
|
+
# Provides:
|
|
9
|
+
# - parse_hook_input: Sets HOOK_INPUT, TOOL_NAME, FILE_PATH, NEW_CONTENT
|
|
10
|
+
# - is_code_file: Check if FILE_PATH matches code extensions
|
|
11
|
+
# - is_test_file: Check if FILE_PATH is a test file
|
|
12
|
+
# - hook_allow: Output JSON to allow the operation
|
|
13
|
+
# - hook_warn: Output JSON warning (non-blocking)
|
|
14
|
+
# - hook_block: Output JSON to block the operation
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
# Global variables set by parse_hook_input
|
|
19
|
+
HOOK_INPUT=""
|
|
20
|
+
TOOL_NAME=""
|
|
21
|
+
FILE_PATH=""
|
|
22
|
+
NEW_CONTENT=""
|
|
23
|
+
|
|
24
|
+
# Parse hook input from stdin and extract common fields
|
|
25
|
+
# Sets: HOOK_INPUT, TOOL_NAME, FILE_PATH, NEW_CONTENT
|
|
26
|
+
parse_hook_input() {
|
|
27
|
+
HOOK_INPUT=$(cat)
|
|
28
|
+
TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // ""')
|
|
29
|
+
FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
|
|
30
|
+
|
|
31
|
+
# Extract content based on tool type
|
|
32
|
+
NEW_CONTENT=""
|
|
33
|
+
if [[ "$TOOL_NAME" == "Write" ]]; then
|
|
34
|
+
NEW_CONTENT=$(echo "$HOOK_INPUT" | jq -r '.tool_input.content // ""')
|
|
35
|
+
elif [[ "$TOOL_NAME" == "Edit" ]]; then
|
|
36
|
+
NEW_CONTENT=$(echo "$HOOK_INPUT" | jq -r '.tool_input.new_string // ""')
|
|
37
|
+
fi
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Check if FILE_PATH matches given extensions
|
|
41
|
+
# Usage: is_code_file "ts,tsx,js,jsx,py" && echo "yes"
|
|
42
|
+
is_code_file() {
|
|
43
|
+
local extensions="$1"
|
|
44
|
+
|
|
45
|
+
# Files without extension don't match
|
|
46
|
+
[[ "$FILE_PATH" != *"."* ]] && return 1
|
|
47
|
+
|
|
48
|
+
local file_ext="${FILE_PATH##*.}"
|
|
49
|
+
|
|
50
|
+
# Check if file extension is in the comma-separated list
|
|
51
|
+
[[ ",$extensions," == *",$file_ext,"* ]]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Check if FILE_PATH is a test file
|
|
55
|
+
is_test_file() {
|
|
56
|
+
case "$FILE_PATH" in
|
|
57
|
+
*.test.*|*.spec.*|*/__tests__/*|*/test/*|*/tests/*|*/fixtures/*)
|
|
58
|
+
return 0
|
|
59
|
+
;;
|
|
60
|
+
esac
|
|
61
|
+
return 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Output JSON to allow the operation (continue: true)
|
|
65
|
+
hook_allow() {
|
|
66
|
+
echo '{"continue": true}'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Output warning JSON (non-blocking, adds context)
|
|
70
|
+
# Usage: hook_warn "Warning message"
|
|
71
|
+
hook_warn() {
|
|
72
|
+
local message="$1"
|
|
73
|
+
jq -n --arg warn "$message" '{
|
|
74
|
+
"continue": true,
|
|
75
|
+
"hookSpecificOutput": {
|
|
76
|
+
"additionalContext": $warn
|
|
77
|
+
}
|
|
78
|
+
}'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Output blocking JSON (continue: false)
|
|
82
|
+
# Usage: hook_block "Error message"
|
|
83
|
+
hook_block() {
|
|
84
|
+
local message="$1"
|
|
85
|
+
jq -n --arg msg "$message" '{
|
|
86
|
+
"continue": false,
|
|
87
|
+
"message": $msg
|
|
88
|
+
}'
|
|
89
|
+
}
|
|
@@ -1,54 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# shellcheck shell=bash
|
|
2
3
|
# warn-debug.sh - Warn about debug statements in written code
|
|
3
4
|
# Hook: PostToolUse matcher: "Edit|Write"
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
source "$(dirname "$0")/common.sh"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
9
|
-
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
|
|
8
|
+
parse_hook_input
|
|
10
9
|
|
|
11
10
|
# Only check code files
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*)
|
|
16
|
-
echo '{"continue": true}'
|
|
17
|
-
exit 0
|
|
18
|
-
;;
|
|
19
|
-
esac
|
|
20
|
-
|
|
21
|
-
# Get the content that was written
|
|
22
|
-
NEW_CONTENT=""
|
|
23
|
-
if [[ "$TOOL_NAME" == "Write" ]]; then
|
|
24
|
-
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // ""')
|
|
25
|
-
elif [[ "$TOOL_NAME" == "Edit" ]]; then
|
|
26
|
-
NEW_CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // ""')
|
|
11
|
+
if ! is_code_file "ts,tsx,js,jsx,py,go,rs"; then
|
|
12
|
+
hook_allow
|
|
13
|
+
exit 0
|
|
27
14
|
fi
|
|
28
15
|
|
|
29
16
|
# Check for debug patterns
|
|
30
17
|
WARNINGS=""
|
|
31
18
|
|
|
32
|
-
if echo "$NEW_CONTENT" | grep -qE 'console\.(log|debug|info|warn|error)
|
|
19
|
+
if echo "$NEW_CONTENT" | grep -qE 'console\.(log|debug|info|warn|error)[[:space:]]*\('; then
|
|
33
20
|
WARNINGS="⚠️ Debug statement detected: console.log/debug. Remove before commit."
|
|
34
21
|
fi
|
|
35
22
|
|
|
36
|
-
if echo "$NEW_CONTENT" | grep -qE '
|
|
37
|
-
WARNINGS="${WARNINGS}
|
|
23
|
+
if echo "$NEW_CONTENT" | grep -qE '^[[:space:]]*debugger[[:space:]]*;?[[:space:]]*$'; then
|
|
24
|
+
WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Debugger statement detected. Remove before commit."
|
|
38
25
|
fi
|
|
39
26
|
|
|
40
|
-
if echo "$NEW_CONTENT" | grep -qE '
|
|
41
|
-
WARNINGS="${WARNINGS}
|
|
27
|
+
if echo "$NEW_CONTENT" | grep -qE '^[[:space:]]*print[[:space:]]*\('; then
|
|
28
|
+
WARNINGS="${WARNINGS}${WARNINGS:+\\n}⚠️ Print statement detected. Remove before commit."
|
|
42
29
|
fi
|
|
43
30
|
|
|
44
|
-
# Output warning
|
|
31
|
+
# Output warning or allow
|
|
45
32
|
if [[ -n "$WARNINGS" ]]; then
|
|
46
|
-
|
|
47
|
-
"continue": true,
|
|
48
|
-
"hookSpecificOutput": {
|
|
49
|
-
"additionalContext": $warn
|
|
50
|
-
}
|
|
51
|
-
}'
|
|
33
|
+
hook_warn "$WARNINGS"
|
|
52
34
|
else
|
|
53
|
-
|
|
35
|
+
hook_allow
|
|
54
36
|
fi
|