@hanhnd/agent-kit 1.0.25 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/kit-server.js +92 -45
- package/dist/tools/agent.js +2 -2
- package/dist/tools/core.js +11 -20
- package/dist/tools/handoffs.d.ts +22 -0
- package/dist/tools/handoffs.js +62 -0
- package/package.json +1 -1
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,66 @@ 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-Z][A-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 requestedTicketSlug = findTicketId(input.requestedSlug);
|
|
21078
|
+
if (requestedTicketSlug) return requestedTicketSlug;
|
|
21079
|
+
const requestedSlug = sanitizeFeatureSlug(input.requestedSlug);
|
|
21080
|
+
if (requestedSlug) return requestedSlug;
|
|
21081
|
+
const contentTicketSlug = findTicketId(input.content);
|
|
21082
|
+
if (contentTicketSlug) return contentTicketSlug;
|
|
21083
|
+
return sanitizeFeatureSlug(contentSlugCandidate(input.content)) || "untitled-handoff";
|
|
21084
|
+
}
|
|
21085
|
+
function resolveHandoffPath(input) {
|
|
21086
|
+
const canonicalType = normalizeHandoffType(input.type);
|
|
21087
|
+
const featureSlug = deriveFeatureSlug({
|
|
21088
|
+
requestedSlug: input.slug,
|
|
21089
|
+
content: input.content,
|
|
21090
|
+
type: canonicalType
|
|
21091
|
+
});
|
|
21092
|
+
const relativePath = path.join(".agent-kit", "handoffs", featureSlug, `${canonicalType}.md`);
|
|
21093
|
+
return {
|
|
21094
|
+
featureSlug,
|
|
21095
|
+
canonicalType,
|
|
21096
|
+
filePath: path.join(input.workspaceRoot, relativePath),
|
|
21097
|
+
relativePath
|
|
21098
|
+
};
|
|
21099
|
+
}
|
|
21100
|
+
|
|
21048
21101
|
// src/tools/core.ts
|
|
21049
21102
|
function registerCoreTools(server2) {
|
|
21050
21103
|
server2.tool(
|
|
21051
21104
|
"kit_save_handoff",
|
|
21052
21105
|
`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
21106
|
{
|
|
21054
|
-
type: external_exports.enum(
|
|
21107
|
+
type: external_exports.enum(HANDOFF_TYPES).describe("Handoff type"),
|
|
21055
21108
|
content: external_exports.string().describe("Full markdown content to save"),
|
|
21056
21109
|
slug: external_exports.string().describe(
|
|
21057
21110
|
'Short identifier for the filename, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'
|
|
@@ -21059,24 +21112,18 @@ function registerCoreTools(server2) {
|
|
|
21059
21112
|
},
|
|
21060
21113
|
async ({ type, content, slug }) => {
|
|
21061
21114
|
try {
|
|
21062
|
-
const
|
|
21063
|
-
|
|
21115
|
+
const location = resolveHandoffPath({
|
|
21116
|
+
workspaceRoot: getWorkspaceRoot(),
|
|
21117
|
+
type,
|
|
21118
|
+
slug,
|
|
21119
|
+
content
|
|
21120
|
+
});
|
|
21121
|
+
const handoffDir = path2.dirname(location.filePath);
|
|
21064
21122
|
if (!fs.existsSync(handoffDir)) {
|
|
21065
21123
|
fs.mkdirSync(handoffDir, { recursive: true });
|
|
21066
21124
|
}
|
|
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}`);
|
|
21125
|
+
fs.writeFileSync(location.filePath, content, "utf8");
|
|
21126
|
+
return mcpText(`\u2705 Saved to: ${location.filePath}`);
|
|
21080
21127
|
} catch (error2) {
|
|
21081
21128
|
return mcpText(`Error saving handoff: ${error2}`);
|
|
21082
21129
|
}
|
|
@@ -21088,27 +21135,27 @@ function registerCoreTools(server2) {
|
|
|
21088
21135
|
import { spawn } from "child_process";
|
|
21089
21136
|
import { randomUUID } from "crypto";
|
|
21090
21137
|
import * as fs2 from "fs";
|
|
21091
|
-
import * as
|
|
21138
|
+
import * as path4 from "path";
|
|
21092
21139
|
|
|
21093
21140
|
// src/tools/security.ts
|
|
21094
21141
|
import { execFileSync } from "child_process";
|
|
21095
|
-
import * as
|
|
21142
|
+
import * as path3 from "path";
|
|
21096
21143
|
function sanitize(input) {
|
|
21097
21144
|
return String(input).replace(/[;&|`$<>\\]/g, "").trim().slice(0, 500);
|
|
21098
21145
|
}
|
|
21099
21146
|
function validatePath(filePath, baseDir = process.cwd()) {
|
|
21100
|
-
const resolved =
|
|
21101
|
-
const root =
|
|
21102
|
-
if (resolved !== root && !resolved.startsWith(root +
|
|
21147
|
+
const resolved = path3.resolve(baseDir, filePath);
|
|
21148
|
+
const root = path3.resolve(baseDir);
|
|
21149
|
+
if (resolved !== root && !resolved.startsWith(root + path3.sep)) {
|
|
21103
21150
|
throw new Error(`Path traversal detected: ${filePath}`);
|
|
21104
21151
|
}
|
|
21105
|
-
const segments = resolved.split(
|
|
21152
|
+
const segments = resolved.split(path3.sep);
|
|
21106
21153
|
for (const segment of segments) {
|
|
21107
21154
|
if (FORBIDDEN_DIRS.some((d) => segment.toLowerCase() === d.toLowerCase())) {
|
|
21108
21155
|
throw new Error(`Access to sensitive directory is FORBIDDEN: ${segment}`);
|
|
21109
21156
|
}
|
|
21110
21157
|
}
|
|
21111
|
-
const fileName =
|
|
21158
|
+
const fileName = path3.basename(resolved);
|
|
21112
21159
|
const isExactForbidden = FORBIDDEN_FILES.some((f) => fileName.toLowerCase() === f.toLowerCase());
|
|
21113
21160
|
const isPatternForbidden = FORBIDDEN_PATTERNS.some((re) => re.test(fileName));
|
|
21114
21161
|
if (isExactForbidden || isPatternForbidden) {
|
|
@@ -21182,10 +21229,10 @@ function taskSlug(task) {
|
|
|
21182
21229
|
return task.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 6).join("-").slice(0, 48);
|
|
21183
21230
|
}
|
|
21184
21231
|
function initAgentLog(workspaceRoot, slug, agent) {
|
|
21185
|
-
const logsDir =
|
|
21232
|
+
const logsDir = path4.join(workspaceRoot, ".agent-kit", "logs", "agents");
|
|
21186
21233
|
fs2.mkdirSync(logsDir, { recursive: true });
|
|
21187
21234
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21188
|
-
const logFile =
|
|
21235
|
+
const logFile = path4.join(logsDir, `delegate-${now}-${slug}.log`);
|
|
21189
21236
|
const header = `Agent: ${agent}
|
|
21190
21237
|
Date: ${now}
|
|
21191
21238
|
${"\u2500".repeat(60)}
|
|
@@ -21195,16 +21242,16 @@ ${"\u2500".repeat(60)}
|
|
|
21195
21242
|
const logStream = fs2.createWriteStream(logFile, { flags: "a" });
|
|
21196
21243
|
logStream.on("error", () => {
|
|
21197
21244
|
});
|
|
21198
|
-
return { logPath:
|
|
21245
|
+
return { logPath: path4.relative(workspaceRoot, logFile), logStream };
|
|
21199
21246
|
}
|
|
21200
21247
|
function registerAgentTools(server2) {
|
|
21201
21248
|
server2.tool(
|
|
21202
21249
|
"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/
|
|
21250
|
+
"!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
21251
|
{
|
|
21205
21252
|
agent: external_exports.enum(["gemini", "claude", "codex"]).describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
|
|
21206
21253
|
task: external_exports.string().describe(
|
|
21207
|
-
'Task message or path to a handoff file (e.g., ".agent-kit/handoffs/
|
|
21254
|
+
'Task message or path to a handoff file (e.g., ".agent-kit/handoffs/feature-slug/plan.md")'
|
|
21208
21255
|
)
|
|
21209
21256
|
},
|
|
21210
21257
|
async ({ agent, task }, extra) => {
|
|
@@ -21214,7 +21261,7 @@ function registerAgentTools(server2) {
|
|
|
21214
21261
|
let killChild;
|
|
21215
21262
|
try {
|
|
21216
21263
|
let prompt = task;
|
|
21217
|
-
const resolvedTask =
|
|
21264
|
+
const resolvedTask = path4.resolve(workspaceRoot, task);
|
|
21218
21265
|
if (fs2.existsSync(resolvedTask) && fs2.statSync(resolvedTask).isFile()) {
|
|
21219
21266
|
try {
|
|
21220
21267
|
validatePath(task, workspaceRoot);
|
|
@@ -21321,8 +21368,8 @@ import { writeFileSync as writeFileSync3 } from "fs";
|
|
|
21321
21368
|
// src/utils/credentials.ts
|
|
21322
21369
|
import * as fs3 from "fs";
|
|
21323
21370
|
import * as os from "os";
|
|
21324
|
-
import * as
|
|
21325
|
-
var CREDENTIALS_FILE =
|
|
21371
|
+
import * as path5 from "path";
|
|
21372
|
+
var CREDENTIALS_FILE = path5.join(os.homedir(), ".claude", "credentials");
|
|
21326
21373
|
var cachedCredentials = null;
|
|
21327
21374
|
function parseIni(content) {
|
|
21328
21375
|
const result = {};
|
package/dist/tools/agent.js
CHANGED
|
@@ -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,62 @@
|
|
|
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-Z][A-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 requestedTicketSlug = findTicketId(input.requestedSlug);
|
|
37
|
+
if (requestedTicketSlug)
|
|
38
|
+
return requestedTicketSlug;
|
|
39
|
+
const requestedSlug = sanitizeFeatureSlug(input.requestedSlug);
|
|
40
|
+
if (requestedSlug)
|
|
41
|
+
return requestedSlug;
|
|
42
|
+
const contentTicketSlug = findTicketId(input.content);
|
|
43
|
+
if (contentTicketSlug)
|
|
44
|
+
return contentTicketSlug;
|
|
45
|
+
return (sanitizeFeatureSlug(contentSlugCandidate(input.content)) ||
|
|
46
|
+
'untitled-handoff');
|
|
47
|
+
}
|
|
48
|
+
export function resolveHandoffPath(input) {
|
|
49
|
+
const canonicalType = normalizeHandoffType(input.type);
|
|
50
|
+
const featureSlug = deriveFeatureSlug({
|
|
51
|
+
requestedSlug: input.slug,
|
|
52
|
+
content: input.content,
|
|
53
|
+
type: canonicalType,
|
|
54
|
+
});
|
|
55
|
+
const relativePath = path.join('.agent-kit', 'handoffs', featureSlug, `${canonicalType}.md`);
|
|
56
|
+
return {
|
|
57
|
+
featureSlug,
|
|
58
|
+
canonicalType,
|
|
59
|
+
filePath: path.join(input.workspaceRoot, relativePath),
|
|
60
|
+
relativePath,
|
|
61
|
+
};
|
|
62
|
+
}
|