issy 0.7.2 → 0.8.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.
package/README.md CHANGED
@@ -159,9 +159,17 @@ issy list --sort priority # Sort by priority instead
159
159
  issy list --sort created # Sort by creation date
160
160
  ```
161
161
 
162
- ### On-Close Hook
162
+ ### Hooks
163
163
 
164
- Create a `.issy/on_close.md` file to inject context after every successful close. The file contents are printed to stdout, making them visible to AI agents in their command output. Use this for post-close reminders like updating documentation.
164
+ issy supports optional hook files in `.issy/` that inject context into stdout after successful operations. The file contents are printed directly, making them visible to AI agents in their command output.
165
+
166
+ | Hook file | Triggered after |
167
+ |-----------|----------------|
168
+ | `on_create.md` | Creating an issue |
169
+ | `on_update.md` | Updating an issue |
170
+ | `on_close.md` | Closing an issue |
171
+
172
+ Use these for post-action reminders like updating documentation, running checks, or prompting the agent with project-specific instructions.
165
173
 
166
174
  ### Monorepo Support
167
175
 
@@ -172,6 +180,8 @@ issy automatically walks up from the current directory to find an existing `.iss
172
180
  my-monorepo/
173
181
  .issy/ # ← issy finds this automatically
174
182
  issues/
183
+ on_create.md
184
+ on_update.md
175
185
  on_close.md
176
186
  packages/
177
187
  frontend/ # ← works from here
package/dist/cli.js CHANGED
@@ -316,15 +316,33 @@ function parseFrontmatter(content) {
316
316
  const colonIdx = line.indexOf(":");
317
317
  if (colonIdx > 0) {
318
318
  const key = line.slice(0, colonIdx).trim();
319
- const value = line.slice(colonIdx + 1).trim();
319
+ const rawValue = line.slice(colonIdx + 1).trim();
320
+ const value = key === "title" ? yamlUnquote(rawValue) : rawValue;
320
321
  frontmatter[key] = value;
321
322
  }
322
323
  }
323
324
  return { frontmatter, body };
324
325
  }
