@pedrocivita/tocket 2.1.0 → 2.2.0

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.
@@ -119,8 +119,12 @@ export function registerInitCommand(program) {
119
119
  .command("init")
120
120
  .description("Scaffold an agentic workspace with Memory Bank and triangulation config")
121
121
  .option("-f, --force", "Overwrite existing files without prompting")
122
+ .option("--minimal", "Scaffold only essential files (.context/ + TOCKET.md)")
123
+ .option("--name <name>", "Project name (skip prompt)")
124
+ .option("--description <desc>", "Project description (skip prompt)")
122
125
  .action(async (options) => {
123
126
  const force = options.force ?? false;
127
+ const minimal = options.minimal ?? false;
124
128
  const cwd = process.cwd();
125
129
  const globalConfig = await getConfig();
126
130
  if (!globalConfig.theme?.disableBanner) {
@@ -142,11 +146,11 @@ export function registerInitCommand(program) {
142
146
  console.log(info(`Extras: ${stack.extras.join(", ")}`));
143
147
  console.log();
144
148
  }
145
- const projectName = await input({
149
+ const projectName = options.name ?? await input({
146
150
  message: "Project Name:",
147
151
  default: detectedName || undefined,
148
152
  });
149
- const description = await input({
153
+ const description = options.description ?? await input({
150
154
  message: "Short Description:",
151
155
  default: detectedDescription || undefined,
152
156
  });
@@ -169,7 +173,15 @@ export function registerInitCommand(program) {
169
173
  ],
170
174
  [join(".context", "progress.md"), progressMd(projectName)],
171
175
  ];
172
- for (const [filePath, content] of files) {
176
+ const minimalPaths = new Set([
177
+ "TOCKET.md",
178
+ join(".context", "activeContext.md"),
179
+ join(".context", "systemPatterns.md"),
180
+ ]);
181
+ const filesToWrite = minimal
182
+ ? files.filter(([path]) => minimalPaths.has(path))
183
+ : files;
184
+ for (const [filePath, content] of filesToWrite) {
173
185
  const fullPath = join(cwd, filePath);
174
186
  const exists = await fileExists(fullPath);
175
187
  if (exists && !force) {
@@ -0,0 +1,12 @@
1
+ import type { Command } from "commander";
2
+ export interface LintResult {
3
+ severity: "pass" | "warn" | "info" | "fail";
4
+ message: string;
5
+ file?: string;
6
+ suggestion?: string;
7
+ }
8
+ export declare function lintActiveContext(content: string): LintResult[];
9
+ export declare function lintSystemPatterns(content: string): LintResult[];
10
+ export declare function lintProtocolSpec(content: string): LintResult[];
11
+ export declare function lintAgentConfig(name: string, content: string): LintResult[];
12
+ export declare function registerLintCommand(program: Command): void;
@@ -0,0 +1,269 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import { success as themePass, warn as themeWarn, error as themeFail, heading, dim, info as themeInfo } from "../utils/theme.js";
5
+ import { isGitRepo } from "../utils/git.js";
6
+ export function lintActiveContext(content) {
7
+ const results = [];
8
+ // Check required sections exist
9
+ const requiredSections = ["## Current Focus", "## Recent Changes", "## Open Decisions"];
10
+ for (const section of requiredSections) {
11
+ if (content.includes(section)) {
12
+ results.push({ severity: "pass", message: `Has ${section} section` });
13
+ }
14
+ else {
15
+ results.push({
16
+ severity: "fail",
17
+ message: `Missing ${section} section`,
18
+ suggestion: `Add a "${section}" heading to activeContext.md`,
19
+ });
20
+ }
21
+ }
22
+ // Check Current Focus is populated
23
+ const focusMatch = content.match(/## Current Focus\s*\n+([\s\S]*?)(?=\n##|\n*$)/);
24
+ const focusBody = focusMatch?.[1]?.trim() ?? "";
25
+ if (!focusBody || focusBody.startsWith("_") || focusBody.includes("No active tasks")) {
26
+ results.push({
27
+ severity: "warn",
28
+ message: "Current Focus is empty or placeholder",
29
+ suggestion: "Run tocket focus \"your current task\" to set it",
30
+ });
31
+ }
32
+ else {
33
+ results.push({ severity: "pass", message: "Current Focus is populated" });
34
+ }
35
+ // Check Open Decisions section
36
+ const decisionsMatch = content.match(/## Open Decisions\s*\n+([\s\S]*?)(?=\n##|\n*$)/);
37
+ const decisionsBody = decisionsMatch?.[1]?.trim() ?? "";
38
+ if (!decisionsBody || decisionsBody.startsWith("_")) {
39
+ results.push({
40
+ severity: "info",
41
+ message: "Open Decisions section is empty",
42
+ suggestion: "List any unresolved decisions or write \"None\" if all resolved",
43
+ });
44
+ }
45
+ return results;
46
+ }
47
+ export function lintSystemPatterns(content) {
48
+ const results = [];
49
+ const hasConvention = content.includes("- ") || content.includes("| ");
50
+ if (hasConvention) {
51
+ results.push({ severity: "pass", message: "Has documented conventions" });
52
+ }
53
+ else {
54
+ results.push({
55
+ severity: "warn",
56
+ message: "No conventions documented",
57
+ suggestion: "Add bullet points or a table with your project's coding conventions",
58
+ });
59
+ }
60
+ // Check for key decisions table
61
+ if (content.includes("Key Decisions") || content.includes("key decisions")) {
62
+ const hasDecisionRows = content.match(/\|[^|]+\|[^|]+\|[^|]+\|/g);
63
+ const hasDataRows = hasDecisionRows && hasDecisionRows.length > 2; // header + separator + at least 1 row
64
+ if (hasDataRows) {
65
+ results.push({ severity: "pass", message: "Has key decisions recorded" });
66
+ }
67
+ else {
68
+ results.push({
69
+ severity: "info",
70
+ message: "Key Decisions table is empty",
71
+ suggestion: "Record architectural decisions with rationale and date",
72
+ });
73
+ }
74
+ }
75
+ return results;
76
+ }
77
+ export function lintProtocolSpec(content) {
78
+ const results = [];
79
+ if (content.includes("payload") || content.includes("Payload")) {
80
+ results.push({ severity: "pass", message: "References payload format" });
81
+ }
82
+ else {
83
+ results.push({
84
+ severity: "warn",
85
+ message: "Does not reference payload format",
86
+ suggestion: "TOCKET.md should describe the payload XML handoff format",
87
+ });
88
+ }
89
+ if (content.includes("Memory Bank") || content.includes(".context/")) {
90
+ results.push({ severity: "pass", message: "References Memory Bank" });
91
+ }
92
+ else {
93
+ results.push({
94
+ severity: "warn",
95
+ message: "Does not reference Memory Bank",
96
+ suggestion: "TOCKET.md should describe the .context/ directory",
97
+ });
98
+ }
99
+ return results;
100
+ }
101
+ export function lintAgentConfig(name, content) {
102
+ const results = [];
103
+ if (content.includes(".context/") || content.includes(".context\\")) {
104
+ results.push({ severity: "pass", message: `References .context/` });
105
+ }
106
+ else {
107
+ results.push({
108
+ severity: "warn",
109
+ message: `Does not reference .context/`,
110
+ suggestion: `Add instructions to read .context/ before acting`,
111
+ });
112
+ }
113
+ return results;
114
+ }
115
+ function severityIcon(severity) {
116
+ switch (severity) {
117
+ case "pass": return themePass("");
118
+ case "warn": return themeWarn("");
119
+ case "info": return themeInfo("");
120
+ case "fail": return themeFail("");
121
+ }
122
+ }
123
+ export function registerLintCommand(program) {
124
+ program
125
+ .command("lint")
126
+ .description("Audit .context/ content quality and suggest improvements")
127
+ .action(() => {
128
+ const cwd = process.cwd();
129
+ let passCount = 0;
130
+ let warnCount = 0;
131
+ let infoCount = 0;
132
+ let failCount = 0;
133
+ console.log(heading("\nTocket Lint\n"));
134
+ // Check TOCKET.md exists
135
+ const tocketPath = join(cwd, "TOCKET.md");
136
+ if (!existsSync(tocketPath)) {
137
+ console.log(themeFail(" TOCKET.md not found. Run tocket init first."));
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+ // Lint activeContext.md
142
+ const acPath = join(cwd, ".context", "activeContext.md");
143
+ if (existsSync(acPath)) {
144
+ console.log(dim(" .context/activeContext.md"));
145
+ const content = readFileSync(acPath, "utf-8");
146
+ const results = lintActiveContext(content);
147
+ for (const r of results) {
148
+ printResult(r);
149
+ countResult(r.severity);
150
+ }
151
+ // Staleness check
152
+ const stats = statSync(acPath);
153
+ const daysSince = Math.floor((Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24));
154
+ if (daysSince > 7) {
155
+ const r = {
156
+ severity: "warn",
157
+ message: `Last modified ${daysSince} days ago`,
158
+ suggestion: "Update activeContext.md with current state",
159
+ };
160
+ printResult(r);
161
+ countResult(r.severity);
162
+ }
163
+ console.log();
164
+ }
165
+ else {
166
+ console.log(dim(" .context/activeContext.md"));
167
+ console.log(` ${severityIcon("fail")} File missing (required)`);
168
+ failCount++;
169
+ console.log();
170
+ }
171
+ // Lint systemPatterns.md
172
+ const spPath = join(cwd, ".context", "systemPatterns.md");
173
+ if (existsSync(spPath)) {
174
+ console.log(dim(" .context/systemPatterns.md"));
175
+ const content = readFileSync(spPath, "utf-8");
176
+ const results = lintSystemPatterns(content);
177
+ for (const r of results) {
178
+ printResult(r);
179
+ countResult(r.severity);
180
+ }
181
+ console.log();
182
+ }
183
+ // Lint TOCKET.md
184
+ console.log(dim(" TOCKET.md"));
185
+ const tocketContent = readFileSync(tocketPath, "utf-8");
186
+ const tocketResults = lintProtocolSpec(tocketContent);
187
+ for (const r of tocketResults) {
188
+ printResult(r);
189
+ countResult(r.severity);
190
+ }
191
+ console.log();
192
+ // Lint agent configs
193
+ const agentFiles = ["CLAUDE.md", "GEMINI.md"];
194
+ for (const af of agentFiles) {
195
+ const afPath = join(cwd, af);
196
+ if (existsSync(afPath)) {
197
+ console.log(dim(` ${af}`));
198
+ const content = readFileSync(afPath, "utf-8");
199
+ const results = lintAgentConfig(af, content);
200
+ for (const r of results) {
201
+ printResult(r);
202
+ countResult(r.severity);
203
+ }
204
+ console.log();
205
+ }
206
+ }
207
+ // Uncommitted context check
208
+ if (isGitRepo(cwd)) {
209
+ try {
210
+ const output = execSync("git status --porcelain .context/", {
211
+ cwd,
212
+ encoding: "utf-8",
213
+ }).trim();
214
+ if (output) {
215
+ const count = output.split("\n").length;
216
+ console.log(dim(" git"));
217
+ const r = {
218
+ severity: "info",
219
+ message: `${count} uncommitted change(s) in .context/`,
220
+ suggestion: "Commit .context/ changes to preserve shared memory",
221
+ };
222
+ printResult(r);
223
+ countResult(r.severity);
224
+ console.log();
225
+ }
226
+ }
227
+ catch {
228
+ // git error — skip
229
+ }
230
+ }
231
+ // Summary
232
+ const total = passCount + warnCount + infoCount + failCount;
233
+ console.log(dim(` ${passCount} passed, ${warnCount} warnings, ${infoCount} info, ${failCount} failures (${total} checks)`));
234
+ console.log();
235
+ if (failCount > 0) {
236
+ console.log(themeFail("Context has issues that need attention."));
237
+ process.exitCode = 1;
238
+ }
239
+ else if (warnCount > 0) {
240
+ console.log(themeWarn("Context is functional but could be improved."));
241
+ }
242
+ else {
243
+ console.log(themePass("Context is in great shape."));
244
+ }
245
+ function printResult(r) {
246
+ const icon = severityIcon(r.severity);
247
+ console.log(` ${icon} ${r.message}`);
248
+ if (r.suggestion) {
249
+ console.log(` ${dim(r.suggestion)}`);
250
+ }
251
+ }
252
+ function countResult(severity) {
253
+ switch (severity) {
254
+ case "pass":
255
+ passCount++;
256
+ break;
257
+ case "warn":
258
+ warnCount++;
259
+ break;
260
+ case "info":
261
+ infoCount++;
262
+ break;
263
+ case "fail":
264
+ failCount++;
265
+ break;
266
+ }
267
+ }
268
+ });
269
+ }
@@ -9,7 +9,8 @@ export function registerSyncCommand(program) {
9
9
  program
10
10
  .command("sync")
11
11
  .description("Update Memory Bank from git history and session artifacts")
12
- .action(async () => {
12
+ .option("--summary <text>", "Session summary (skip prompt)")
13
+ .action(async (options) => {
13
14
  const progressPath = join(process.cwd(), ".context", "progress.md");
14
15
  const contextDir = join(process.cwd(), ".context");
15
16
  if (!existsSync(contextDir)) {
@@ -18,7 +19,7 @@ export function registerSyncCommand(program) {
18
19
  return;
19
20
  }
20
21
  const config = await getConfig();
21
- const summary = await input({
22
+ const summary = options.summary ?? await input({
22
23
  message: "What did you accomplish in this session?",
23
24
  });
24
25
  const commits = getRecentCommitsRaw();
@@ -1,2 +1,9 @@
1
1
  import type { Command } from "commander";
2
+ export interface CheckResult {
3
+ icon: string;
4
+ message: string;
5
+ }
6
+ export declare function checkFile(basePath: string, relativePath: string, required: boolean): CheckResult;
7
+ export declare function checkStale(basePath: string, relativePath: string): CheckResult | null;
8
+ export declare function checkAgentFile(basePath: string): CheckResult;
2
9
  export declare function registerValidateCommand(program: Command): void;
@@ -4,7 +4,7 @@ import { success as themePass, warn as themeWarn, error as themeFail, heading }
4
4
  const PASS = themePass("");
5
5
  const WARN = themeWarn("");
6
6
  const FAIL = themeFail("");
7
- function checkFile(basePath, relativePath, required) {
7
+ export function checkFile(basePath, relativePath, required) {
8
8
  const fullPath = join(basePath, relativePath);
9
9
  if (existsSync(fullPath)) {
10
10
  return { icon: PASS, message: `${relativePath} found` };
@@ -14,7 +14,7 @@ function checkFile(basePath, relativePath, required) {
14
14
  }
15
15
  return { icon: WARN, message: `${relativePath} missing (optional)` };
16
16
  }
17
- function checkStale(basePath, relativePath) {
17
+ export function checkStale(basePath, relativePath) {
18
18
  const fullPath = join(basePath, relativePath);
19
19
  if (!existsSync(fullPath))
20
20
  return null;
@@ -28,7 +28,7 @@ function checkStale(basePath, relativePath) {
28
28
  }
29
29
  return null;
30
30
  }
31
- function checkAgentFile(basePath) {
31
+ export function checkAgentFile(basePath) {
32
32
  const agentFiles = [
33
33
  "CLAUDE.md",
34
34
  "GEMINI.md",
package/dist/index.js CHANGED
@@ -10,6 +10,8 @@ import { registerConfigCommand } from "./commands/config.cmd.js";
10
10
  import { registerEjectCommand } from "./commands/eject.cmd.js";
11
11
  import { registerFocusCommand } from "./commands/focus.cmd.js";
12
12
  import { registerStatusCommand } from "./commands/status.cmd.js";
13
+ import { registerDoctorCommand } from "./commands/doctor.cmd.js";
14
+ import { registerLintCommand } from "./commands/lint.cmd.js";
13
15
  const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
14
16
  const program = new Command();
15
17
  program
@@ -24,6 +26,8 @@ registerConfigCommand(program);
24
26
  registerEjectCommand(program);
25
27
  registerFocusCommand(program);
26
28
  registerStatusCommand(program);
29
+ registerDoctorCommand(program);
30
+ registerLintCommand(program);
27
31
  // No-args: show interactive dashboard (TTY) or help (non-TTY)
28
32
  const args = process.argv.slice(2);
29
33
  if (args.length === 0 && process.stdin.isTTY) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,102 @@
1
+ import { describe, it, after } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { checkContentHealth, checkGitTracking, checkStaleness } from "../commands/doctor.cmd.js";
7
+ describe("checkContentHealth", () => {
8
+ const tempDir = mkdtempSync(join(tmpdir(), "tocket-doctor-content-"));
9
+ after(() => {
10
+ rmSync(tempDir, { recursive: true, force: true });
11
+ });
12
+ it("detects active focus in activeContext.md", () => {
13
+ mkdirSync(join(tempDir, ".context"), { recursive: true });
14
+ writeFileSync(join(tempDir, ".context", "activeContext.md"), "# Active Context\n\n## Current Focus\n\nWorking on feature X.\n\n## Recent Changes\n", "utf-8");
15
+ const results = checkContentHealth(tempDir);
16
+ const focusResult = results.find((r) => r.message.includes("active focus"));
17
+ assert.ok(focusResult);
18
+ assert.ok(focusResult.message.includes("active focus"));
19
+ });
20
+ it("warns when Current Focus is placeholder", () => {
21
+ const dir = join(tempDir, "placeholder");
22
+ mkdirSync(join(dir, ".context"), { recursive: true });
23
+ writeFileSync(join(dir, ".context", "activeContext.md"), "# Active Context\n\n## Current Focus\n\n_Describe what you're working on._\n", "utf-8");
24
+ const results = checkContentHealth(dir);
25
+ const focusResult = results.find((r) => r.message.includes("no meaningful"));
26
+ assert.ok(focusResult);
27
+ });
28
+ it("detects conventions in systemPatterns.md", () => {
29
+ writeFileSync(join(tempDir, ".context", "systemPatterns.md"), "# Patterns\n\n## Conventions\n\n- Code in English\n- ESM only\n", "utf-8");
30
+ const results = checkContentHealth(tempDir);
31
+ const patternResult = results.find((r) => r.message.includes("documented conventions"));
32
+ assert.ok(patternResult);
33
+ });
34
+ it("warns when systemPatterns.md has no conventions", () => {
35
+ const dir = join(tempDir, "empty-patterns");
36
+ mkdirSync(join(dir, ".context"), { recursive: true });
37
+ writeFileSync(join(dir, ".context", "systemPatterns.md"), "# Patterns\n\nNothing here yet.\n", "utf-8");
38
+ const results = checkContentHealth(dir);
39
+ const patternResult = results.find((r) => r.message.includes("no conventions"));
40
+ assert.ok(patternResult);
41
+ });
42
+ it("validates TOCKET.md contains protocol keywords", () => {
43
+ writeFileSync(join(tempDir, "TOCKET.md"), "# Tocket Protocol\n\nThe payload version is 2.0.\n", "utf-8");
44
+ const results = checkContentHealth(tempDir);
45
+ const tocketResult = results.find((r) => r.message.includes("protocol keywords"));
46
+ assert.ok(tocketResult);
47
+ });
48
+ it("checks agent config references .context/", () => {
49
+ writeFileSync(join(tempDir, "CLAUDE.md"), "# Claude\n\nRead `.context/` before acting.\n", "utf-8");
50
+ const results = checkContentHealth(tempDir);
51
+ const agentResult = results.find((r) => r.message.includes("CLAUDE.md references"));
52
+ assert.ok(agentResult);
53
+ });
54
+ it("warns when agent config does not reference .context/", () => {
55
+ const dir = join(tempDir, "no-ref");
56
+ mkdirSync(dir, { recursive: true });
57
+ writeFileSync(join(dir, "CLAUDE.md"), "# Claude\n\nJust some instructions.\n", "utf-8");
58
+ const results = checkContentHealth(dir);
59
+ const agentResult = results.find((r) => r.message.includes("does not reference"));
60
+ assert.ok(agentResult);
61
+ });
62
+ });
63
+ describe("checkGitTracking", () => {
64
+ // Use the Tocket repo itself for git tests
65
+ const tocketRoot = join(import.meta.dirname, "..", "..");
66
+ it("returns results in a git repository", () => {
67
+ const results = checkGitTracking(tocketRoot);
68
+ assert.ok(results.length > 0);
69
+ });
70
+ it("detects .context/ is not gitignored", () => {
71
+ const results = checkGitTracking(tocketRoot);
72
+ const ignoreResult = results.find((r) => r.message.includes("gitignore"));
73
+ assert.ok(ignoreResult);
74
+ assert.ok(ignoreResult.message.includes("not gitignored"));
75
+ });
76
+ it("returns warn for non-git directory", () => {
77
+ const tempDir = mkdtempSync(join(tmpdir(), "tocket-doctor-git-"));
78
+ try {
79
+ const results = checkGitTracking(tempDir);
80
+ assert.ok(results.some((r) => r.message.includes("Not a git repository")));
81
+ }
82
+ finally {
83
+ rmSync(tempDir, { recursive: true, force: true });
84
+ }
85
+ });
86
+ });
87
+ describe("checkStaleness", () => {
88
+ const tempDir = mkdtempSync(join(tmpdir(), "tocket-doctor-stale-"));
89
+ after(() => {
90
+ rmSync(tempDir, { recursive: true, force: true });
91
+ });
92
+ it("returns null when .context/activeContext.md does not exist", () => {
93
+ const result = checkStaleness(tempDir);
94
+ assert.equal(result, null);
95
+ });
96
+ it("returns null when file is fresh", () => {
97
+ mkdirSync(join(tempDir, ".context"), { recursive: true });
98
+ writeFileSync(join(tempDir, ".context", "activeContext.md"), "fresh", "utf-8");
99
+ const result = checkStaleness(tempDir);
100
+ assert.equal(result, null);
101
+ });
102
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { buildPayloadXml } from "../commands/generate.cmd.js";
4
+ describe("buildPayloadXml", () => {
5
+ const baseTask = {
6
+ intent: "Add input validation",
7
+ scope: "src/commands/sync.cmd.ts",
8
+ priority: "medium",
9
+ skills: "",
10
+ };
11
+ it("produces valid XML with version 2.0", () => {
12
+ const xml = buildPayloadXml([baseTask]);
13
+ assert.ok(xml.includes('<payload version="2.0">'));
14
+ assert.ok(xml.includes("</payload>"));
15
+ });
16
+ it("includes intent, scope, and priority in meta", () => {
17
+ const xml = buildPayloadXml([baseTask]);
18
+ assert.ok(xml.includes("<intent>Add input validation</intent>"));
19
+ assert.ok(xml.includes("<scope>src/commands/sync.cmd.ts</scope>"));
20
+ assert.ok(xml.includes("<priority>medium</priority>"));
21
+ });
22
+ it("omits skills tag when skills is empty", () => {
23
+ const xml = buildPayloadXml([baseTask]);
24
+ assert.ok(!xml.includes("<skills>"));
25
+ });
26
+ it("includes skills tag when skills is provided", () => {
27
+ const task = { ...baseTask, skills: "core,lsp" };
28
+ const xml = buildPayloadXml([task]);
29
+ assert.ok(xml.includes("<skills>core,lsp</skills>"));
30
+ });
31
+ it("generates correct task ids for multiple tasks", () => {
32
+ const tasks = [
33
+ { ...baseTask, intent: "Task one" },
34
+ { ...baseTask, intent: "Task two" },
35
+ { ...baseTask, intent: "Task three" },
36
+ ];
37
+ const xml = buildPayloadXml(tasks);
38
+ assert.ok(xml.includes('task id="1"'));
39
+ assert.ok(xml.includes('task id="2"'));
40
+ assert.ok(xml.includes('task id="3"'));
41
+ });
42
+ it("includes action text from each task intent", () => {
43
+ const tasks = [
44
+ { ...baseTask, intent: "First action" },
45
+ { ...baseTask, intent: "Second action" },
46
+ ];
47
+ const xml = buildPayloadXml(tasks);
48
+ assert.ok(xml.includes("<action>First action</action>"));
49
+ assert.ok(xml.includes("<action>Second action</action>"));
50
+ });
51
+ it("includes validate section", () => {
52
+ const xml = buildPayloadXml([baseTask]);
53
+ assert.ok(xml.includes("<validate>"));
54
+ assert.ok(xml.includes("<check>"));
55
+ });
56
+ it("uses first task meta for the payload-level meta", () => {
57
+ const tasks = [
58
+ { intent: "Main goal", scope: "src/", priority: "high", skills: "api" },
59
+ { intent: "Secondary", scope: "test/", priority: "low", skills: "" },
60
+ ];
61
+ const xml = buildPayloadXml(tasks);
62
+ // Meta should come from first task
63
+ assert.ok(xml.includes("<intent>Main goal</intent>"));
64
+ assert.ok(xml.includes("<scope>src/</scope>"));
65
+ assert.ok(xml.includes("<priority>high</priority>"));
66
+ });
67
+ });
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { mkdtempSync, rmSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { isGitRepo, getStagedFiles, getModifiedFiles, getRecentCommits, getRecentCommitsRaw, getCurrentBranch, } from "../utils/git.js";
6
+ import { isGitRepo, getStagedFiles, getModifiedFiles, getRecentCommits, getRecentCommitsRaw, getCurrentBranch, getLastCommitMessage, } from "../utils/git.js";
7
7
  // Use the Tocket repo itself for "in git" tests
8
8
  const tocketRoot = join(import.meta.dirname, "..", "..");
9
9
  describe("git - in a git repository", () => {
@@ -33,6 +33,11 @@ describe("git - in a git repository", () => {
33
33
  const files = getModifiedFiles(tocketRoot);
34
34
  assert.ok(Array.isArray(files));
35
35
  });
36
+ it("getLastCommitMessage returns a non-empty string", () => {
37
+ const msg = getLastCommitMessage(tocketRoot);
38
+ assert.ok(typeof msg === "string");
39
+ assert.ok(msg.length > 0);
40
+ });
36
41
  });
37
42
  describe("git - in a non-git directory", () => {
38
43
  const tempDir = mkdtempSync(join(tmpdir(), "tocket-git-test-"));
@@ -58,4 +63,7 @@ describe("git - in a non-git directory", () => {
58
63
  it("getCurrentBranch returns empty string", () => {
59
64
  assert.equal(getCurrentBranch(tempDir), "");
60
65
  });
66
+ it("getLastCommitMessage returns empty string", () => {
67
+ assert.equal(getLastCommitMessage(tempDir), "");
68
+ });
61
69
  });
@@ -0,0 +1 @@
1
+ export {};