issy 0.7.3 → 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;
@@ -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.3",
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.3",
39
- "@miketromba/issy-core": "^0.7.3",
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
  }