326
+ function yamlQuote(value) {
327
+ if (/[:#\[\]{}&*!|>'"%@`,\n]/.test(value) || value !== value.trim()) {
328
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
329
+ return `"${escaped}"`;
330
+ }
331
+ return value;
332
+ }
333
+ function yamlUnquote(value) {
334
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
335
+ const inner = value.slice(1, -1);
336
+ if (value.startsWith('"')) {
337
+ return inner.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
338
+ }
339
+ return inner;
340
+ }
341
+ return value;
342
+ }
325
343
  function generateFrontmatter(data) {
326
344
  const lines = ["---"];
327
- lines.push(`title: ${data.title}`);
345
+ lines.push(`title: ${yamlQuote(data.title)}`);
328
346
  lines.push(`priority: ${data.priority}`);
329
347
  if (data.scope) {
330
348
  lines.push(`scope: ${data.scope}`);
@@ -542,14 +560,22 @@ async function closeIssue(id) {
542
560
  async function reopenIssue(id, order) {
543
561
  return updateIssue(id, { status: "open", order });
544
562
  }
545
- async function getOnCloseContent() {
563
+ async function readHookFile(filename) {
546
564
  try {
547
- const onClosePath = join(getIssyDir(), "on_close.md");
548
- return await readFile(onClosePath, "utf-8");
565
+ return await readFile(join(getIssyDir(), filename), "utf-8");
549
566
  } catch {
550
567
  return null;
551
568
  }
552
569
  }
570
+ async function getOnCloseContent() {
571
+ return readHookFile("on_close.md");
572
+ }
573
+ async function getOnCreateContent() {
574
+ return readHookFile("on_create.md");
575
+ }
576
+ async function getOnUpdateContent() {
577
+ return readHookFile("on_update.md");
578
+ }
553
579
  async function getNextIssue() {
554
580
  const openIssues = await getOpenIssuesByOrder();
555
581
  return openIssues.length > 0 ? openIssues[0] : null;
@@ -2100,7 +2126,7 @@ function typeSymbol(type) {
2100
2126
  }
2101
2127
  function formatIssueRow(issue) {
2102
2128
  const status = issue.frontmatter.status === "open" ? "OPEN " : "CLOSED";
2103
- return ` ${issue.id} ${prioritySymbol(issue.frontmatter.priority)} ${typeSymbol(issue.frontmatter.type)} ${status} ${issue.frontmatter.title.slice(0, 45)}`;
2129
+ return ` ${issue.id} ${prioritySymbol(issue.frontmatter.priority)} ${typeSymbol(issue.frontmatter.type)} ${status} ${issue.frontmatter.title}`;
2104
2130
  }
2105
2131
  async function resolvePosition(opts) {
2106
2132
  const openIssues = await getOpenIssuesByOrder();
@@ -2152,7 +2178,7 @@ async function listIssues(options) {
2152
2178
  }
2153
2179
  console.log(`
2154
2180
  ID Pri Type Status Title`);
2155
- console.log(` ${"-".repeat(70)}`);
2181
+ console.log(` ${"-".repeat(100)}`);
2156
2182
  for (const issue of issues) {
2157
2183
  console.log(formatIssueRow(issue));
2158
2184
  }
@@ -2203,7 +2229,7 @@ async function searchIssuesCommand(query, options) {
2203
2229
  Search results for "${query}":`);
2204
2230
  console.log(`
2205
2231
  ID Pri Type Status Title`);
2206
- console.log(` ${"-".repeat(70)}`);
2232
+ console.log(` ${"-".repeat(100)}`);
2207
2233
  for (const issue of issues) {
2208
2234
  console.log(formatIssueRow(issue));
2209
2235
  }
@@ -2236,6 +2262,12 @@ async function createIssueCommand(options) {
2236
2262
  const issue = await createIssue(input);
2237
2263
  console.log(`
2238
2264
  Created issue: ${issue.filename}`);
2265
+ const onCreateContent = await getOnCreateContent();
2266
+ if (onCreateContent) {
2267
+ console.log(`
2268
+ ${onCreateContent.trim()}
2269
+ `);
2270
+ }
2239
2271
  } catch (e) {
2240
2272
  console.error(e instanceof Error ? e.message : "Failed to create issue");
2241
2273
  process.exit(1);
@@ -2264,6 +2296,12 @@ async function updateIssueCommand(id, options) {
2264
2296
  order
2265
2297
  });
2266
2298
  console.log(`Updated issue: ${issue.filename}`);
2299
+ const onUpdateContent = await getOnUpdateContent();
2300
+ if (onUpdateContent) {
2301
+ console.log(`
2302
+ ${onUpdateContent.trim()}
2303
+ `);
2304
+ }
2267
2305
  } catch (e) {
2268
2306
  console.error(e instanceof Error ? e.message : "Failed to update issue");
2269
2307
  process.exit(1);
package/dist/main.js CHANGED
@@ -327,15 +327,33 @@ function parseFrontmatter(content) {
327
327
  const colonIdx = line.indexOf(":");
328
328
  if (colonIdx > 0) {
329
329
  const key = line.slice(0, colonIdx).trim();
330
- const value = line.slice(colonIdx + 1).trim();
330
+ const rawValue = line.slice(colonIdx + 1).trim();
331
+ const value = key === "title" ? yamlUnquote(rawValue) : rawValue;
331
332
  frontmatter[key] = value;
332
333
  }
333
334
  }
334
335
  return { frontmatter, body };
335
336
  }
337
+ function yamlQuote(value) {
338
+ if (/[:#\[\]{}&*!|>'"%@`,\n]/.test(value) || value !== value.trim()) {
339
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
340
+ return `"${escaped}"`;
341
+ }
342
+ return value;
343
+ }
344
+ function yamlUnquote(value) {
345
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
346
+ const inner = value.slice(1, -1);
347
+ if (value.startsWith('"')) {
348
+ return inner.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
349
+ }
350
+ return inner;
351
+ }
352
+ return value;
353
+ }
336
354
  function generateFrontmatter(data) {
337
355
  const lines = ["---"];
338
- lines.push(`title: ${data.title}`);
356
+ lines.push(`title: ${yamlQuote(data.title)}`);
339
357
  lines.push(`priority: ${data.priority}`);
340
358
  if (data.scope) {
341
359
  lines.push(`scope: ${data.scope}`);
@@ -553,14 +571,22 @@ async function closeIssue(id) {
553
571
  async function reopenIssue(id, order) {
554
572
  return updateIssue(id, { status: "open", order });
555
573
  }
556
- async function getOnCloseContent() {
574
+ async function readHookFile(filename) {
557
575
  try {
558
- const onClosePath = join(getIssyDir(), "on_close.md");
559
- return await readFile(onClosePath, "utf-8");
576
+ return await readFile(join(getIssyDir(), filename), "utf-8");
560
577
  } catch {
561
578
  return null;
562
579
  }
563
580
  }
581
+ async function getOnCloseContent() {
582
+ return readHookFile("on_close.md");
583
+ }
584
+ async function getOnCreateContent() {
585
+ return readHookFile("on_create.md");
586
+ }
587
+ async function getOnUpdateContent() {
588
+ return readHookFile("on_update.md");
589
+ }
564
590
  async function getNextIssue() {
565
591
  const openIssues = await getOpenIssuesByOrder();
566
592
  return openIssues.length > 0 ? openIssues[0] : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "issy",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "AI-native issue tracking. Markdown files in .issues/, managed by your coding assistant.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,8 +35,8 @@
35
35
  "lint": "biome check src bin"
36
36
  },
37
37
  "dependencies": {
38
- "@miketromba/issy-app": "^0.7.2",
39
- "@miketromba/issy-core": "^0.7.2",
38
+ "@miketromba/issy-app": "^0.8.0",
39
+ "@miketromba/issy-core": "^0.8.0",
40
40
  "update-notifier": "^7.3.1"
41
41
  }
42
42
  }