@hanhnd/agent-kit 1.0.24 → 1.0.26
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/README.md +1 -1
- package/dist/kit-server.js +89 -46
- package/dist/tools/agent.js +3 -3
- package/dist/tools/core.js +11 -20
- package/dist/tools/handoffs.d.ts +22 -0
- package/dist/tools/handoffs.js +57 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Agent Kit ships these workflows as skills. In Claude Code, invoke them as slash
|
|
|
18
18
|
| `/code [plan or report]` | Implement from a WBS plan or investigation report |
|
|
19
19
|
| `/test [intent]` | Add or update focused tests after implementation intent exists |
|
|
20
20
|
| `/code-review [diff or target]` | Review diffs, PRs, or commits with evidence-backed findings |
|
|
21
|
+
| `/e2e-review [diff or target]` | Review diffs, PRs, or commits with evidence-backed findings |
|
|
21
22
|
| `/review [base]` | Review local staged and unstaged changes |
|
|
22
23
|
| `/review-pr [PR URL]` | Fetch PR/Jira context, check out the branch, and run code review |
|
|
23
24
|
| `/code-simplify [target]` | Improve readability without changing behavior or public shape |
|
|
@@ -27,7 +28,6 @@ Agent Kit ships these workflows as skills. In Claude Code, invoke them as slash
|
|
|
27
28
|
| `/debate [subject]` | Run adversarial validation of an analysis, review, or plan |
|
|
28
29
|
| `/ticket [ID]` | Fetch a Jira ticket and route it into the planning pipeline |
|
|
29
30
|
| `/init` | Extract project DNA for downstream coding and planning workflows |
|
|
30
|
-
| `/orchestrate [file or idea]` | Decompose cross-service work into scoped handoff briefs |
|
|
31
31
|
| `/delegate <agent> <task>` | Delegate to Gemini, Claude, or Codex CLI with optional handoff |
|
|
32
32
|
| `/wiki [compile\|query\|lint]` | Maintain and query the persistent project knowledge wiki |
|
|
33
33
|
|
package/dist/kit-server.js
CHANGED
|
@@ -3223,8 +3223,8 @@ var require_utils = __commonJS({
|
|
|
3223
3223
|
}
|
|
3224
3224
|
return ind;
|
|
3225
3225
|
}
|
|
3226
|
-
function removeDotSegments(
|
|
3227
|
-
let input =
|
|
3226
|
+
function removeDotSegments(path6) {
|
|
3227
|
+
let input = path6;
|
|
3228
3228
|
const output = [];
|
|
3229
3229
|
let nextSlash = -1;
|
|
3230
3230
|
let len = 0;
|
|
@@ -3423,8 +3423,8 @@ var require_schemes = __commonJS({
|
|
|
3423
3423
|
wsComponent.secure = void 0;
|
|
3424
3424
|
}
|
|
3425
3425
|
if (wsComponent.resourceName) {
|
|
3426
|
-
const [
|
|
3427
|
-
wsComponent.path =
|
|
3426
|
+
const [path6, query] = wsComponent.resourceName.split("?");
|
|
3427
|
+
wsComponent.path = path6 && path6 !== "/" ? path6 : void 0;
|
|
3428
3428
|
wsComponent.query = query;
|
|
3429
3429
|
wsComponent.resourceName = void 0;
|
|
3430
3430
|
}
|
|
@@ -7277,8 +7277,8 @@ function getErrorMap() {
|
|
|
7277
7277
|
|
|
7278
7278
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
7279
7279
|
var makeIssue = (params) => {
|
|
7280
|
-
const { data, path:
|
|
7281
|
-
const fullPath = [...
|
|
7280
|
+
const { data, path: path6, errorMaps, issueData } = params;
|
|
7281
|
+
const fullPath = [...path6, ...issueData.path || []];
|
|
7282
7282
|
const fullIssue = {
|
|
7283
7283
|
...issueData,
|
|
7284
7284
|
path: fullPath
|
|
@@ -7394,11 +7394,11 @@ var errorUtil;
|
|
|
7394
7394
|
|
|
7395
7395
|
// ../../node_modules/zod/v3/types.js
|
|
7396
7396
|
var ParseInputLazyPath = class {
|
|
7397
|
-
constructor(parent, value,
|
|
7397
|
+
constructor(parent, value, path6, key) {
|
|
7398
7398
|
this._cachedPath = [];
|
|
7399
7399
|
this.parent = parent;
|
|
7400
7400
|
this.data = value;
|
|
7401
|
-
this._path =
|
|
7401
|
+
this._path = path6;
|
|
7402
7402
|
this._key = key;
|
|
7403
7403
|
}
|
|
7404
7404
|
get path() {
|
|
@@ -11036,10 +11036,10 @@ function assignProp(target, prop, value) {
|
|
|
11036
11036
|
configurable: true
|
|
11037
11037
|
});
|
|
11038
11038
|
}
|
|
11039
|
-
function getElementAtPath(obj,
|
|
11040
|
-
if (!
|
|
11039
|
+
function getElementAtPath(obj, path6) {
|
|
11040
|
+
if (!path6)
|
|
11041
11041
|
return obj;
|
|
11042
|
-
return
|
|
11042
|
+
return path6.reduce((acc, key) => acc?.[key], obj);
|
|
11043
11043
|
}
|
|
11044
11044
|
function promiseAllObject(promisesObj) {
|
|
11045
11045
|
const keys = Object.keys(promisesObj);
|
|
@@ -11359,11 +11359,11 @@ function aborted(x, startIndex = 0) {
|
|
|
11359
11359
|
}
|
|
11360
11360
|
return false;
|
|
11361
11361
|
}
|
|
11362
|
-
function prefixIssues(
|
|
11362
|
+
function prefixIssues(path6, issues) {
|
|
11363
11363
|
return issues.map((iss) => {
|
|
11364
11364
|
var _a;
|
|
11365
11365
|
(_a = iss).path ?? (_a.path = []);
|
|
11366
|
-
iss.path.unshift(
|
|
11366
|
+
iss.path.unshift(path6);
|
|
11367
11367
|
return iss;
|
|
11368
11368
|
});
|
|
11369
11369
|
}
|
|
@@ -20991,7 +20991,7 @@ var StdioServerTransport = class {
|
|
|
20991
20991
|
|
|
20992
20992
|
// src/tools/core.ts
|
|
20993
20993
|
import * as fs from "fs";
|
|
20994
|
-
import * as
|
|
20994
|
+
import * as path2 from "path";
|
|
20995
20995
|
|
|
20996
20996
|
// src/utils/utils.ts
|
|
20997
20997
|
function getWorkspaceRoot() {
|
|
@@ -21045,13 +21045,62 @@ var FORBIDDEN_DIRS = [
|
|
|
21045
21045
|
".docker"
|
|
21046
21046
|
];
|
|
21047
21047
|
|
|
21048
|
+
// src/tools/handoffs.ts
|
|
21049
|
+
import * as path from "path";
|
|
21050
|
+
var HANDOFF_TYPES = [
|
|
21051
|
+
"brainstorm",
|
|
21052
|
+
"clarification",
|
|
21053
|
+
"plan",
|
|
21054
|
+
"ticket",
|
|
21055
|
+
"research",
|
|
21056
|
+
"scenario",
|
|
21057
|
+
"investigation"
|
|
21058
|
+
];
|
|
21059
|
+
var TICKET_ID_PATTERN = /\b[A-Za-z][A-Za-z0-9]+-\d+\b/;
|
|
21060
|
+
function findTicketId(value) {
|
|
21061
|
+
return value.match(TICKET_ID_PATTERN)?.[0].toLowerCase() ?? null;
|
|
21062
|
+
}
|
|
21063
|
+
function sanitizeFeatureSlug(value) {
|
|
21064
|
+
return value.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
21065
|
+
}
|
|
21066
|
+
function contentSlugCandidate(content) {
|
|
21067
|
+
const heading = content.split(/\r?\n/).map((line) => line.replace(/^#+\s*/, "").trim()).find((line) => line.length > 0);
|
|
21068
|
+
return heading ?? content.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
21069
|
+
}
|
|
21070
|
+
function normalizeHandoffType(type) {
|
|
21071
|
+
if (!HANDOFF_TYPES.includes(type)) {
|
|
21072
|
+
throw new Error(`Unsupported handoff type: ${type}`);
|
|
21073
|
+
}
|
|
21074
|
+
return type;
|
|
21075
|
+
}
|
|
21076
|
+
function deriveFeatureSlug(input) {
|
|
21077
|
+
const ticketSlug = findTicketId(input.requestedSlug) ?? findTicketId(input.content);
|
|
21078
|
+
if (ticketSlug) return ticketSlug;
|
|
21079
|
+
return sanitizeFeatureSlug(input.requestedSlug) || sanitizeFeatureSlug(contentSlugCandidate(input.content)) || "untitled-handoff";
|
|
21080
|
+
}
|
|
21081
|
+
function resolveHandoffPath(input) {
|
|
21082
|
+
const canonicalType = normalizeHandoffType(input.type);
|
|
21083
|
+
const featureSlug = deriveFeatureSlug({
|
|
21084
|
+
requestedSlug: input.slug,
|
|
21085
|
+
content: input.content,
|
|
21086
|
+
type: canonicalType
|
|
21087
|
+
});
|
|
21088
|
+
const relativePath = path.join(".agent-kit", "handoffs", featureSlug, `${canonicalType}.md`);
|
|
21089
|
+
return {
|
|
21090
|
+
featureSlug,
|
|
21091
|
+
canonicalType,
|
|
21092
|
+
filePath: path.join(input.workspaceRoot, relativePath),
|
|
21093
|
+
relativePath
|
|
21094
|
+
};
|
|
21095
|
+
}
|
|
21096
|
+
|
|
21048
21097
|
// src/tools/core.ts
|
|
21049
21098
|
function registerCoreTools(server2) {
|
|
21050
21099
|
server2.tool(
|
|
21051
21100
|
"kit_save_handoff",
|
|
21052
21101
|
`Save a handoff artifact to .agent-kit/handoffs/. Returns the saved file path to use in next-step instructions. Do NOT append version numbers (v2, v3, etc.) to the slug.`,
|
|
21053
21102
|
{
|
|
21054
|
-
type: external_exports.enum(
|
|
21103
|
+
type: external_exports.enum(HANDOFF_TYPES).describe("Handoff type"),
|
|
21055
21104
|
content: external_exports.string().describe("Full markdown content to save"),
|
|
21056
21105
|
slug: external_exports.string().describe(
|
|
21057
21106
|
'Short identifier for the filename, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'
|
|
@@ -21059,24 +21108,18 @@ function registerCoreTools(server2) {
|
|
|
21059
21108
|
},
|
|
21060
21109
|
async ({ type, content, slug }) => {
|
|
21061
21110
|
try {
|
|
21062
|
-
const
|
|
21063
|
-
|
|
21111
|
+
const location = resolveHandoffPath({
|
|
21112
|
+
workspaceRoot: getWorkspaceRoot(),
|
|
21113
|
+
type,
|
|
21114
|
+
slug,
|
|
21115
|
+
content
|
|
21116
|
+
});
|
|
21117
|
+
const handoffDir = path2.dirname(location.filePath);
|
|
21064
21118
|
if (!fs.existsSync(handoffDir)) {
|
|
21065
21119
|
fs.mkdirSync(handoffDir, { recursive: true });
|
|
21066
21120
|
}
|
|
21067
|
-
|
|
21068
|
-
|
|
21069
|
-
const existingFile = files.filter((f) => slugPattern.test(f)).sort().pop();
|
|
21070
|
-
let filePath;
|
|
21071
|
-
if (existingFile) {
|
|
21072
|
-
filePath = path.join(handoffDir, existingFile);
|
|
21073
|
-
} else {
|
|
21074
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
21075
|
-
const filename = `${type}-${timestamp}-${safeSlug}.md`;
|
|
21076
|
-
filePath = path.join(handoffDir, filename);
|
|
21077
|
-
}
|
|
21078
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
21079
|
-
return mcpText(`\u2705 Saved to: ${filePath}`);
|
|
21121
|
+
fs.writeFileSync(location.filePath, content, "utf8");
|
|
21122
|
+
return mcpText(`\u2705 Saved to: ${location.filePath}`);
|
|
21080
21123
|
} catch (error2) {
|
|
21081
21124
|
return mcpText(`Error saving handoff: ${error2}`);
|
|
21082
21125
|
}
|
|
@@ -21088,27 +21131,27 @@ function registerCoreTools(server2) {
|
|
|
21088
21131
|
import { spawn } from "child_process";
|
|
21089
21132
|
import { randomUUID } from "crypto";
|
|
21090
21133
|
import * as fs2 from "fs";
|
|
21091
|
-
import * as
|
|
21134
|
+
import * as path4 from "path";
|
|
21092
21135
|
|
|
21093
21136
|
// src/tools/security.ts
|
|
21094
21137
|
import { execFileSync } from "child_process";
|
|
21095
|
-
import * as
|
|
21138
|
+
import * as path3 from "path";
|
|
21096
21139
|
function sanitize(input) {
|
|
21097
21140
|
return String(input).replace(/[;&|`$<>\\]/g, "").trim().slice(0, 500);
|
|
21098
21141
|
}
|
|
21099
21142
|
function validatePath(filePath, baseDir = process.cwd()) {
|
|
21100
|
-
const resolved =
|
|
21101
|
-
const root =
|
|
21102
|
-
if (resolved !== root && !resolved.startsWith(root +
|
|
21143
|
+
const resolved = path3.resolve(baseDir, filePath);
|
|
21144
|
+
const root = path3.resolve(baseDir);
|
|
21145
|
+
if (resolved !== root && !resolved.startsWith(root + path3.sep)) {
|
|
21103
21146
|
throw new Error(`Path traversal detected: ${filePath}`);
|
|
21104
21147
|
}
|
|
21105
|
-
const segments = resolved.split(
|
|
21148
|
+
const segments = resolved.split(path3.sep);
|
|
21106
21149
|
for (const segment of segments) {
|
|
21107
21150
|
if (FORBIDDEN_DIRS.some((d) => segment.toLowerCase() === d.toLowerCase())) {
|
|
21108
21151
|
throw new Error(`Access to sensitive directory is FORBIDDEN: ${segment}`);
|
|
21109
21152
|
}
|
|
21110
21153
|
}
|
|
21111
|
-
const fileName =
|
|
21154
|
+
const fileName = path3.basename(resolved);
|
|
21112
21155
|
const isExactForbidden = FORBIDDEN_FILES.some((f) => fileName.toLowerCase() === f.toLowerCase());
|
|
21113
21156
|
const isPatternForbidden = FORBIDDEN_PATTERNS.some((re) => re.test(fileName));
|
|
21114
21157
|
if (isExactForbidden || isPatternForbidden) {
|
|
@@ -21167,13 +21210,13 @@ function agentArgs(agent, prompt, workspaceRoot) {
|
|
|
21167
21210
|
return ["--dangerously-skip-permissions", "-p", prompt];
|
|
21168
21211
|
case "codex":
|
|
21169
21212
|
return [
|
|
21170
|
-
"exec",
|
|
21171
21213
|
"--ask-for-approval",
|
|
21172
21214
|
"never",
|
|
21173
21215
|
"--sandbox",
|
|
21174
21216
|
"workspace-write",
|
|
21175
21217
|
"--cd",
|
|
21176
21218
|
workspaceRoot,
|
|
21219
|
+
"exec",
|
|
21177
21220
|
prompt
|
|
21178
21221
|
];
|
|
21179
21222
|
}
|
|
@@ -21182,10 +21225,10 @@ function taskSlug(task) {
|
|
|
21182
21225
|
return task.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 6).join("-").slice(0, 48);
|
|
21183
21226
|
}
|
|
21184
21227
|
function initAgentLog(workspaceRoot, slug, agent) {
|
|
21185
|
-
const logsDir =
|
|
21228
|
+
const logsDir = path4.join(workspaceRoot, ".agent-kit", "logs", "agents");
|
|
21186
21229
|
fs2.mkdirSync(logsDir, { recursive: true });
|
|
21187
21230
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21188
|
-
const logFile =
|
|
21231
|
+
const logFile = path4.join(logsDir, `delegate-${now}-${slug}.log`);
|
|
21189
21232
|
const header = `Agent: ${agent}
|
|
21190
21233
|
Date: ${now}
|
|
21191
21234
|
${"\u2500".repeat(60)}
|
|
@@ -21195,16 +21238,16 @@ ${"\u2500".repeat(60)}
|
|
|
21195
21238
|
const logStream = fs2.createWriteStream(logFile, { flags: "a" });
|
|
21196
21239
|
logStream.on("error", () => {
|
|
21197
21240
|
});
|
|
21198
|
-
return { logPath:
|
|
21241
|
+
return { logPath: path4.relative(workspaceRoot, logFile), logStream };
|
|
21199
21242
|
}
|
|
21200
21243
|
function registerAgentTools(server2) {
|
|
21201
21244
|
server2.tool(
|
|
21202
21245
|
"kit_trigger_agent",
|
|
21203
|
-
"!Important: Trigger only when user explicitly asks to delegate a task to an external agent CLI (gemini, claude, or codex). The task can be a direct message or a path to a handoff file (.agent-kit/handoffs/
|
|
21246
|
+
"!Important: Trigger only when user explicitly asks to delegate a task to an external agent CLI (gemini, claude, or codex). The task can be a direct message or a path to a handoff file (.agent-kit/handoffs/feature-slug/plan.md).",
|
|
21204
21247
|
{
|
|
21205
21248
|
agent: external_exports.enum(["gemini", "claude", "codex"]).describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
|
|
21206
21249
|
task: external_exports.string().describe(
|
|
21207
|
-
'Task message or path to a handoff file (e.g., ".agent-kit/handoffs/
|
|
21250
|
+
'Task message or path to a handoff file (e.g., ".agent-kit/handoffs/feature-slug/plan.md")'
|
|
21208
21251
|
)
|
|
21209
21252
|
},
|
|
21210
21253
|
async ({ agent, task }, extra) => {
|
|
@@ -21214,7 +21257,7 @@ function registerAgentTools(server2) {
|
|
|
21214
21257
|
let killChild;
|
|
21215
21258
|
try {
|
|
21216
21259
|
let prompt = task;
|
|
21217
|
-
const resolvedTask =
|
|
21260
|
+
const resolvedTask = path4.resolve(workspaceRoot, task);
|
|
21218
21261
|
if (fs2.existsSync(resolvedTask) && fs2.statSync(resolvedTask).isFile()) {
|
|
21219
21262
|
try {
|
|
21220
21263
|
validatePath(task, workspaceRoot);
|
|
@@ -21321,8 +21364,8 @@ import { writeFileSync as writeFileSync3 } from "fs";
|
|
|
21321
21364
|
// src/utils/credentials.ts
|
|
21322
21365
|
import * as fs3 from "fs";
|
|
21323
21366
|
import * as os from "os";
|
|
21324
|
-
import * as
|
|
21325
|
-
var CREDENTIALS_FILE =
|
|
21367
|
+
import * as path5 from "path";
|
|
21368
|
+
var CREDENTIALS_FILE = path5.join(os.homedir(), ".claude", "credentials");
|
|
21326
21369
|
var cachedCredentials = null;
|
|
21327
21370
|
function parseIni(content) {
|
|
21328
21371
|
const result = {};
|
package/dist/tools/agent.js
CHANGED
|
@@ -25,13 +25,13 @@ function agentArgs(agent, prompt, workspaceRoot) {
|
|
|
25
25
|
return ['--dangerously-skip-permissions', '-p', prompt];
|
|
26
26
|
case 'codex':
|
|
27
27
|
return [
|
|
28
|
-
'exec',
|
|
29
28
|
'--ask-for-approval',
|
|
30
29
|
'never',
|
|
31
30
|
'--sandbox',
|
|
32
31
|
'workspace-write',
|
|
33
32
|
'--cd',
|
|
34
33
|
workspaceRoot,
|
|
34
|
+
'exec',
|
|
35
35
|
prompt,
|
|
36
36
|
];
|
|
37
37
|
}
|
|
@@ -58,13 +58,13 @@ function initAgentLog(workspaceRoot, slug, agent) {
|
|
|
58
58
|
return { logPath: path.relative(workspaceRoot, logFile), logStream };
|
|
59
59
|
}
|
|
60
60
|
export function registerAgentTools(server) {
|
|
61
|
-
server.tool('kit_trigger_agent', '!Important: Trigger only when user explicitly asks to delegate a task to an external agent CLI (gemini, claude, or codex). The task can be a direct message or a path to a handoff file (.agent-kit/handoffs/
|
|
61
|
+
server.tool('kit_trigger_agent', '!Important: Trigger only when user explicitly asks to delegate a task to an external agent CLI (gemini, claude, or codex). The task can be a direct message or a path to a handoff file (.agent-kit/handoffs/feature-slug/plan.md).', {
|
|
62
62
|
agent: z
|
|
63
63
|
.enum(['gemini', 'claude', 'codex'])
|
|
64
64
|
.describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
|
|
65
65
|
task: z
|
|
66
66
|
.string()
|
|
67
|
-
.describe('Task message or path to a handoff file (e.g., ".agent-kit/handoffs/
|
|
67
|
+
.describe('Task message or path to a handoff file (e.g., ".agent-kit/handoffs/feature-slug/plan.md")'),
|
|
68
68
|
}, async ({ agent, task }, extra) => {
|
|
69
69
|
const workspaceRoot = getWorkspaceRoot();
|
|
70
70
|
let job;
|
package/dist/tools/core.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as path from 'path';
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { getWorkspaceRoot, mcpText } from '../utils/utils.js';
|
|
8
8
|
import { DEFAULT_EXTENSIONS } from './config.js';
|
|
9
|
+
import { HANDOFF_TYPES, resolveHandoffPath } from './handoffs.js';
|
|
9
10
|
/**
|
|
10
11
|
* Register core tools with MCP server
|
|
11
12
|
*/
|
|
@@ -17,7 +18,7 @@ export function registerCoreTools(server) {
|
|
|
17
18
|
// ═══════════════════════════════════════════════════════════════
|
|
18
19
|
server.tool('kit_save_handoff', `Save a handoff artifact to .agent-kit/handoffs/. Returns the saved file path to use in next-step instructions. Do NOT append version numbers (v2, v3, etc.) to the slug.`, {
|
|
19
20
|
type: z
|
|
20
|
-
.enum(
|
|
21
|
+
.enum(HANDOFF_TYPES)
|
|
21
22
|
.describe('Handoff type'),
|
|
22
23
|
content: z.string().describe('Full markdown content to save'),
|
|
23
24
|
slug: z
|
|
@@ -25,28 +26,18 @@ export function registerCoreTools(server) {
|
|
|
25
26
|
.describe('Short identifier for the filename, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'),
|
|
26
27
|
}, async ({ type, content, slug }) => {
|
|
27
28
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
29
|
+
const location = resolveHandoffPath({
|
|
30
|
+
workspaceRoot: getWorkspaceRoot(),
|
|
31
|
+
type,
|
|
32
|
+
slug,
|
|
33
|
+
content,
|
|
34
|
+
});
|
|
35
|
+
const handoffDir = path.dirname(location.filePath);
|
|
30
36
|
if (!fs.existsSync(handoffDir)) {
|
|
31
37
|
fs.mkdirSync(handoffDir, { recursive: true });
|
|
32
38
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const existingFile = files
|
|
36
|
-
.filter((f) => slugPattern.test(f))
|
|
37
|
-
.sort()
|
|
38
|
-
.pop();
|
|
39
|
-
let filePath;
|
|
40
|
-
if (existingFile) {
|
|
41
|
-
filePath = path.join(handoffDir, existingFile);
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
45
|
-
const filename = `${type}-${timestamp}-${safeSlug}.md`;
|
|
46
|
-
filePath = path.join(handoffDir, filename);
|
|
47
|
-
}
|
|
48
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
49
|
-
return mcpText(`✅ Saved to: ${filePath}`);
|
|
39
|
+
fs.writeFileSync(location.filePath, content, 'utf8');
|
|
40
|
+
return mcpText(`✅ Saved to: ${location.filePath}`);
|
|
50
41
|
}
|
|
51
42
|
catch (error) {
|
|
52
43
|
return mcpText(`Error saving handoff: ${error}`);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const HANDOFF_TYPES: readonly ["brainstorm", "clarification", "plan", "ticket", "research", "scenario", "investigation"];
|
|
2
|
+
export type HandoffType = (typeof HANDOFF_TYPES)[number];
|
|
3
|
+
export type CanonicalHandoffType = 'brainstorm' | 'clarification' | 'plan' | 'ticket' | 'research' | 'scenario' | 'investigation';
|
|
4
|
+
export interface SavedHandoffLocation {
|
|
5
|
+
featureSlug: string;
|
|
6
|
+
canonicalType: CanonicalHandoffType;
|
|
7
|
+
filePath: string;
|
|
8
|
+
relativePath: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function sanitizeFeatureSlug(value: string): string;
|
|
11
|
+
export declare function normalizeHandoffType(type: HandoffType): CanonicalHandoffType;
|
|
12
|
+
export declare function deriveFeatureSlug(input: {
|
|
13
|
+
requestedSlug: string;
|
|
14
|
+
content: string;
|
|
15
|
+
type: CanonicalHandoffType;
|
|
16
|
+
}): string;
|
|
17
|
+
export declare function resolveHandoffPath(input: {
|
|
18
|
+
workspaceRoot: string;
|
|
19
|
+
type: HandoffType;
|
|
20
|
+
slug: string;
|
|
21
|
+
content: string;
|
|
22
|
+
}): SavedHandoffLocation;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
export const HANDOFF_TYPES = [
|
|
3
|
+
'brainstorm',
|
|
4
|
+
'clarification',
|
|
5
|
+
'plan',
|
|
6
|
+
'ticket',
|
|
7
|
+
'research',
|
|
8
|
+
'scenario',
|
|
9
|
+
'investigation',
|
|
10
|
+
];
|
|
11
|
+
const TICKET_ID_PATTERN = /\b[A-Za-z][A-Za-z0-9]+-\d+\b/;
|
|
12
|
+
function findTicketId(value) {
|
|
13
|
+
return value.match(TICKET_ID_PATTERN)?.[0].toLowerCase() ?? null;
|
|
14
|
+
}
|
|
15
|
+
export function sanitizeFeatureSlug(value) {
|
|
16
|
+
return value
|
|
17
|
+
.replace(/[^a-zA-Z0-9-_]/g, '-')
|
|
18
|
+
.replace(/-+/g, '-')
|
|
19
|
+
.replace(/^-|-$/g, '')
|
|
20
|
+
.toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
function contentSlugCandidate(content) {
|
|
23
|
+
const heading = content
|
|
24
|
+
.split(/\r?\n/)
|
|
25
|
+
.map((line) => line.replace(/^#+\s*/, '').trim())
|
|
26
|
+
.find((line) => line.length > 0);
|
|
27
|
+
return heading ?? content.replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
28
|
+
}
|
|
29
|
+
export function normalizeHandoffType(type) {
|
|
30
|
+
if (!HANDOFF_TYPES.includes(type)) {
|
|
31
|
+
throw new Error(`Unsupported handoff type: ${type}`);
|
|
32
|
+
}
|
|
33
|
+
return type;
|
|
34
|
+
}
|
|
35
|
+
export function deriveFeatureSlug(input) {
|
|
36
|
+
const ticketSlug = findTicketId(input.requestedSlug) ?? findTicketId(input.content);
|
|
37
|
+
if (ticketSlug)
|
|
38
|
+
return ticketSlug;
|
|
39
|
+
return (sanitizeFeatureSlug(input.requestedSlug) ||
|
|
40
|
+
sanitizeFeatureSlug(contentSlugCandidate(input.content)) ||
|
|
41
|
+
'untitled-handoff');
|
|
42
|
+
}
|
|
43
|
+
export function resolveHandoffPath(input) {
|
|
44
|
+
const canonicalType = normalizeHandoffType(input.type);
|
|
45
|
+
const featureSlug = deriveFeatureSlug({
|
|
46
|
+
requestedSlug: input.slug,
|
|
47
|
+
content: input.content,
|
|
48
|
+
type: canonicalType,
|
|
49
|
+
});
|
|
50
|
+
const relativePath = path.join('.agent-kit', 'handoffs', featureSlug, `${canonicalType}.md`);
|
|
51
|
+
return {
|
|
52
|
+
featureSlug,
|
|
53
|
+
canonicalType,
|
|
54
|
+
filePath: path.join(input.workspaceRoot, relativePath),
|
|
55
|
+
relativePath,
|
|
56
|
+
};
|
|
57
|
+
}
|