internaltool-mcp 1.6.40 → 1.6.42
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/index.js +285 -44
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
19
19
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
20
20
|
import { execSync } from 'child_process'
|
|
21
|
-
import { mkdirSync, writeFileSync, unlinkSync, existsSync, readdirSync, statSync } from 'fs'
|
|
21
|
+
import { mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync, readdirSync, statSync } from 'fs'
|
|
22
22
|
import { join } from 'path'
|
|
23
23
|
import { z } from 'zod'
|
|
24
24
|
import { api, login, configure } from './api-client.js'
|
|
@@ -83,6 +83,180 @@ async function assertAdmin() {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// ── Codebase analysis helpers (used by plan_task_from_codebase) ───────────────
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reads manifest files in cwd and returns detected stack info.
|
|
90
|
+
* Works for any language — JS/TS, Python, Go, Ruby, Java, Rust, PHP.
|
|
91
|
+
*/
|
|
92
|
+
function detectStack(cwd) {
|
|
93
|
+
const stack = { language: null, framework: null, testRunner: null, packageManager: null, extra: [] }
|
|
94
|
+
try {
|
|
95
|
+
// Node.js / JS / TS
|
|
96
|
+
const pkgPath = join(cwd, 'package.json')
|
|
97
|
+
if (existsSync(pkgPath)) {
|
|
98
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
99
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
100
|
+
stack.language = pkg.scripts?.build?.includes('tsc') || existsSync(join(cwd, 'tsconfig.json')) ? 'typescript' : 'javascript'
|
|
101
|
+
stack.packageManager = existsSync(join(cwd, 'pnpm-lock.yaml')) ? 'pnpm' : existsSync(join(cwd, 'yarn.lock')) ? 'yarn' : 'npm'
|
|
102
|
+
// Framework detection
|
|
103
|
+
if (deps.next) stack.framework = 'nextjs'
|
|
104
|
+
else if (deps.nuxt) stack.framework = 'nuxt'
|
|
105
|
+
else if (deps.react) stack.framework = 'react'
|
|
106
|
+
else if (deps.vue) stack.framework = 'vue'
|
|
107
|
+
else if (deps.svelte) stack.framework = 'svelte'
|
|
108
|
+
else if (deps.express) stack.framework = 'express'
|
|
109
|
+
else if (deps.fastify) stack.framework = 'fastify'
|
|
110
|
+
else if (deps.nestjs || deps['@nestjs/core']) stack.framework = 'nestjs'
|
|
111
|
+
else if (deps.hono) stack.framework = 'hono'
|
|
112
|
+
// Test runner
|
|
113
|
+
if (deps.jest || deps['@jest/core']) stack.testRunner = 'jest'
|
|
114
|
+
else if (deps.vitest) stack.testRunner = 'vitest'
|
|
115
|
+
else if (deps.mocha) stack.testRunner = 'mocha'
|
|
116
|
+
else if (deps.playwright || deps['@playwright/test']) stack.testRunner = 'playwright'
|
|
117
|
+
// ORM / DB
|
|
118
|
+
if (deps.mongoose) stack.extra.push('mongoose')
|
|
119
|
+
if (deps.prisma) stack.extra.push('prisma')
|
|
120
|
+
if (deps.drizzle) stack.extra.push('drizzle')
|
|
121
|
+
if (deps.typeorm) stack.extra.push('typeorm')
|
|
122
|
+
if (deps.sequelize) stack.extra.push('sequelize')
|
|
123
|
+
if (deps['@supabase/supabase-js']) stack.extra.push('supabase')
|
|
124
|
+
if (deps.trpc || deps['@trpc/server']) stack.extra.push('trpc')
|
|
125
|
+
return stack
|
|
126
|
+
}
|
|
127
|
+
// Python
|
|
128
|
+
const reqPath = join(cwd, 'requirements.txt')
|
|
129
|
+
const pyprojPath = join(cwd, 'pyproject.toml')
|
|
130
|
+
if (existsSync(reqPath) || existsSync(pyprojPath)) {
|
|
131
|
+
stack.language = 'python'
|
|
132
|
+
const content = existsSync(reqPath) ? readFileSync(reqPath, 'utf8') : readFileSync(pyprojPath, 'utf8')
|
|
133
|
+
if (content.includes('django')) stack.framework = 'django'
|
|
134
|
+
else if (content.includes('fastapi')) stack.framework = 'fastapi'
|
|
135
|
+
else if (content.includes('flask')) stack.framework = 'flask'
|
|
136
|
+
else if (content.includes('starlette')) stack.framework = 'starlette'
|
|
137
|
+
if (content.includes('pytest')) stack.testRunner = 'pytest'
|
|
138
|
+
if (content.includes('sqlalchemy')) stack.extra.push('sqlalchemy')
|
|
139
|
+
if (content.includes('alembic')) stack.extra.push('alembic')
|
|
140
|
+
return stack
|
|
141
|
+
}
|
|
142
|
+
// Go
|
|
143
|
+
if (existsSync(join(cwd, 'go.mod'))) {
|
|
144
|
+
stack.language = 'go'
|
|
145
|
+
const gomod = readFileSync(join(cwd, 'go.mod'), 'utf8')
|
|
146
|
+
if (gomod.includes('gin-gonic/gin')) stack.framework = 'gin'
|
|
147
|
+
else if (gomod.includes('go-chi/chi')) stack.framework = 'chi'
|
|
148
|
+
else if (gomod.includes('labstack/echo')) stack.framework = 'echo'
|
|
149
|
+
else if (gomod.includes('gofiber/fiber')) stack.framework = 'fiber'
|
|
150
|
+
stack.testRunner = 'go test'
|
|
151
|
+
return stack
|
|
152
|
+
}
|
|
153
|
+
// Ruby
|
|
154
|
+
if (existsSync(join(cwd, 'Gemfile'))) {
|
|
155
|
+
stack.language = 'ruby'
|
|
156
|
+
const gemfile = readFileSync(join(cwd, 'Gemfile'), 'utf8')
|
|
157
|
+
if (gemfile.includes('rails')) stack.framework = 'rails'
|
|
158
|
+
else if (gemfile.includes('sinatra')) stack.framework = 'sinatra'
|
|
159
|
+
if (gemfile.includes('rspec')) stack.testRunner = 'rspec'
|
|
160
|
+
return stack
|
|
161
|
+
}
|
|
162
|
+
// Rust
|
|
163
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) {
|
|
164
|
+
stack.language = 'rust'
|
|
165
|
+
const cargo = readFileSync(join(cwd, 'Cargo.toml'), 'utf8')
|
|
166
|
+
if (cargo.includes('actix-web')) stack.framework = 'actix-web'
|
|
167
|
+
else if (cargo.includes('axum')) stack.framework = 'axum'
|
|
168
|
+
else if (cargo.includes('warp')) stack.framework = 'warp'
|
|
169
|
+
stack.testRunner = 'cargo test'
|
|
170
|
+
return stack
|
|
171
|
+
}
|
|
172
|
+
// Java / Kotlin
|
|
173
|
+
if (existsSync(join(cwd, 'pom.xml')) || existsSync(join(cwd, 'build.gradle'))) {
|
|
174
|
+
stack.language = existsSync(join(cwd, 'build.gradle.kts')) ? 'kotlin' : 'java'
|
|
175
|
+
stack.framework = 'spring-boot'
|
|
176
|
+
stack.testRunner = 'junit'
|
|
177
|
+
return stack
|
|
178
|
+
}
|
|
179
|
+
// PHP
|
|
180
|
+
if (existsSync(join(cwd, 'composer.json'))) {
|
|
181
|
+
stack.language = 'php'
|
|
182
|
+
const composer = JSON.parse(readFileSync(join(cwd, 'composer.json'), 'utf8'))
|
|
183
|
+
const req = { ...composer.require, ...composer['require-dev'] }
|
|
184
|
+
if (req['laravel/framework']) stack.framework = 'laravel'
|
|
185
|
+
else if (req['symfony/symfony'] || req['symfony/framework-bundle']) stack.framework = 'symfony'
|
|
186
|
+
return stack
|
|
187
|
+
}
|
|
188
|
+
} catch { /* non-fatal */ }
|
|
189
|
+
return stack
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns a shallow directory tree (depth 2) so the agent knows the folder layout.
|
|
194
|
+
* Ignores node_modules, .git, vendor, __pycache__, dist, build, .next
|
|
195
|
+
*/
|
|
196
|
+
function getDirTree(cwd, maxDepth = 2) {
|
|
197
|
+
const IGNORE = new Set(['node_modules', '.git', 'vendor', '__pycache__', 'dist', 'build', '.next', '.turbo', 'coverage', '.cache', 'target', 'venv', '.venv'])
|
|
198
|
+
function walk(dir, depth) {
|
|
199
|
+
if (depth > maxDepth) return []
|
|
200
|
+
let entries
|
|
201
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return [] }
|
|
202
|
+
return entries
|
|
203
|
+
.filter(e => !IGNORE.has(e.name) && !e.name.startsWith('.'))
|
|
204
|
+
.map(e => {
|
|
205
|
+
if (e.isDirectory()) {
|
|
206
|
+
const children = walk(join(dir, e.name), depth + 1)
|
|
207
|
+
return { name: e.name + '/', children: children.length ? children : undefined }
|
|
208
|
+
}
|
|
209
|
+
return { name: e.name }
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
return walk(cwd, 0)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Based on detected stack, grep for the most likely entry-point files.
|
|
217
|
+
* Returns a list of { pattern, matches } — actual file paths found locally.
|
|
218
|
+
*/
|
|
219
|
+
function findEntryPoints(cwd, stack) {
|
|
220
|
+
const results = {}
|
|
221
|
+
const greps = {
|
|
222
|
+
javascript: { typescript: true,
|
|
223
|
+
routes: ['router\\.', 'app\\.get\\|app\\.post\\|app\\.put\\|fastify\\.', 'Route path='],
|
|
224
|
+
models: ['mongoose\\.Schema\\|new Schema\\|@Entity\\|@Table'],
|
|
225
|
+
components: ['export default function\\|export const.*=.*=>\\|React\\.FC'],
|
|
226
|
+
},
|
|
227
|
+
python: {
|
|
228
|
+
routes: ['@app\\.route\\|@router\\.\\|path(\\|urlpatterns\\|@api_view'],
|
|
229
|
+
models: ['class.*Model\\|db\\.Model\\|Base\\)\\|models\\.Model'],
|
|
230
|
+
schemas: ['class.*Schema\\|serializers\\.\\|Pydantic\\|BaseModel'],
|
|
231
|
+
},
|
|
232
|
+
go: {
|
|
233
|
+
routes: ['router\\.\\.\\|http\\.HandleFunc\\|r\\.GET\\|r\\.POST\\|e\\.GET'],
|
|
234
|
+
models: ['type.*struct\\|gorm\\.Model'],
|
|
235
|
+
},
|
|
236
|
+
ruby: {
|
|
237
|
+
routes: ['resources \\|get \'\\|post \'\\|Rails\\.application\\.routes'],
|
|
238
|
+
models: ['ApplicationRecord\\|ActiveRecord::Base\\|belongs_to\\|has_many'],
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const lang = stack.language === 'typescript' ? 'javascript' : stack.language
|
|
243
|
+
const patterns = greps[lang] || {}
|
|
244
|
+
|
|
245
|
+
for (const [type, patternList] of Object.entries(patterns)) {
|
|
246
|
+
if (type === 'typescript') continue
|
|
247
|
+
try {
|
|
248
|
+
const pat = Array.isArray(patternList) ? patternList.join('\\|') : patternList
|
|
249
|
+
const out = execSync(
|
|
250
|
+
`grep -rl "${pat}" . --include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" --include="*.py" --include="*.go" --include="*.rb" --include="*.java" --include="*.kt" --include="*.rs" --include="*.php" 2>/dev/null | grep -v node_modules | grep -v ".git" | head -8`,
|
|
251
|
+
{ cwd, encoding: 'utf8', timeout: 4000 }
|
|
252
|
+
)
|
|
253
|
+
const files = out.trim().split('\n').filter(Boolean)
|
|
254
|
+
if (files.length) results[type] = files
|
|
255
|
+
} catch { /* non-fatal */ }
|
|
256
|
+
}
|
|
257
|
+
return results
|
|
258
|
+
}
|
|
259
|
+
|
|
86
260
|
// ── Tool registration functions ───────────────────────────────────────────────
|
|
87
261
|
|
|
88
262
|
function registerAuthTools(server) {
|
|
@@ -289,6 +463,60 @@ Returns tasks with key, title, column, assignees, priority, taskType, and branch
|
|
|
289
463
|
}
|
|
290
464
|
)
|
|
291
465
|
|
|
466
|
+
// ── suggest_skill ─────────────────────────────────────────────────────────────
|
|
467
|
+
server.tool(
|
|
468
|
+
'suggest_skill',
|
|
469
|
+
`Propose a reusable skill, rule, prompt, subagent, or hook to the project's AgentKit — pending human approval.
|
|
470
|
+
|
|
471
|
+
## When to call this
|
|
472
|
+
|
|
473
|
+
Call this when you discover something during a coding session that would help future sessions in this codebase:
|
|
474
|
+
|
|
475
|
+
- **skill**: A step-by-step methodology you used (e.g. "how to add a new API endpoint in this Express app", "how to run and interpret the test suite")
|
|
476
|
+
- **rule**: A constraint that should always apply (e.g. "always validate with zod before touching the DB", "never modify migrations directly")
|
|
477
|
+
- **prompt**: A prompt template that reliably produces good results for this project (e.g. "security sweep prompt", "PR review checklist")
|
|
478
|
+
- **subagent**: A specialized agent persona (e.g. "database migration specialist with access to prisma tools only")
|
|
479
|
+
- **hook**: A shell command that should run automatically (e.g. "check .internaltool-active-task before any Edit", "run lint after Write")
|
|
480
|
+
|
|
481
|
+
## When NOT to call this
|
|
482
|
+
|
|
483
|
+
- Do not suggest things that are obvious from the README or existing docs
|
|
484
|
+
- Do not suggest generic skills that apply to any codebase (e.g. "how to write clean code")
|
|
485
|
+
- Do not suggest more than 1-2 skills per session — be selective and high-signal
|
|
486
|
+
|
|
487
|
+
## The human reviews and decides
|
|
488
|
+
|
|
489
|
+
Your suggestion goes into a "Pending" queue visible in the project's AgentKit UI.
|
|
490
|
+
The developer can edit the body, approve (which promotes it to active config), or reject.
|
|
491
|
+
After approval, the skill/rule is injected at every future kickoff_task.`,
|
|
492
|
+
{
|
|
493
|
+
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
494
|
+
type: z.enum(['skill', 'rule', 'prompt', 'subagent', 'hook']).describe('What kind of suggestion this is'),
|
|
495
|
+
name: z.string().describe('Slug name — lowercase, hyphens, no spaces (e.g. "add-express-route", "require-zod-validation")'),
|
|
496
|
+
description: z.string().describe('One-line description of what this does — shown in the AgentKit UI'),
|
|
497
|
+
body: z.string().describe('Full markdown content — for skills: step-by-step instructions; for rules: constraint language; for hooks: shell command + explanation'),
|
|
498
|
+
rationale: z.string().describe('Why this would help future sessions — what problem it solves, what mistake it prevents, or what pattern it captures'),
|
|
499
|
+
sourceTaskId: z.string().optional().describe('Task ID this was discovered during (for traceability)'),
|
|
500
|
+
sourceTaskKey: z.string().optional().describe('Task key (e.g. PROJ-42) for display'),
|
|
501
|
+
// Hook-specific
|
|
502
|
+
hookTrigger: z.string().optional().describe('For type=hook: when it fires (PreToolUse | PostToolUse | Stop)'),
|
|
503
|
+
hookMatcher: z.string().optional().describe('For type=hook: tool name regex matcher (e.g. "Edit|Write")'),
|
|
504
|
+
hookCommand: z.string().optional().describe('For type=hook: the shell command to execute'),
|
|
505
|
+
},
|
|
506
|
+
async ({ projectId, type, name, description, body, rationale, sourceTaskId, sourceTaskKey, hookTrigger, hookMatcher, hookCommand }) => {
|
|
507
|
+
try { assertProjectScope(projectId) } catch (e) { return errorText(e.message) }
|
|
508
|
+
return call(() => api.post(`/api/projects/${projectId}/skill-suggestions`, {
|
|
509
|
+
type, name, description, body, rationale,
|
|
510
|
+
sourceTaskId: sourceTaskId || null,
|
|
511
|
+
sourceTaskKey: sourceTaskKey || '',
|
|
512
|
+
suggestedBy: 'Claude Code',
|
|
513
|
+
hookTrigger: hookTrigger || '',
|
|
514
|
+
hookMatcher: hookMatcher || '',
|
|
515
|
+
hookCommand: hookCommand || '',
|
|
516
|
+
}))
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
|
|
292
520
|
// ── plan_task_from_codebase ───────────────────────────────────────────────────
|
|
293
521
|
server.tool(
|
|
294
522
|
'plan_task_from_codebase',
|
|
@@ -370,7 +598,13 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
370
598
|
return errorText(`Access denied: session is scoped to project ${scopedProjectId}`)
|
|
371
599
|
}
|
|
372
600
|
|
|
373
|
-
//
|
|
601
|
+
// ── 1. Run codebase analysis using local filesystem access ────────────────
|
|
602
|
+
const cwd = process.cwd()
|
|
603
|
+
const stack = detectStack(cwd)
|
|
604
|
+
const dirTree = getDirTree(cwd, 2)
|
|
605
|
+
const entryPoints = findEntryPoints(cwd, stack)
|
|
606
|
+
|
|
607
|
+
// ── 2. Fetch project context ──────────────────────────────────────────────
|
|
374
608
|
let projectContext = null
|
|
375
609
|
try {
|
|
376
610
|
const projRes = await api.get(`/api/projects/${projectId}`)
|
|
@@ -384,63 +618,58 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
384
618
|
}
|
|
385
619
|
} catch { /* non-fatal */ }
|
|
386
620
|
|
|
387
|
-
//
|
|
388
|
-
let
|
|
621
|
+
// ── 3. Duplicate check ────────────────────────────────────────────────────
|
|
622
|
+
let similarTasks = null
|
|
389
623
|
try {
|
|
390
624
|
const keywords = request.split(' ').filter(w => w.length > 4).slice(0, 3).join(' ')
|
|
391
625
|
const searchRes = await api.get(
|
|
392
626
|
`/api/projects/${projectId}/tasks/search?q=${encodeURIComponent(keywords)}&limit=3`
|
|
393
627
|
)
|
|
394
628
|
if (searchRes?.success && searchRes.data?.tasks?.length > 0) {
|
|
395
|
-
|
|
396
|
-
taskId:
|
|
397
|
-
key: t.key,
|
|
398
|
-
title: t.title,
|
|
399
|
-
column: t.column,
|
|
400
|
-
taskType: t.taskType,
|
|
629
|
+
similarTasks = searchRes.data.tasks.map(t => ({
|
|
630
|
+
taskId: t._id, key: t.key, title: t.title, column: t.column, taskType: t.taskType,
|
|
401
631
|
}))
|
|
402
632
|
}
|
|
403
633
|
} catch { /* non-fatal */ }
|
|
404
634
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
instruction: [
|
|
411
|
-
'If one of these IS the same task: call kickoff_task on it instead of creating a new one.',
|
|
412
|
-
'If none match: proceed with the analysis protocol below.',
|
|
413
|
-
],
|
|
414
|
-
analysisProtocol: {
|
|
415
|
-
step1: 'Read package.json / go.mod / requirements.txt to identify the stack',
|
|
416
|
-
step2: 'Grep for existing patterns relevant to the request',
|
|
417
|
-
step3: 'Identify files to create and modify',
|
|
418
|
-
step4: `Call create_task(projectId="${projectId}", title="...", readmeMarkdown="...", taskType="...", priority="${priority}", column="todo", subtasks=[...], suggestedFiles=[...])`,
|
|
419
|
-
step5: 'Call kickoff_task(taskId=<id>, confirmed=true, agentRole="builder", files=[...suggestedFiles])',
|
|
420
|
-
},
|
|
421
|
-
projectContext,
|
|
422
|
-
request,
|
|
423
|
-
projectId,
|
|
424
|
-
priority,
|
|
425
|
-
})
|
|
426
|
-
}
|
|
635
|
+
// ── 4. Return codebase intelligence + tight instructions ──────────────────
|
|
636
|
+
const hasEntryPoints = Object.keys(entryPoints).length > 0
|
|
637
|
+
const stackSummary = [
|
|
638
|
+
stack.language, stack.framework, stack.testRunner, ...(stack.extra || [])
|
|
639
|
+
].filter(Boolean).join(', ') || 'unknown (no manifest found at cwd)'
|
|
427
640
|
|
|
428
|
-
// No duplicate — return the analysis brief
|
|
429
641
|
return text({
|
|
430
|
-
|
|
642
|
+
// Real codebase data — use these directly, do NOT re-read what's already here
|
|
643
|
+
codebaseIntelligence: {
|
|
644
|
+
cwd,
|
|
645
|
+
stack: { ...stack, summary: stackSummary },
|
|
646
|
+
dirTree,
|
|
647
|
+
entryPoints: hasEntryPoints
|
|
648
|
+
? entryPoints
|
|
649
|
+
: { note: 'No entry points found at cwd — check that MCP server is running from the project root' },
|
|
650
|
+
},
|
|
651
|
+
|
|
652
|
+
// Duplicate guard
|
|
653
|
+
...(similarTasks?.length > 0 && {
|
|
654
|
+
duplicateWarning: true,
|
|
655
|
+
similarTasks,
|
|
656
|
+
duplicateInstruction: 'Check the list above. If one matches → call kickoff_task on it. If none match → proceed.',
|
|
657
|
+
}),
|
|
658
|
+
|
|
659
|
+
// What to do now — use codebaseIntelligence above to skip re-reading the filesystem
|
|
660
|
+
nextSteps: [
|
|
661
|
+
'Use dirTree + entryPoints above to identify which files need changing — you already have the map.',
|
|
662
|
+
`Read 2-3 files from entryPoints.routes / entryPoints.models / entryPoints.components to extract naming conventions and wiring patterns.`,
|
|
663
|
+
'Draft readmeMarkdown with: ## Goal, ## Stack, ## Technical approach (name real files), ## Files to create, ## Files to modify, ## Subtasks (ordered), ## Acceptance criteria.',
|
|
664
|
+
`Call create_task(projectId="${projectId}", title="<verb + noun>", readmeMarkdown="<plan>", taskType="<feature|bugfix|...>", priority="${priority}", column="todo", subtasks=[...], suggestedFiles=[...])`,
|
|
665
|
+
'Immediately after: call kickoff_task(taskId=<returned id>, confirmed=true, agentRole="builder", files=[...suggestedFiles])',
|
|
666
|
+
],
|
|
667
|
+
|
|
668
|
+
// Context
|
|
431
669
|
request,
|
|
670
|
+
projectId,
|
|
432
671
|
priority,
|
|
433
672
|
projectContext,
|
|
434
|
-
analysisProtocol: {
|
|
435
|
-
overview: 'Follow these steps IN ORDER before calling create_task',
|
|
436
|
-
step1_stack: 'Read package.json / go.mod / requirements.txt / Cargo.toml — detect language, framework, test runner',
|
|
437
|
-
step2_entrypoint: 'Grep for the relevant area: routes, controllers, components, models — find where similar features live',
|
|
438
|
-
step3_patterns: 'Read 2-3 existing similar files — note naming conventions, folder structure, wiring patterns',
|
|
439
|
-
step4_impact: 'List every file to CREATE and every file to MODIFY — be exhaustive',
|
|
440
|
-
step5_create: `Call create_task(projectId="${projectId}", title="<action-verb noun>", readmeMarkdown="<full plan>", taskType="<type>", priority="${priority}", column="todo", subtasks=[{title: "step1"}, ...], suggestedFiles=["path/to/file1", ...])`,
|
|
441
|
-
step6_kickoff: 'Call kickoff_task(taskId=<returned id>, confirmed=true, agentRole="builder", files=[...suggestedFiles])',
|
|
442
|
-
},
|
|
443
|
-
reminder: 'Do NOT call create_task before completing steps 1-4. The readmeMarkdown must reference real file paths from your analysis.',
|
|
444
673
|
})
|
|
445
674
|
}
|
|
446
675
|
)
|
|
@@ -680,6 +909,9 @@ Set confirmed=false first to preview, then confirmed=true to execute everything.
|
|
|
680
909
|
agentWorkspace: { clearedAt: new Date().toISOString() }
|
|
681
910
|
}).catch(() => {/* non-fatal */})
|
|
682
911
|
|
|
912
|
+
// Remove .internaltool-active-task so the Claude Code hook blocks edits until next kickoff
|
|
913
|
+
try { unlinkSync(join(process.cwd(), '.internaltool-active-task')) } catch { /* non-fatal */ }
|
|
914
|
+
|
|
683
915
|
// ── #9 Capture last commit for handoff metadata ───────────────────────
|
|
684
916
|
const lastCommit = getLastCommitMeta(repoRoot)
|
|
685
917
|
|
|
@@ -3913,6 +4145,15 @@ After \`request_human_input\`: STOP, show the question in chat, wait for reply,
|
|
|
3913
4145
|
}
|
|
3914
4146
|
api.patch(`/api/tasks/${taskId}`, workspacePatch).catch(() => {/* non-fatal */})
|
|
3915
4147
|
|
|
4148
|
+
// Write .internaltool-active-task so the Claude Code hook knows a task is active
|
|
4149
|
+
try {
|
|
4150
|
+
writeFileSync(
|
|
4151
|
+
join(process.cwd(), '.internaltool-active-task'),
|
|
4152
|
+
JSON.stringify({ taskId, taskKey: task.key, title: task.title, agentRole: agentRole || null, kickedOffAt: new Date().toISOString() }, null, 2),
|
|
4153
|
+
'utf8'
|
|
4154
|
+
)
|
|
4155
|
+
} catch { /* non-fatal — hook gracefully degrades if file can't be written */ }
|
|
4156
|
+
|
|
3916
4157
|
// Auto-scan workspace so the Workspace tab in the UI is fresh immediately after kickoff.
|
|
3917
4158
|
if (kickoffProject) {
|
|
3918
4159
|
runWorkspaceScan(taskId, task, kickoffProject, repoPath).catch(() => {/* non-fatal */})
|
package/package.json
CHANGED