internaltool-mcp 1.6.24 → 1.6.25
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 +263 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -391,11 +391,16 @@ Set confirmed=false first to preview, then confirmed=true to execute everything.
|
|
|
391
391
|
// ── Save park note + server-side comment & notifications ──────────────
|
|
392
392
|
await api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers })
|
|
393
393
|
|
|
394
|
-
// ── Delete cursor rules file
|
|
394
|
+
// ── Delete cursor rules file + task-scoped workspace files ───────────────
|
|
395
395
|
let cursorRulesCleared = null
|
|
396
396
|
if (task?.cursorRules?.trim()) {
|
|
397
397
|
cursorRulesCleared = deleteCursorRulesFile(task.key, repoPath)
|
|
398
398
|
}
|
|
399
|
+
deleteCursorWorkspace(task?.agentRole || null, repoPath)
|
|
400
|
+
// Mark workspace as cleared in DB so UI shows "not active"
|
|
401
|
+
api.patch(`/api/tasks/${taskId}`, {
|
|
402
|
+
agentWorkspace: { clearedAt: new Date().toISOString() }
|
|
403
|
+
}).catch(() => {/* non-fatal */})
|
|
399
404
|
|
|
400
405
|
// ── #9 Capture last commit for handoff metadata ───────────────────────
|
|
401
406
|
const lastCommit = getLastCommitMeta(repoRoot)
|
|
@@ -438,6 +443,7 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
438
443
|
.describe('Set the agent role for this task session. Role-specific behavioral constraints are injected into cursor rules.'),
|
|
439
444
|
},
|
|
440
445
|
async ({ taskId, confirmed = false, repoPath, autoStash = false, agentRole }) => {
|
|
446
|
+
trackTaskActivity(taskId, 'unpark_task')
|
|
441
447
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
442
448
|
const task = taskRes?.data?.task
|
|
443
449
|
const branch = task?.github?.headBranch || null
|
|
@@ -613,6 +619,7 @@ Roles: BUILDER agents MUST call this before editing. Coordinators call this as p
|
|
|
613
619
|
files: z.array(z.string()).min(1).describe('List of file paths this task will exclusively edit (e.g. ["server/routes/tasks.js", "client/src/App.jsx"])'),
|
|
614
620
|
},
|
|
615
621
|
async ({ taskId, files }) => {
|
|
622
|
+
trackTaskActivity(taskId, 'claim_files')
|
|
616
623
|
const res = await api.post(`/api/tasks/${taskId}/files/claim`, { files })
|
|
617
624
|
if (!res?.success) {
|
|
618
625
|
if (res?.conflicts) {
|
|
@@ -693,6 +700,7 @@ Call confirmed=false to preview the decomposition, confirmed=true to save it.`,
|
|
|
693
700
|
confirmed: z.boolean().optional().default(false).describe('Set true to save the decomposition and create subtasks on the board'),
|
|
694
701
|
},
|
|
695
702
|
async ({ taskId, subtaskPlan, confirmed = false }) => {
|
|
703
|
+
trackTaskActivity(taskId, 'decompose_task')
|
|
696
704
|
const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
|
|
697
705
|
if (!taskRes?.success) return errorText('Task not found')
|
|
698
706
|
const task = taskRes.data.task
|
|
@@ -820,6 +828,7 @@ Scouts MUST NOT modify any source code files or create branches.`,
|
|
|
820
828
|
report: z.string().optional().default('').describe('Your structured scout findings in markdown (required when confirmed=true)'),
|
|
821
829
|
},
|
|
822
830
|
async ({ taskId, confirmed = false, report = '' }) => {
|
|
831
|
+
trackTaskActivity(taskId, 'scout_task')
|
|
823
832
|
const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
|
|
824
833
|
if (!taskRes?.success) return errorText('Task not found')
|
|
825
834
|
const task = taskRes.data.task
|
|
@@ -891,6 +900,7 @@ Returns systemPrompt ready to use as a Claude system prompt.`,
|
|
|
891
900
|
.describe('Agent role for this session. Falls back to task.agentRole if omitted.'),
|
|
892
901
|
},
|
|
893
902
|
async ({ taskId, role }) => {
|
|
903
|
+
trackTaskActivity(taskId, 'get_agent_context')
|
|
894
904
|
const qs = role ? `?role=${role}` : ''
|
|
895
905
|
const res = await apiWithRetry(() => api.get(`/api/tasks/${taskId}/agent-context${qs}`))
|
|
896
906
|
if (!res?.success) return errorText(res?.message || 'Failed to get agent context')
|
|
@@ -1500,6 +1510,33 @@ Flow:
|
|
|
1500
1510
|
})
|
|
1501
1511
|
}
|
|
1502
1512
|
)
|
|
1513
|
+
|
|
1514
|
+
// ── log_session_event ─────────────────────────────────────────────────────────
|
|
1515
|
+
server.tool(
|
|
1516
|
+
'log_session_event',
|
|
1517
|
+
`Log a skill invocation, subagent start, rule activation, or informational note to the task's session timeline.
|
|
1518
|
+
Call this whenever you:
|
|
1519
|
+
- Invoke a skill (type="skill", name=skill file name e.g. "scout-codebase")
|
|
1520
|
+
- Spawn a subagent (type="subagent", name=subagent name)
|
|
1521
|
+
- Activate a rule (type="rule", name=rule name)
|
|
1522
|
+
- Want to note a significant step (type="info", name=short label)
|
|
1523
|
+
The log is visible in InternalTool under the task's Session tab — gives the human full visibility into what the agent is doing.`,
|
|
1524
|
+
{
|
|
1525
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
1526
|
+
type: z.enum(['skill', 'subagent', 'rule', 'info']).describe('Event type'),
|
|
1527
|
+
name: z.string().describe('Name of the skill / subagent / rule / step'),
|
|
1528
|
+
summary: z.string().optional().default('').describe('Optional one-line description of what this event does'),
|
|
1529
|
+
role: z.string().optional().describe('Active agent role at this point (builder, scout, reviewer, coordinator)'),
|
|
1530
|
+
},
|
|
1531
|
+
async ({ taskId, type, name, summary = '', role }) => {
|
|
1532
|
+
trackTaskActivity(taskId, 'log_session_event', { role, summary: `[${type}] ${name}` })
|
|
1533
|
+
// Also push a dedicated entry with the correct event type
|
|
1534
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
1535
|
+
type, name, role: role || null, summary,
|
|
1536
|
+
}).catch(() => {})
|
|
1537
|
+
return text({ logged: true, type, name, summary })
|
|
1538
|
+
}
|
|
1539
|
+
)
|
|
1503
1540
|
}
|
|
1504
1541
|
|
|
1505
1542
|
// ── Standup activity formatter ────────────────────────────────────────────────
|
|
@@ -1998,6 +2035,7 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
1998
2035
|
.describe('Set the agent role for this task session. Role-specific behavioral constraints are injected into cursor rules. builder=implements code, scout=reads/analyzes only, reviewer=reviews PRs only, coordinator=decomposes work.'),
|
|
1999
2036
|
},
|
|
2000
2037
|
async ({ taskId, confirmed = false, repoPath, agentRole }) => {
|
|
2038
|
+
trackTaskActivity(taskId, 'kickoff_task')
|
|
2001
2039
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
2002
2040
|
if (!taskRes?.success) return errorText('Task not found')
|
|
2003
2041
|
const task = taskRes.data.task
|
|
@@ -2028,6 +2066,7 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
2028
2066
|
// ── Preview: show the full plan before touching anything ──
|
|
2029
2067
|
const pendingApv = (task.approvals || []).find(a => a.state === 'pending') || null
|
|
2030
2068
|
const hasApprovedApv = (task.approvals || []).some(a => a.state === 'approved')
|
|
2069
|
+
const approvalState = pendingApv ? 'pending' : hasApprovedApv ? 'approved' : 'none'
|
|
2031
2070
|
const approvalBlocks = ['backlog', 'todo'].includes(task.column) && !hasApprovedApv
|
|
2032
2071
|
|
|
2033
2072
|
// Build the next-step roadmap so developer knows exactly what comes after reading the plan
|
|
@@ -2227,11 +2266,18 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
2227
2266
|
|
|
2228
2267
|
// ── Confirmed: move to in_progress and fetch recent commits ──
|
|
2229
2268
|
let recentCommits = []
|
|
2269
|
+
let projectAgentConfig = null
|
|
2230
2270
|
try {
|
|
2231
2271
|
const commitsRes = await api.get(`/api/projects/${task.project}/github/commits?per_page=10`)
|
|
2232
2272
|
if (commitsRes?.success) recentCommits = commitsRes.data.commits || []
|
|
2233
2273
|
} catch { /* GitHub may not be linked */ }
|
|
2234
2274
|
|
|
2275
|
+
// Fetch project agentConfig for custom role generation
|
|
2276
|
+
try {
|
|
2277
|
+
const projRes = await api.get(`/api/projects/${task.project}`)
|
|
2278
|
+
if (projRes?.success) projectAgentConfig = projRes.data.project?.agentConfig || null
|
|
2279
|
+
} catch { /* non-fatal */ }
|
|
2280
|
+
|
|
2235
2281
|
// If the task already has a branch linked, it's safe to move to in_progress now.
|
|
2236
2282
|
// If not, create_branch will do the move once the branch is created.
|
|
2237
2283
|
const alreadyHasBranch = !!task.github?.headBranch
|
|
@@ -2252,10 +2298,21 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
2252
2298
|
cursorRulesFile = writeCursorRulesFile(task.key, '(No task-specific rules — follow role constraints above.)', repoPath, agentRole)
|
|
2253
2299
|
}
|
|
2254
2300
|
|
|
2255
|
-
//
|
|
2256
|
-
|
|
2257
|
-
|
|
2301
|
+
// Dynamically generate .cursor/agents, .cursor/skills, .cursor/commands
|
|
2302
|
+
// based on live task data — so every agent gets the right task ID, files, and role
|
|
2303
|
+
const workspaceResult = writeCursorWorkspace(task, projectAgentConfig, repoPath || process.cwd())
|
|
2304
|
+
|
|
2305
|
+
// Persist agentRole + workspace status to server
|
|
2306
|
+
const workspacePatch = {
|
|
2307
|
+
...(agentRole ? { agentRole } : {}),
|
|
2308
|
+
agentWorkspace: {
|
|
2309
|
+
kickedOffAt: new Date().toISOString(),
|
|
2310
|
+
clearedAt: null,
|
|
2311
|
+
role: agentRole || task.agentRole || null,
|
|
2312
|
+
files: workspaceResult?.written?.map(f => f.replace(workspaceResult.repoRoot, '').replace(/^\//, '')) || [],
|
|
2313
|
+
},
|
|
2258
2314
|
}
|
|
2315
|
+
api.patch(`/api/tasks/${taskId}`, workspacePatch).catch(() => {/* non-fatal */})
|
|
2259
2316
|
|
|
2260
2317
|
return text({
|
|
2261
2318
|
started: {
|
|
@@ -2282,6 +2339,9 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
2282
2339
|
cursorRulesFile: cursorRulesFile
|
|
2283
2340
|
? { written: true, path: cursorRulesFile, note: 'Rules file written to your repo. Cursor enforces it automatically on every prompt.' }
|
|
2284
2341
|
: (hasCursorRules || agentRole) ? { written: false, note: 'Could not write rules file — not inside a git repo.' } : null,
|
|
2342
|
+
cursorWorkspace: workspaceResult
|
|
2343
|
+
? { written: true, files: workspaceResult.written.map(f => f.replace(workspaceResult.repoRoot, '')), note: 'Agent, skill, and command files generated dynamically from task data. Reload Cursor Settings → Rules, Skills, Subagents to see them.' }
|
|
2344
|
+
: { written: false, note: 'Could not generate workspace files — not inside a git repo.' },
|
|
2285
2345
|
recentCommits: recentCommits.slice(0, 5).map(c => ({
|
|
2286
2346
|
sha: c.sha?.slice(0, 7),
|
|
2287
2347
|
message: c.commit?.message?.split('\n')[0],
|
|
@@ -2824,6 +2884,202 @@ function writeCursorRulesFile(taskKey, rulesMarkdown, startPath, role = null) {
|
|
|
2824
2884
|
}
|
|
2825
2885
|
}
|
|
2826
2886
|
|
|
2887
|
+
/**
|
|
2888
|
+
* Dynamically generate .cursor/ workspace files from live InternalTool data.
|
|
2889
|
+
*
|
|
2890
|
+
* Sources (in priority order):
|
|
2891
|
+
* 1. project.agentConfig.subagents[] → .cursor/agents/<name>.md
|
|
2892
|
+
* 2. project.agentConfig.skills[] → .cursor/skills/<name>.md
|
|
2893
|
+
* 3. project.agentConfig.rules[] → .cursor/rules/<name>.mdc (project-level, alwaysApply)
|
|
2894
|
+
* 4. Built-in ROLE_RULES / default skill templates (fallback when DB arrays are empty)
|
|
2895
|
+
*
|
|
2896
|
+
* Task-specific agent file (.cursor/agents/active-agent.md) is always written
|
|
2897
|
+
* from the task's role + claimed files + task ID so Cursor knows exactly what to do.
|
|
2898
|
+
* It is deleted by deleteCursorWorkspace() when the task is parked or completed.
|
|
2899
|
+
*/
|
|
2900
|
+
function writeCursorWorkspace(task, projectAgentConfig, startPath) {
|
|
2901
|
+
try {
|
|
2902
|
+
const repoRoot = findRepoRoot(startPath)
|
|
2903
|
+
if (!repoRoot) return null
|
|
2904
|
+
|
|
2905
|
+
const role = task.agentRole || null
|
|
2906
|
+
const taskId = String(task._id)
|
|
2907
|
+
const taskKey = task.key || 'TASK-???'
|
|
2908
|
+
const taskTitle = task.title || ''
|
|
2909
|
+
const claimedFiles = task.claimedFiles || []
|
|
2910
|
+
const hasScout = !!(task.scoutReport?.trim())
|
|
2911
|
+
const hasDec = !!(task.decomposition?.trim())
|
|
2912
|
+
const cfg = projectAgentConfig || {}
|
|
2913
|
+
|
|
2914
|
+
const agentsDir = join(repoRoot, '.cursor', 'agents')
|
|
2915
|
+
const skillsDir = join(repoRoot, '.cursor', 'skills')
|
|
2916
|
+
const commandsDir = join(repoRoot, '.cursor', 'commands')
|
|
2917
|
+
const rulesDir = join(repoRoot, '.cursor', 'rules')
|
|
2918
|
+
mkdirSync(agentsDir, { recursive: true })
|
|
2919
|
+
mkdirSync(skillsDir, { recursive: true })
|
|
2920
|
+
mkdirSync(commandsDir, { recursive: true })
|
|
2921
|
+
mkdirSync(rulesDir, { recursive: true })
|
|
2922
|
+
|
|
2923
|
+
const written = []
|
|
2924
|
+
|
|
2925
|
+
// ── 1. Project-level rules from DB → .cursor/rules/<name>.mdc ─────────────
|
|
2926
|
+
const projectRules = cfg.rules || []
|
|
2927
|
+
for (const r of projectRules) {
|
|
2928
|
+
if (!r.name || !r.body) continue
|
|
2929
|
+
const content = `---\ndescription: ${r.description || r.name} — project rule managed in InternalTool. Do not edit manually.\nalwaysApply: ${r.alwaysApply ? 'true' : 'false'}\n---\n\n${r.body}\n`
|
|
2930
|
+
const f = join(rulesDir, `${r.name}.mdc`)
|
|
2931
|
+
writeFileSync(f, content, 'utf8')
|
|
2932
|
+
written.push(f)
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// ── 2. Subagents from DB → .cursor/agents/<name>.md ───────────────────────
|
|
2936
|
+
// If DB has subagents defined, write them verbatim (admin-authored markdown)
|
|
2937
|
+
const dbSubagents = cfg.subagents || []
|
|
2938
|
+
for (const s of dbSubagents) {
|
|
2939
|
+
if (!s.name || !s.body) continue
|
|
2940
|
+
const header = `---\nname: ${s.name}\ndescription: ${s.description || s.name}\n---\n\n`
|
|
2941
|
+
const f = join(agentsDir, `${s.name}.md`)
|
|
2942
|
+
writeFileSync(f, header + s.body, 'utf8')
|
|
2943
|
+
written.push(f)
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// ── 3. Task-specific active-agent.md — always written, deleted on done ────
|
|
2947
|
+
// Uses DB subagent body as template if one matches the role; falls back to
|
|
2948
|
+
// built-in ROLE_RULES template with task-specific data injected.
|
|
2949
|
+
if (role) {
|
|
2950
|
+
const dbMatch = dbSubagents.find(s => s.name === `${role}-agent` || s.name === role)
|
|
2951
|
+
let agentBody
|
|
2952
|
+
if (dbMatch?.body) {
|
|
2953
|
+
// Admin wrote a custom body — inject task data into placeholder tokens
|
|
2954
|
+
agentBody = dbMatch.body
|
|
2955
|
+
.replace(/\{\{taskId\}\}/g, taskId)
|
|
2956
|
+
.replace(/\{\{taskKey\}\}/g, taskKey)
|
|
2957
|
+
.replace(/\{\{taskTitle\}\}/g, taskTitle)
|
|
2958
|
+
.replace(/\{\{claimedFiles\}\}/g, claimedFiles.map(f => `- \`${f}\``).join('\n') || '(none)')
|
|
2959
|
+
} else {
|
|
2960
|
+
// Built-in fallback template
|
|
2961
|
+
const fileLine = claimedFiles.length
|
|
2962
|
+
? claimedFiles.map(f => `- \`${f}\``).join('\n')
|
|
2963
|
+
: '- (no files claimed yet — call claim_files first)'
|
|
2964
|
+
const scoutLine = hasScout ? '✅ Scout report available — call `get_agent_context` to read it.' : '⚠️ No scout report yet.'
|
|
2965
|
+
const decLine = hasDec ? '✅ Execution plan exists — follow the decomposition order.' : ''
|
|
2966
|
+
|
|
2967
|
+
const BUILT_IN = {
|
|
2968
|
+
scout: `# Scout Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n## Constraints\n- READ ONLY — do not write or modify any file\n- You CAN read files, run tests, and use MCP tools\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\`\n2. Read every source file systematically\n3. Call \`scout_task\` with confirmed=false, then confirmed=true + your report\n`,
|
|
2969
|
+
builder: `# Builder Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n**Claimed files:**\n${fileLine}\n\n${scoutLine}\n${decLine}\n\n## Constraints\n- Only modify your claimed files\n- Every change needs a test update\n- Run tests before marking done\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\`\n2. Implement the feature\n3. Use the \`run-tests\` skill to verify\n4. Call \`commit_helper\` then \`raise_pr\`\n`,
|
|
2970
|
+
coordinator: `# Coordinator Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n## Constraints\n- Do NOT write code — plan and delegate only\n- Each subtask must have exclusive file ownership\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\`\n2. Break work into subtasks with role + files + description\n3. Call \`decompose_task\` with taskId \`${taskId}\` to save the plan\n`,
|
|
2971
|
+
reviewer: `# Reviewer Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n## Constraints\n- Read the full diff before approving\n- Post specific, file-level comments\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\`\n2. Call \`review_pr\` to fetch the diff\n3. Call \`post_pr_review\` with your verdict\n`,
|
|
2972
|
+
}
|
|
2973
|
+
agentBody = BUILT_IN[role] || `# ${role} Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n`
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
const activeAgentContent = `---\nname: active-agent\ndescription: ${role} agent for ${taskKey} — auto-generated by InternalTool MCP on kickoff. Deleted when task is parked or done.\n---\n\n${agentBody}`
|
|
2977
|
+
writeFileSync(join(agentsDir, 'active-agent.md'), activeAgentContent, 'utf8')
|
|
2978
|
+
written.push(join(agentsDir, 'active-agent.md'))
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
// ── 4. Custom role agents from project.agentConfig.roles (non-subagent) ───
|
|
2982
|
+
for (const cr of (cfg.roles || [])) {
|
|
2983
|
+
if (!cr.name || dbSubagents.some(s => s.name === `${cr.name}-agent`)) continue
|
|
2984
|
+
const content = `---\nname: ${cr.name}-agent\ndescription: ${cr.label || cr.name}${cr.promptHint ? ' — ' + cr.promptHint.slice(0, 80) : ''}\n---\n\n# ${cr.label || cr.name} Agent\n\n**Role:** \`${cr.name}\`\n${cr.promptHint ? `\n## Instructions\n${cr.promptHint}\n` : ''}${cr.allowedTools?.length ? `\n## Allowed tools\n${cr.allowedTools.map(t => `- ${t}`).join('\n')}\n` : ''}`
|
|
2985
|
+
const f = join(agentsDir, `${cr.name}-agent.md`)
|
|
2986
|
+
writeFileSync(f, content, 'utf8')
|
|
2987
|
+
written.push(f)
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// ── 5. Skills from DB → .cursor/skills/<name>.md ──────────────────────────
|
|
2991
|
+
// If DB is empty, write built-in defaults so Cursor always has something useful.
|
|
2992
|
+
const dbSkills = cfg.skills || []
|
|
2993
|
+
if (dbSkills.length > 0) {
|
|
2994
|
+
for (const s of dbSkills) {
|
|
2995
|
+
if (!s.name || !s.body) continue
|
|
2996
|
+
const header = `---\nname: ${s.name}\ndescription: ${s.description || s.name}\n---\n\n`
|
|
2997
|
+
const f = join(skillsDir, `${s.name}.md`)
|
|
2998
|
+
writeFileSync(f, header + s.body, 'utf8')
|
|
2999
|
+
written.push(f)
|
|
3000
|
+
}
|
|
3001
|
+
} else {
|
|
3002
|
+
// Built-in default skills
|
|
3003
|
+
const defaults = [
|
|
3004
|
+
{
|
|
3005
|
+
name: 'scout-codebase',
|
|
3006
|
+
description: 'Read every source file and save a scout report to InternalTool.',
|
|
3007
|
+
body: `1. Read all source files: routes, models, middleware, utils, tests, README, .cursorrules\n2. Write a report: Auth Flow, Data Model, API Surface, Utilities, Test Coverage, Gaps & Risks\n3. Call \`scout_task\` with taskId \`${taskId}\` confirmed=false (preview), then confirmed=true + report\n\n**READ ONLY — do not modify any file.**`,
|
|
3008
|
+
},
|
|
3009
|
+
{
|
|
3010
|
+
name: 'run-tests',
|
|
3011
|
+
description: 'Run the test suite and fix any failures before marking done.',
|
|
3012
|
+
body: `1. Run \`npm test\`\n2. Report: passed / failed / skipped\n3. For each failure: identify root cause, fix source (not the test)\n4. Re-run until 0 failures\n5. Do NOT mark done until tests are green`,
|
|
3013
|
+
},
|
|
3014
|
+
]
|
|
3015
|
+
for (const s of defaults) {
|
|
3016
|
+
const content = `---\nname: ${s.name}\ndescription: ${s.description}\n---\n\n${s.body}\n`
|
|
3017
|
+
const f = join(skillsDir, `${s.name}.md`)
|
|
3018
|
+
writeFileSync(f, content, 'utf8')
|
|
3019
|
+
written.push(f)
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// ── 6. Command — start-task pre-filled with this task ─────────────────────
|
|
3024
|
+
const startCmd = `---\nname: start-task\ndescription: Start a task from InternalTool — brief, role rules, moves to In Progress.\n---\n\nDefault task: \`${taskId}\` (${taskKey} — ${taskTitle})\n\n1. Ask the user to confirm the task ID or use the default above\n2. Call \`kickoff_task\` with confirmed=false to show the brief\n3. Present: role, description, claimedFiles, cursorRules\n4. Ask "Ready to confirm and move to In Progress?"\n5. If yes, call \`kickoff_task\` again with confirmed=true\n`
|
|
3025
|
+
writeFileSync(join(commandsDir, 'start-task.md'), startCmd, 'utf8')
|
|
3026
|
+
written.push(join(commandsDir, 'start-task.md'))
|
|
3027
|
+
|
|
3028
|
+
return { repoRoot, written }
|
|
3029
|
+
} catch {
|
|
3030
|
+
return null
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
/**
|
|
3035
|
+
* Delete task-specific workspace files written at kickoff.
|
|
3036
|
+
* Project-level rules (.cursor/rules/<project-rule>.mdc) and skills are kept.
|
|
3037
|
+
* Only the active-agent and start-task command are removed (they are task-scoped).
|
|
3038
|
+
*/
|
|
3039
|
+
function deleteCursorWorkspace(role, startPath) {
|
|
3040
|
+
const deleted = []
|
|
3041
|
+
try {
|
|
3042
|
+
const repoRoot = findRepoRoot(startPath)
|
|
3043
|
+
if (!repoRoot) return deleted
|
|
3044
|
+
const toDelete = [
|
|
3045
|
+
join(repoRoot, '.cursor', 'agents', 'active-agent.md'),
|
|
3046
|
+
join(repoRoot, '.cursor', 'commands', 'start-task.md'),
|
|
3047
|
+
]
|
|
3048
|
+
for (const f of toDelete) {
|
|
3049
|
+
try {
|
|
3050
|
+
if (existsSync(f)) { unlinkSync(f); deleted.push(f) }
|
|
3051
|
+
} catch { /* non-fatal */ }
|
|
3052
|
+
}
|
|
3053
|
+
} catch { /* non-fatal */ }
|
|
3054
|
+
return deleted
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
/**
|
|
3058
|
+
* Record that a specific MCP tool was called for a task.
|
|
3059
|
+
* 1. Updates agentWorkspace.lastActivityAt + lastTool (fast summary for overview card)
|
|
3060
|
+
* 2. Appends to agentSessionLog (the ordered call history shown in Session tab)
|
|
3061
|
+
* Fire-and-forget: never blocks the tool response.
|
|
3062
|
+
*/
|
|
3063
|
+
function trackTaskActivity(taskId, toolName, opts = {}) {
|
|
3064
|
+
if (!taskId) return
|
|
3065
|
+
// Fast summary update
|
|
3066
|
+
api.patch(`/api/tasks/${taskId}`, {
|
|
3067
|
+
agentWorkspace: {
|
|
3068
|
+
lastActivityAt: new Date().toISOString(),
|
|
3069
|
+
lastTool: toolName,
|
|
3070
|
+
_incCallCount: true,
|
|
3071
|
+
...(opts.role ? { role: opts.role } : {}),
|
|
3072
|
+
},
|
|
3073
|
+
}).catch(() => {})
|
|
3074
|
+
// Ordered session log entry
|
|
3075
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
3076
|
+
type: 'tool',
|
|
3077
|
+
name: toolName,
|
|
3078
|
+
role: opts.role || null,
|
|
3079
|
+
summary: opts.summary || '',
|
|
3080
|
+
}).catch(() => {})
|
|
3081
|
+
}
|
|
3082
|
+
|
|
2827
3083
|
/** Delete the task-specific cursor rules file when work is complete. */
|
|
2828
3084
|
function deleteCursorRulesFile(taskKey, startPath) {
|
|
2829
3085
|
try {
|
|
@@ -3278,6 +3534,7 @@ Use this when returning to a task after a break, or when Claude needs the full p
|
|
|
3278
3534
|
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory)'),
|
|
3279
3535
|
},
|
|
3280
3536
|
async ({ taskId, repoPath }) => {
|
|
3537
|
+
trackTaskActivity(taskId, 'get_task_context')
|
|
3281
3538
|
let taskRes, activityRes
|
|
3282
3539
|
try {
|
|
3283
3540
|
[taskRes, activityRes] = await Promise.all([
|
|
@@ -3414,7 +3671,9 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
|
|
|
3414
3671
|
// ── Approval gate check ───────────────────────────────────────────────────
|
|
3415
3672
|
// The server blocks non-admins from moving todo → in_progress without approval.
|
|
3416
3673
|
// Detect this early and guide the developer instead of failing silently after branch creation.
|
|
3674
|
+
const pendingApv2 = (task.approvals || []).find(a => a.state === 'pending') || null
|
|
3417
3675
|
const hasApprovedApv2 = (task.approvals || []).some(a => a.state === 'approved')
|
|
3676
|
+
const approvalState = pendingApv2 ? 'pending' : hasApprovedApv2 ? 'approved' : 'none'
|
|
3418
3677
|
const PLANNING_COLS = ['backlog', 'todo']
|
|
3419
3678
|
const needsApproval = PLANNING_COLS.includes(task.column) && !hasApprovedApv2
|
|
3420
3679
|
if (needsApproval && !confirmed) {
|
package/package.json
CHANGED