@difflab/difflab-cli 0.3.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +1339 -57
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -33,6 +33,19 @@ var __toESM = (mod, isNodeMode, target) => {
33
33
  return to;
34
34
  };
35
35
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
36
+ var __returnValue = (v) => v;
37
+ function __exportSetter(name, newValue) {
38
+ this[name] = __returnValue.bind(null, newValue);
39
+ }
40
+ var __export = (target, all) => {
41
+ for (var name in all)
42
+ __defProp(target, name, {
43
+ get: all[name],
44
+ enumerable: true,
45
+ configurable: true,
46
+ set: __exportSetter.bind(all, name)
47
+ });
48
+ };
36
49
  var __esm = (fn, res, err) => () => {
37
50
  if (fn)
38
51
  try {
@@ -12783,17 +12796,17 @@ var init_registry = __esm(() => {
12783
12796
  // ../../libs/difflab-ts/src/gitx.ts
12784
12797
  import { execFileSync } from "child_process";
12785
12798
  import { appendFile, readFile } from "fs/promises";
12786
- import { isAbsolute, join, resolve as resolve2 } from "path";
12799
+ import { isAbsolute, resolve as resolve2 } from "path";
12787
12800
  function gitRoot(cwd) {
12788
12801
  return runGit(cwd, ["rev-parse", "--show-toplevel"]);
12789
12802
  }
12790
12803
  function discoverGitRepository(cwd) {
12791
12804
  const root = gitRoot(cwd);
12792
- const gitDir = runGit(root, ["rev-parse", "--git-dir"]);
12805
+ const exclude = runGit(root, ["rev-parse", "--git-path", "info/exclude"]);
12793
12806
  return {
12794
12807
  root,
12795
12808
  originUrl: runGit(root, ["remote", "get-url", "origin"]),
12796
- excludePath: join(isAbsolute(gitDir) ? gitDir : resolve2(root, gitDir), "info", "exclude")
12809
+ excludePath: isAbsolute(exclude) ? exclude : resolve2(root, exclude)
12797
12810
  };
12798
12811
  }
12799
12812
  async function addPathToGitExcludes(path, entry) {
@@ -26472,9 +26485,9 @@ var init_dist3 = __esm(() => {
26472
26485
 
26473
26486
  // src/projects/config.ts
26474
26487
  import { lstat as lstat2, readFile as readFile2, writeFile } from "fs/promises";
26475
- import { join as join2 } from "path";
26488
+ import { join } from "path";
26476
26489
  function repoConfigPath(root = process.cwd()) {
26477
- return join2(root, "difflab.yaml");
26490
+ return join(root, "difflab.yaml");
26478
26491
  }
26479
26492
  async function readRepoConfig(root = process.cwd()) {
26480
26493
  const path = repoConfigPath(root);
@@ -26535,7 +26548,7 @@ var init_config = __esm(() => {
26535
26548
  import { realpathSync } from "fs";
26536
26549
  import { lstat as lstat3, mkdir } from "fs/promises";
26537
26550
  import { homedir } from "os";
26538
- import { join as join3, resolve as resolve3 } from "path";
26551
+ import { join as join2, resolve as resolve3 } from "path";
26539
26552
  function userPaths(home = process.env.HOME || homedir()) {
26540
26553
  let tilde;
26541
26554
  try {
@@ -26545,8 +26558,8 @@ function userPaths(home = process.env.HOME || homedir()) {
26545
26558
  throw error;
26546
26559
  tilde = resolve3(home);
26547
26560
  }
26548
- const root = join3(tilde, ".difflab");
26549
- return { root, projects: join3(root, "projects"), database: join3(root, "difflab.sqlite") };
26561
+ const root = join2(tilde, ".difflab");
26562
+ return { root, projects: join2(root, "projects"), database: join2(root, "difflab.sqlite") };
26550
26563
  }
26551
26564
  function createUserStore(home) {
26552
26565
  return {
@@ -26630,25 +26643,65 @@ function hasErrorCode(error, code) {
26630
26643
  }
26631
26644
  var init_osx = () => {};
26632
26645
 
26646
+ // src/extensions/pathx.ts
26647
+ import { lstat as lstat4, realpath, stat } from "fs/promises";
26648
+ import { isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve4, sep } from "path";
26649
+ async function resolvePath(cwd, path) {
26650
+ if (!isAbsolute2(cwd))
26651
+ throw new Error("cwd must be an absolute path to an existing directory");
26652
+ if (!path.trim() || isAbsolute2(path))
26653
+ throw new Error("path must be a non-empty relative file path");
26654
+ const root = await realpath(cwd);
26655
+ if (!(await stat(root)).isDirectory())
26656
+ throw new Error("cwd must be an existing directory");
26657
+ const file = resolve4(root, path);
26658
+ const inside = relative(root, file);
26659
+ if (!inside || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute2(inside)) {
26660
+ throw new Error("path must name a file inside cwd; parent traversal outside cwd is not allowed");
26661
+ }
26662
+ let current = root;
26663
+ for (const segment of inside.split(sep)) {
26664
+ current = join3(current, segment);
26665
+ try {
26666
+ if ((await lstat4(current)).isSymbolicLink()) {
26667
+ throw new Error(`Symlink paths are not allowed for files: ${current}`);
26668
+ }
26669
+ } catch (error) {
26670
+ if (hasErrorCode(error, "ENOENT"))
26671
+ break;
26672
+ throw error;
26673
+ }
26674
+ }
26675
+ return file;
26676
+ }
26677
+ var init_pathx = __esm(() => {
26678
+ init_osx();
26679
+ });
26680
+
26633
26681
  // src/extensions/fsx.ts
26634
26682
  import { randomUUID as randomUUID3 } from "crypto";
26635
- import { lstat as lstat4, mkdir as mkdir2, readFile as readFile3, rename, rm, writeFile as writeFile2 } from "fs/promises";
26683
+ import { lstat as lstat5, mkdir as mkdir2, readFile as readFile3, rename, rm, writeFile as writeFile2 } from "fs/promises";
26636
26684
  import { dirname } from "path";
26637
26685
  async function lstatOrNull(file) {
26638
26686
  try {
26639
- return await lstat4(file);
26687
+ return await lstat5(file);
26640
26688
  } catch (error) {
26641
26689
  if (hasErrorCode(error, "ENOENT"))
26642
26690
  return null;
26643
26691
  throw error;
26644
26692
  }
26645
26693
  }
26694
+ async function ensurePathDirsExist(path, cwd = process.cwd()) {
26695
+ const file = await resolvePath(cwd, path);
26696
+ await mkdir2(dirname(file), { recursive: true });
26697
+ return resolvePath(cwd, path);
26698
+ }
26646
26699
  function isObject3(value) {
26647
26700
  return value !== null && typeof value === "object" && !Array.isArray(value);
26648
26701
  }
26649
26702
  async function readJsonFile(file) {
26650
26703
  try {
26651
- if ((await lstat4(file)).isSymbolicLink())
26704
+ if ((await lstat5(file)).isSymbolicLink())
26652
26705
  throw new Error(`JSON file at ${file} is a symlink`);
26653
26706
  const parsed = JSON.parse(await readFile3(file, "utf8"));
26654
26707
  if (!isObject3(parsed))
@@ -26674,11 +26727,12 @@ async function patchJsonToFile(file, patch) {
26674
26727
  }
26675
26728
  var init_fsx = __esm(() => {
26676
26729
  init_osx();
26730
+ init_pathx();
26677
26731
  });
26678
26732
 
26679
26733
  // src/projects/repo-store.ts
26680
26734
  import { mkdir as mkdir3, readlink, symlink } from "fs/promises";
26681
- import { dirname as dirname2, join as join4, resolve as resolve4 } from "path";
26735
+ import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
26682
26736
  async function preflightRepoStore(root, target) {
26683
26737
  for (const path of [root, dirname2(target)]) {
26684
26738
  const entry = await lstatOrNull(path);
@@ -26693,7 +26747,7 @@ async function preflightRepoStore(root, target) {
26693
26747
  if (entry) {
26694
26748
  if (!entry.isSymbolicLink())
26695
26749
  throw new RepoStoreError(`Refusing to replace existing .difflab path: ${link}`);
26696
- if (resolve4(root, await readlink(link)) !== resolve4(target))
26750
+ if (resolve5(root, await readlink(link)) !== resolve5(target))
26697
26751
  throw new RepoStoreError(`.difflab already links elsewhere: ${link}`);
26698
26752
  }
26699
26753
  }
@@ -26704,7 +26758,7 @@ async function checkRepoStore(root, target, excludePath) {
26704
26758
  throw new RepoStoreError(`Missing .difflab symlink at ${link}`);
26705
26759
  if (!entry.isSymbolicLink())
26706
26760
  throw new RepoStoreError(`Refusing to replace existing .difflab path: ${link}`);
26707
- if (resolve4(root, await readlink(link)) !== resolve4(target))
26761
+ if (resolve5(root, await readlink(link)) !== resolve5(target))
26708
26762
  throw new RepoStoreError(`.difflab already links elsewhere: ${link}`);
26709
26763
  const targetStat = await lstatOrNull(target);
26710
26764
  if (!targetStat?.isDirectory())
@@ -47818,7 +47872,8 @@ function defineTool(name, config, handler, options = {}) {
47818
47872
  },
47819
47873
  callback: async (untrustedInput) => {
47820
47874
  try {
47821
- return serializeToolResult(await handler(config.inputSchema.parse(untrustedInput)), config.outputSchema);
47875
+ const value = await handler(config.inputSchema.parse(untrustedInput));
47876
+ return serializeToolResult(value, config.outputSchema, options.toStructuredContent?.(value) ?? value);
47822
47877
  } catch (error) {
47823
47878
  return {
47824
47879
  ...serializeToolResult({
@@ -47830,49 +47885,14 @@ function defineTool(name, config, handler, options = {}) {
47830
47885
  }
47831
47886
  };
47832
47887
  }
47833
- function serializeToolResult(value, outputSchema) {
47834
- const structuredContent = outputSchema?.parse(value);
47888
+ function serializeToolResult(value, outputSchema, content = value) {
47889
+ const structuredContent = outputSchema?.parse(content);
47835
47890
  return {
47836
47891
  content: [{ type: "text", text: JSON.stringify(value) }],
47837
47892
  ...structuredContent ? { structuredContent } : {}
47838
47893
  };
47839
47894
  }
47840
47895
 
47841
- // src/extensions/pathx.ts
47842
- import { lstat as lstat5, realpath, stat } from "fs/promises";
47843
- import { isAbsolute as isAbsolute2, join as join6, relative, resolve as resolve5, sep } from "path";
47844
- async function resolvePath(cwd, path) {
47845
- if (!isAbsolute2(cwd))
47846
- throw new Error("cwd must be an absolute path to an existing directory");
47847
- if (!path.trim() || isAbsolute2(path))
47848
- throw new Error("path must be a non-empty relative file path");
47849
- const root = await realpath(cwd);
47850
- if (!(await stat(root)).isDirectory())
47851
- throw new Error("cwd must be an existing directory");
47852
- const file = resolve5(root, path);
47853
- const inside = relative(root, file);
47854
- if (!inside || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute2(inside)) {
47855
- throw new Error("path must name a file inside cwd; parent traversal outside cwd is not allowed");
47856
- }
47857
- let current = root;
47858
- for (const segment of inside.split(sep)) {
47859
- current = join6(current, segment);
47860
- try {
47861
- if ((await lstat5(current)).isSymbolicLink()) {
47862
- throw new Error(`Symlink paths are not allowed for files: ${current}`);
47863
- }
47864
- } catch (error) {
47865
- if (hasErrorCode(error, "ENOENT"))
47866
- break;
47867
- throw error;
47868
- }
47869
- }
47870
- return file;
47871
- }
47872
- var init_pathx = __esm(() => {
47873
- init_osx();
47874
- });
47875
-
47876
47896
  // src/stores/json-file-store.ts
47877
47897
  import { randomUUID as randomUUID4 } from "crypto";
47878
47898
  import { mkdir as mkdir4, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile3 } from "fs/promises";
@@ -48161,14 +48181,100 @@ var init_tools = __esm(() => {
48161
48181
  ];
48162
48182
  });
48163
48183
 
48164
- // src/projects/tools.ts
48184
+ // src/extensions/zodx.ts
48165
48185
  import { isAbsolute as isAbsolute4 } from "path";
48186
+ var absolutePath, relativePath, fileName;
48187
+ var init_zodx = __esm(() => {
48188
+ init_zod();
48189
+ absolutePath = string2().refine(isAbsolute4, "path must be absolute");
48190
+ relativePath = string2().min(1).refine((value) => value.trim().length > 0 && !isAbsolute4(value), "path must be relative");
48191
+ fileName = string2().refine((value) => value.trim().length > 0 && value !== "." && value !== ".." && !/[/\\\0]/.test(value), "filename must be a single non-empty file name");
48192
+ });
48193
+
48194
+ // src/logs/store.ts
48195
+ import { constants } from "fs";
48196
+ import { lstat as lstat6, open as open2, realpath as realpath2 } from "fs/promises";
48197
+ import { basename, dirname as dirname5, isAbsolute as isAbsolute5, resolve as resolve8 } from "path";
48198
+ async function appendProgressLog(path, message, now = new Date) {
48199
+ if (!isAbsolute5(path) || resolve8(path) !== path || basename(path) !== "logs.txt") {
48200
+ throw new ProgressLogError("path must be an absolute, normalized path ending in logs.txt");
48201
+ }
48202
+ if (!message.trim() || /[\r\n\v\f\0\u0085\u2028\u2029]/.test(message) || Buffer.byteLength(message, "utf8") > 2048) {
48203
+ throw new ProgressLogError("message must be one non-empty line of at most 2048 bytes");
48204
+ }
48205
+ const parent = dirname5(path);
48206
+ if (await realpath2(parent) !== parent) {
48207
+ throw new ProgressLogError("path must not contain symlinked directories");
48208
+ }
48209
+ const pad = (value) => String(value).padStart(2, "0");
48210
+ const stamp = `${pad(now.getFullYear() % 100)}${pad(now.getMonth() + 1)}${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
48211
+ const entry = `[${stamp}]: ${message}`;
48212
+ try {
48213
+ const existing = await lstat6(path);
48214
+ if (existing.isSymbolicLink())
48215
+ throw new ProgressLogError("logs.txt must not be a symlink");
48216
+ if (!existing.isFile())
48217
+ throw new ProgressLogError("logs.txt must be a regular file");
48218
+ } catch (error) {
48219
+ if (!hasErrorCode(error, "ENOENT"))
48220
+ throw error;
48221
+ }
48222
+ let handle;
48223
+ try {
48224
+ handle = await open2(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 384);
48225
+ } catch (error) {
48226
+ if (hasErrorCode(error, "ELOOP"))
48227
+ throw new ProgressLogError("logs.txt must not be a symlink");
48228
+ throw error;
48229
+ }
48230
+ try {
48231
+ if (!(await handle.stat()).isFile())
48232
+ throw new ProgressLogError("logs.txt must be a regular file");
48233
+ const line = Buffer.from(`${entry}
48234
+ `, "utf8");
48235
+ const { bytesWritten } = await handle.write(line);
48236
+ if (bytesWritten !== line.length)
48237
+ throw new ProgressLogError("Could not append the full log line");
48238
+ } finally {
48239
+ await handle.close();
48240
+ }
48241
+ return { path, entry };
48242
+ }
48243
+ var ProgressLogError;
48244
+ var init_store2 = __esm(() => {
48245
+ init_osx();
48246
+ ProgressLogError = class ProgressLogError extends Error {
48247
+ };
48248
+ });
48249
+
48250
+ // src/logs/tools.ts
48251
+ var logOutput, logTools;
48252
+ var init_tools2 = __esm(() => {
48253
+ init_zod();
48254
+ init_zodx();
48255
+ init_store2();
48256
+ logOutput = object({ result: object({ path: string2(), entry: string2() }) });
48257
+ logTools = [
48258
+ defineTool("log_append", {
48259
+ description: "Append one locally timestamped progress line to logs.txt at an absolute physical path. The parent directory must already exist; create the file if absent, never overwrite prior entries.",
48260
+ inputSchema: strictObject({
48261
+ path: absolutePath.describe("Absolute physical path to an existing plan directory/logs.txt"),
48262
+ message: string2().min(1).describe("Single-line progress message")
48263
+ }),
48264
+ outputSchema: logOutput,
48265
+ annotations: { openWorldHint: false }
48266
+ }, async ({ path, message }) => appendProgressLog(path, message), { toStructuredContent: (result) => ({ result }) })
48267
+ ];
48268
+ });
48269
+
48270
+ // src/projects/tools.ts
48271
+ import { isAbsolute as isAbsolute6 } from "path";
48166
48272
  function createProjectTools(home) {
48167
48273
  return [
48168
48274
  defineTool("project_context", {
48169
48275
  description: "Read the configured Difflab project and GitHub repository for an existing absolute cwd. Never sets up or modifies a repository.",
48170
48276
  inputSchema: strictObject({
48171
- cwd: string2().refine(isAbsolute4, "cwd must be an absolute existing directory")
48277
+ cwd: string2().refine(isAbsolute6, "cwd must be an absolute existing directory")
48172
48278
  }),
48173
48279
  outputSchema: projectContextOutput,
48174
48280
  annotations: { readOnlyHint: true, openWorldHint: false }
@@ -48176,7 +48282,7 @@ function createProjectTools(home) {
48176
48282
  ];
48177
48283
  }
48178
48284
  var setupHint = "Use the difflab-init skill to set up this repository with the Difflab CLI.", projectContextOutput, projectTools;
48179
- var init_tools2 = __esm(() => {
48285
+ var init_tools3 = __esm(() => {
48180
48286
  init_zod();
48181
48287
  init_context();
48182
48288
  projectContextOutput = strictObject({
@@ -48193,10 +48299,1171 @@ var init_tools2 = __esm(() => {
48193
48299
  projectTools = createProjectTools();
48194
48300
  });
48195
48301
 
48302
+ // templates/spec-driven-plan.md
48303
+ var spec_driven_plan_default = `# [Outcome to deliver]
48304
+
48305
+ - **Status:** draft
48306
+ - **Branch:** [branch]
48307
+ - **Issue:** [issue or none]
48308
+
48309
+ ## Intent
48310
+
48311
+ [Who needs this change, what should be possible afterward, and why now?]
48312
+
48313
+ ## Requirements
48314
+
48315
+ - [Observable outcome and how to tell whether it works.]
48316
+ - [Limits and explicit non-goals.]
48317
+
48318
+ ## Design
48319
+
48320
+ ### Big Ideas
48321
+
48322
+ [Describe the intended behavior and the main architectural choices.]
48323
+
48324
+ ### Infrastructure Changes
48325
+
48326
+ [Any infra changes. Optional. Only include if there's actual infra being handled in repo.]
48327
+
48328
+ ### Interfaces
48329
+
48330
+ [CLI commands, API endpoint, data contracts, db entities, modules, apps, libs, project tasks added, changed or removed. Summary of how a user's interaction with the app/lib/cli/sdk/etc. will change as a result.]
48331
+
48332
+ ### Consequences
48333
+
48334
+ [Trade-offs, risks, compatibility, and what is deferred.]
48335
+
48336
+ ## Phases
48337
+
48338
+ [Instructions broken down granularly, specifying modules/classes/methods/files to be added/updated/removed, and relevant technical work done with terse reasoning. Terse for human readers, sufficiently detailed for delegating implementation to less capable language models i.e. a fool proof guide for a junior developer.]
48339
+
48340
+ ### Phase 1: [Outcome]
48341
+
48342
+ - [ ] [Task A]
48343
+ - [ ] [Task B]
48344
+ - [ ] [Task C]
48345
+ - [ ] [Task D]
48346
+ - [ ] Validation
48347
+ - [ ] Lint, format, etc.
48348
+ - [ ] Commit, CI green (if expected)
48349
+
48350
+ ### Phase 2: [Outcome]
48351
+
48352
+ - [ ] [Task A]
48353
+ - [ ] [Task B]
48354
+ - [ ] [Task C]
48355
+ - [ ] [Task D]
48356
+ - [ ] Validation
48357
+ - [ ] Lint, format, etc.
48358
+ - [ ] Commit, CI green (if expected)
48359
+
48360
+ ### Phase 3: [Outcome]
48361
+
48362
+ - [ ] [Task A]
48363
+ - [ ] [Task B]
48364
+ - [ ] [Task C]
48365
+ - [ ] [Task D]
48366
+ - [ ] Validation
48367
+ - [ ] Lint, format, etc.
48368
+ - [ ] Commit, CI green (if expected)
48369
+
48370
+ ## References
48371
+
48372
+ - [Source or related decision.]
48373
+ - [External references, docs supporting the implementation plan.]
48374
+ `;
48375
+ var init_spec_driven_plan = () => {};
48376
+
48377
+ // templates/software-architecture-design.md
48378
+ var software_architecture_design_default = `# [System or feature] \u2014 software architecture
48379
+
48380
+ ## Overview
48381
+
48382
+ [Purpose, system boundary, users, and explicit non-goals.]
48383
+
48384
+ ## Requirement
48385
+
48386
+ ### Functional
48387
+
48388
+ [Functionality app, lib or product must perform.]
48389
+
48390
+ - FR1
48391
+ - FR2
48392
+ - ...
48393
+
48394
+ ### Non-Functional
48395
+
48396
+ [Performance, security, cost and other non-functional characteristics which the system must fulfil.]
48397
+
48398
+ - NFR1
48399
+ - NFR2
48400
+
48401
+ ## Design
48402
+
48403
+ ### Components and Responsibilities
48404
+
48405
+ [System diagram(s) with bullet list enumerating and explaining components and their relationships. Prefer mermaid diagrams unless user/AGENTS.md provde other stated preferences.]
48406
+
48407
+ > @agent: add example mermaid diagram
48408
+
48409
+ \`\`\`mermaid
48410
+
48411
+ \`\`\`
48412
+
48413
+ - Component A does this.
48414
+ - It then calls component B to do this.
48415
+ - And so on.
48416
+
48417
+ ### Data and Control Flow
48418
+
48419
+ [Describe the main request/event path and where state lives. Include flow charts or sequence diagrams if applicable.]
48420
+
48421
+ ### Interfaces and Contracts
48422
+
48423
+ [Database entities, input/output schemas, endpoints, core types, and public methods. Include ERD where applicable.]
48424
+
48425
+ ## Implementation
48426
+
48427
+ ### Important topic 1
48428
+
48429
+ [Owned modules, dependency direction, and extension points.]
48430
+
48431
+ ### Important topic 2
48432
+
48433
+ [More important topics.]
48434
+
48435
+ ## References
48436
+
48437
+ - [Relevant code, ADR, or external specification.]
48438
+ `;
48439
+ var init_software_architecture_design = () => {};
48440
+
48441
+ // templates/architecture-decision-record.md
48442
+ var architecture_decision_record_default = `# [Decision: problem and chosen solution]
48443
+
48444
+ - **Status:** proposed
48445
+ - **Deciders:** [people or team]
48446
+ - **Date:** [YYYY-MM-DD]
48447
+
48448
+ ## Context and Problem Statement
48449
+
48450
+ [What decision is needed, and what happens if we do nothing?]
48451
+
48452
+ ## Decision Drivers
48453
+
48454
+ - [Constraint or quality attribute.]
48455
+
48456
+ ## Considered Options
48457
+
48458
+ ### [Option A]
48459
+
48460
+ - Good: [reason.]
48461
+ - Bad: [reason.]
48462
+
48463
+ ### [Option B]
48464
+
48465
+ - Good: [reason.]
48466
+ - Bad: [reason.]
48467
+
48468
+ ## Decision Outcome
48469
+
48470
+ Chosen option: [option], because [reason and evidence].
48471
+
48472
+ ### Positive Consequences
48473
+
48474
+ - [Benefit.]
48475
+
48476
+ ### Negative Consequences
48477
+
48478
+ - [Cost or follow-up work.]
48479
+
48480
+ ## References
48481
+
48482
+ - [Related decision, issue, or evidence.]
48483
+
48484
+ ## Appendix
48485
+
48486
+ ### Appendix A
48487
+
48488
+ Useful background information.
48489
+
48490
+ ### Appendix B
48491
+
48492
+ More useful background information.
48493
+ `;
48494
+ var init_architecture_decision_record = () => {};
48495
+
48496
+ // templates/product-requirements-document.md
48497
+ var product_requirements_document_default = `# [User outcome or product change]
48498
+
48499
+ > @agent: make this closer to the PRD under prd dir!
48500
+
48501
+ - **Status:** draft
48502
+ - **Owner:** [person or team]
48503
+ - **Date:** [YYYY-MM-DD]
48504
+
48505
+ ## Summary
48506
+
48507
+ [What changes for users, and why?]
48508
+
48509
+ ## Problem and Evidence
48510
+
48511
+ [Who has the problem? Give observed evidence or mark assumptions.]
48512
+
48513
+ ## Users and Use Cases
48514
+
48515
+ - [User]: [job to be done and context.]
48516
+
48517
+ ## Goals
48518
+
48519
+ - [User outcome to achieve.]
48520
+
48521
+ ## Non-Goals
48522
+
48523
+ - [Related need that this work does not address.]
48524
+
48525
+ ## Proposed Experience
48526
+
48527
+ [Describe the user journey, inputs, outputs, errors, and recovery.]
48528
+
48529
+ ## Requirements
48530
+
48531
+ ### [Capability]
48532
+
48533
+ [Required user-facing behavior, edge cases, and constraints.]
48534
+
48535
+ **Acceptance:** [Observable way to check the requirement.]
48536
+
48537
+ ## Success Measures
48538
+
48539
+ [Signal, baseline or assumption, target, and when/how it will be measured.]
48540
+
48541
+ ## Risks and Dependencies
48542
+
48543
+ - [Risk or dependency]: [mitigation or owner.]
48544
+
48545
+ ## Open Questions
48546
+
48547
+ - [Question and decision owner.]
48548
+
48549
+ ## Rollout
48550
+
48551
+ [Release, feedback, migration, and rollback plan, if needed.]
48552
+ `;
48553
+ var init_product_requirements_document = () => {};
48554
+
48555
+ // templates/code-review.md
48556
+ var code_review_default = `# Review: [change or pull request]
48557
+
48558
+ - **Revision:** [commit or diff]
48559
+ - **Intent:** [stated objective and source]
48560
+ - **Reviewer:** [name]
48561
+
48562
+ ## Intent Delivery
48563
+
48564
+ [Does the change actually deliver the intended behavior? What is missing or outside scope?]
48565
+
48566
+ ## Readability
48567
+
48568
+ [Can a new reader follow the flow, names, and error paths? Is code readable for and understandable for an average mide-level developer at a glance?]
48569
+
48570
+ ## Semantic Organization
48571
+
48572
+ [Are responsibilities, boundaries, and public APIs arranged by meaning? Are modules semantically meaningful? Is code in applications and libraries well sliced by concerns?]
48573
+
48574
+ ## Maintainability
48575
+
48576
+ [Is the code DRY? Is the code over-engineered beyond required scope? Are well trusted / industry-standard libraries and frameworks used for standard tasks? Are domain specific conventions approprate for the codebase being followed? Is basic linting, formatting and static analysis hygiene present?]
48577
+
48578
+ ## Reliability/Testing
48579
+
48580
+ [Behavior, edge cases, test coverage, and evidence from executed checks. Adhere to repo testing standards if specified in AGENTS.md or docs. Otherwise, generally prioritize e2e coverage and test readability and organization.]
48581
+
48582
+ ## Performance
48583
+
48584
+ [Cost, allocations, I/O, scaling, and measured or expected impact. State SKIPPED with reasons for non-perf bugfixes, minor refactors, and non-performance critical applications.]
48585
+
48586
+ ## Security
48587
+
48588
+ [Evaluate authentication and autorization, permissions, secrets, dependencies, input validation, proper error handling, and trust boundaries. State SKIPPED if skipped with reason, for example for chores, simple scripting, pure cosmetic UI work, documentation, etc.]
48589
+
48590
+ ## Change Requests
48591
+
48592
+ Track actionable inline requests with one entry per request. End each generated request and response with its own model attribution quote; preserve human text unchanged.
48593
+
48594
+ ### [file:line \u2014 request]
48595
+
48596
+ - **Status:** [open/in progress/resolved]
48597
+ - **Request:** [verbatim request or concise generated finding]
48598
+
48599
+ > Generated via [actual model name, only for a generated request]
48600
+
48601
+ - **Response:** [resolution, evidence, or reason this remains open]
48602
+
48603
+ > Generated via [actual model name, only for a generated response]
48604
+
48605
+ ## Findings
48606
+
48607
+ | Severity | Location | Evidence and impact | Required change or question |
48608
+ | ------------------------ | ----------- | -------------------------- | --------------------------- |
48609
+ | [blocking/consider/note] | [file:line] | [reproducible observation] | [concrete action] |
48610
+
48611
+ ## Overall Assessment
48612
+
48613
+ [Recommendation and reasons; distinguish findings from preferences.]
48614
+
48615
+ ## Verification and Limits
48616
+
48617
+ - **Checks run:** [command and result.]
48618
+ - **Not verified:** [environment, path, or behavior not checked.]
48619
+ `;
48620
+ var init_code_review = () => {};
48621
+
48622
+ // templates/planning-intent.md
48623
+ var planning_intent_default = `# Intent: [short name]
48624
+
48625
+ ## Objective
48626
+
48627
+ [What do you want to add/change?]
48628
+
48629
+ ## Requirements/Instructions
48630
+
48631
+ [Requirements deliverable must meet. Alternately, imperative instructions which must be followed.]
48632
+
48633
+ ## Constraints
48634
+
48635
+ [Implementation requirements (particularly non-functional ones), important invariants, behavioral must-haves or limits. Optional.]
48636
+
48637
+ ## Open Questions
48638
+
48639
+ [Decisions you want help making. Optional.]
48640
+ `;
48641
+ var init_planning_intent = () => {};
48642
+
48643
+ // templates/ui-component-architecture.md
48644
+ var ui_component_architecture_default = `# [UI component name]
48645
+
48646
+ ## Overview
48647
+
48648
+ [What does this component do, who uses it, and where does it appear?]
48649
+
48650
+ ## Design
48651
+
48652
+ ### Layout
48653
+
48654
+ \`\`\`text
48655
+ +---------------------------------------+
48656
+ | [Title] [Action] |
48657
+ |----------------------------------------|
48658
+ | [Main content] |
48659
+ | [Status or feedback] |
48660
+ +---------------------------------------+
48661
+ \`\`\`
48662
+
48663
+ [Replace the sketch with the actual component layout, including responsive changes. Could also be an image or inline svg.]
48664
+
48665
+ ### Behavior
48666
+
48667
+ - **Default:** [Initial content and action.]
48668
+ - **Interaction:** [Inputs, focus changes, and responses.]
48669
+ - **Loading, empty, and error:** [What users see and how they recover.]
48670
+ - **Accessibility:** [Keyboard path, semantics, labels, and announcements.]
48671
+
48672
+ ## Implementation
48673
+
48674
+ ### Public API
48675
+
48676
+ [Props, events, slots, and example usage.]
48677
+
48678
+ ### State and Data Flow
48679
+
48680
+ [Who owns state? What is derived? What is persisted?]
48681
+
48682
+ ### Styling and Responsiveness
48683
+
48684
+ [Tokens, breakpoints, layout constraints, loading behavior, interaction design, etc. Optional. Often theme controlled.]
48685
+
48686
+ ### Maintainer Notes
48687
+
48688
+ [Non-obvious invariants, testing strategy, performance concerns, and known limits.]
48689
+ `;
48690
+ var init_ui_component_architecture = () => {};
48691
+
48692
+ // templates/pull-request-description.md
48693
+ var pull_request_description_default = `## Intent
48694
+
48695
+ {{intent}}
48696
+
48697
+ ## Changes
48698
+
48699
+ {{changes}}
48700
+
48701
+ ## Validation
48702
+
48703
+ {{validation}}
48704
+
48705
+ ## Further Work
48706
+
48707
+ {{further_work}}
48708
+
48709
+ ## References
48710
+
48711
+ - Issue: {{issue_url}}
48712
+ `;
48713
+ var init_pull_request_description = () => {};
48714
+
48715
+ // templates/examples/plan-feature.md
48716
+ var plan_feature_default = `# Invite teammates to a project
48717
+
48718
+ - **Status:** draft
48719
+ - **Branch:** feature/project-invites
48720
+ - **Issue:** EX-12
48721
+
48722
+ ## Intent
48723
+
48724
+ Project owners need to invite teammates without sharing credentials. Recipients should know which project they are joining before accepting.
48725
+
48726
+ ## Requirements
48727
+
48728
+ - An owner can send an invite to a valid email address and see its pending state.
48729
+ - Only the intended recipient can accept; expired or used invites fail without changing membership.
48730
+ - Bulk invitations and organization-level roles are out of scope.
48731
+
48732
+ ## Design
48733
+
48734
+ ### Big Ideas
48735
+
48736
+ Store a single-use invitation tied to project, email, and expiry. Acceptance adds membership in the same transaction that consumes the invitation. Queue delivery after saving the invite so a temporary email failure cannot lose it.
48737
+
48738
+ ### Interfaces
48739
+
48740
+ \`POST /projects/:id/invitations\` creates a pending invite; \`POST /invitations/:token/accept\` returns membership or a safe expired/used error. \`src/invitations/store.ts\` owns persistence; \`src/invitations/service.ts\` owns eligibility, expiry, and token consumption; \`src/http/invitations.ts\` maps results to responses.
48741
+
48742
+ ### Consequences
48743
+
48744
+ Email delivery can fail after persistence, so queue retries and show a pending invitation. Rate-limit creation to avoid abuse. Resend and cancellation are deferred.
48745
+
48746
+ ## Phases
48747
+
48748
+ ### Phase 1: Persist and consume invitations
48749
+
48750
+ - [ ] Add a unique token hash, recipient email, project ID, expiry, and consumed timestamp to invitation storage in \`src/invitations/store.ts\`; never log raw tokens.
48751
+ - [ ] Implement transactional membership creation and token consumption in \`src/invitations/service.ts\`; reject expired, used, or wrong-recipient tokens without a membership change.
48752
+ - [ ] Add boundary, repeat-use, and rollback tests in \`src/invitations/service.test.ts\`.
48753
+ - [ ] Validation
48754
+ - [ ] Run invitation unit tests, lint, and format checks.
48755
+ - [ ] Commit this phase and confirm CI is green.
48756
+
48757
+ ### Phase 2: Expose and deliver invitations
48758
+
48759
+ - [ ] Add owner creation and recipient acceptance handlers in \`src/http/invitations.ts\` with safe error responses and creation rate limits.
48760
+ - [ ] Queue delivery after persistence, retry transient mail failures, and expose the pending state to owners.
48761
+ - [ ] Add endpoint integration tests for authorization, expiry, used tokens, and delivery failures in \`src/http/invitations.spec.ts\`.
48762
+ - [ ] Validation
48763
+ - [ ] Run unit and integration tests, lint, and format checks.
48764
+ - [ ] Commit this phase and confirm CI is green.
48765
+
48766
+ ## References
48767
+
48768
+ - EX-12 \u2014 sample product request.
48769
+ `;
48770
+ var init_plan_feature = () => {};
48771
+
48772
+ // templates/examples/plan-migration.md
48773
+ var plan_migration_default = `# Move activity history to a new store
48774
+
48775
+ - **Status:** draft
48776
+ - **Branch:** feature/activity-migration
48777
+ - **Issue:** EX-27
48778
+
48779
+ ## Intent
48780
+
48781
+ Readers should retain access to historical activity while the team changes storage engines without downtime.
48782
+
48783
+ ## Requirements
48784
+
48785
+ - Old and new records produce the same user-visible history, order, and cursors.
48786
+ - Rollback can restore the prior reader without losing activity written during migration.
48787
+ - No reader switches until backfill and comparison pass.
48788
+
48789
+ ## Design
48790
+
48791
+ ### Big Ideas
48792
+
48793
+ Write to both stores temporarily, backfill older records with stable IDs, compare counts and ordering, then switch readers behind a reversible flag. Keep the legacy reader authoritative until the new store is verified.
48794
+
48795
+ ### Infrastructure Changes
48796
+
48797
+ Provision the new activity store and credentials before dual writes. Retain the legacy store and its capacity until the rollback window closes; monitor write failures for each store separately.
48798
+
48799
+ ### Interfaces
48800
+
48801
+ \`listActivity(projectId, cursor)\` preserves ordering and cursor behavior during cutover. \`src/activity/writer.ts\` owns dual writes, \`src/activity/backfill.ts\` copies historical records, and \`src/activity/reader.ts\` selects the store behind the feature flag.
48802
+
48803
+ ### Consequences
48804
+
48805
+ Dual writes increase latency and create a risk of partial failure. Record discrepancies and retry failed writes instead of hiding them. Removing the legacy store is outside this plan.
48806
+
48807
+ ## Phases
48808
+
48809
+ ### Phase 1: Provision and dual-write
48810
+
48811
+ - [ ] Provision the new store and credentials; add write-failure metrics for both stores.
48812
+ - [ ] Make \`src/activity/writer.ts\` write idempotently to both stores with stable activity IDs while retaining the legacy reader.
48813
+ - [ ] Test partial-write retries and duplicate events in \`src/activity/writer.test.ts\`.
48814
+ - [ ] Validation
48815
+ - [ ] Run writer tests, lint, and format checks; confirm write-failure metrics are visible.
48816
+ - [ ] Commit this phase and confirm CI is green.
48817
+
48818
+ ### Phase 2: Backfill and compare
48819
+
48820
+ - [ ] Add resumable, idempotent batches in \`src/activity/backfill.ts\` without replacing newer dual-written records.
48821
+ - [ ] Compare record IDs, counts, and ordering per project; report mismatches before any reader switch.
48822
+ - [ ] Add backfill restart and parity tests in \`src/activity/backfill.test.ts\`.
48823
+ - [ ] Validation
48824
+ - [ ] Run backfill and writer tests, lint, and format checks; review mismatch reports.
48825
+ - [ ] Commit this phase and confirm CI is green.
48826
+
48827
+ ### Phase 3: Switch reads with rollback
48828
+
48829
+ - [ ] Route \`listActivity(projectId, cursor)\` through a reversible flag in \`src/activity/reader.ts\` and preserve cursor compatibility.
48830
+ - [ ] Test new-store reads, cursor boundaries, and rollback to the legacy reader in \`src/activity/reader.spec.ts\`.
48831
+ - [ ] Keep dual writes and monitor parity through the rollback window.
48832
+ - [ ] Validation
48833
+ - [ ] Run unit and integration tests, lint, and format checks; verify flag rollback.
48834
+ - [ ] Commit this phase and confirm CI is green.
48835
+
48836
+ ## References
48837
+
48838
+ - EX-27 \u2014 sample migration request.
48839
+ `;
48840
+ var init_plan_migration = () => {};
48841
+
48842
+ // templates/examples/design-service.md
48843
+ var design_service_default = `# Notification service \u2014 software architecture
48844
+
48845
+ ## Overview
48846
+
48847
+ The service delivers email notifications for account events. It consumes messages from the application queue; it does not decide who should receive a message or block account requests on email delivery.
48848
+
48849
+ ## Requirement
48850
+
48851
+ ### Functional
48852
+
48853
+ - FR1: Consume and validate account events with an ID, type, recipient, and timestamp.
48854
+ - FR2: Deduplicate deliveries, send email, and record each attempt and its outcome.
48855
+ - FR3: Retry transient provider failures and report permanent rejections.
48856
+
48857
+ ### Non-Functional
48858
+
48859
+ - NFR1: Queue redelivery must not send the same event twice; retain a durable event ID for deduplication.
48860
+ - NFR2: Provider credentials must stay outside event payloads and the account service.
48861
+ - NFR3: A burst of events must not delay account requests; alert when the queue grows.
48862
+
48863
+ ## Design
48864
+
48865
+ ### Components and Responsibilities
48866
+
48867
+ \`\`\`mermaid
48868
+ flowchart LR
48869
+ Account[Account service] -->|Account event| Queue[(Durable queue)]
48870
+ Queue --> Consumer[Notification consumer]
48871
+ Consumer --> Sender[Email sender]
48872
+ Sender --> Provider[Email API]
48873
+ Consumer --> Store[(Attempt store)]
48874
+ \`\`\`
48875
+
48876
+ - The account service publishes events and knows only the event contract.
48877
+ - The durable queue isolates delivery from account requests and retries transient failures.
48878
+ - The consumer validates and deduplicates events; the sender formats and submits mail to the provider.
48879
+ - The attempt store records event IDs, delivery outcomes, and retry attempts.
48880
+
48881
+ ### Data and Control Flow
48882
+
48883
+ An account event enters the queue. The consumer validates it and checks the attempt store for its ID before the sender calls the provider. The consumer records the result; a transient failure is retried, while a permanent rejection is reported. Sending synchronously would avoid the queue but could delay account requests whenever the provider is slow.
48884
+
48885
+ ### Interfaces and Contracts
48886
+
48887
+ \`AccountEvent\` contains \`id\`, \`type\`, \`recipient\`, and \`timestamp\`. \`handle(event)\` consumes the event, \`send(message)\` calls the provider, and \`record(attempt)\` persists its result. The event ID is the deduplication key; provider credentials never enter the payload.
48888
+
48889
+ ## Implementation
48890
+
48891
+ ### Consumer and storage
48892
+
48893
+ The notification service owns its consumer and attempt store; the account service imports only the event contract. Persist the deduplication key with the delivery outcome so queue redelivery is safe. Integration tests simulate duplicate events and provider timeouts.
48894
+
48895
+ ### Delivery operations
48896
+
48897
+ Keep provider credentials in the notification service. Alert on a growing queue and repeated provider failures; disable sending and drain pending work if credentials are compromised. Load tests cover bursts without slowing the account service.
48898
+
48899
+ ## References
48900
+
48901
+ - EX-34 \u2014 example notification requirement.
48902
+ `;
48903
+ var init_design_service = () => {};
48904
+
48905
+ // templates/examples/adr-storage.md
48906
+ var adr_storage_default = `# Use object storage for user exports
48907
+
48908
+ - **Status:** accepted
48909
+ - **Deciders:** Platform team
48910
+ - **Date:** 2026-01-14
48911
+
48912
+ ## Context and Problem Statement
48913
+
48914
+ Exports may exceed the response size limit of the application server. Where should completed exports live until users download them?
48915
+
48916
+ ## Decision Drivers
48917
+
48918
+ - Exports must expire after seven days.
48919
+ - The application server should not retain large files on local disk.
48920
+
48921
+ ## Considered Options
48922
+
48923
+ ### Object storage
48924
+
48925
+ - Good: Lifecycle rules remove expired exports without an application job.
48926
+ - Bad: Requires bucket credentials and signed download URLs.
48927
+
48928
+ ### Database
48929
+
48930
+ - Good: Existing backup process covers exports.
48931
+ - Bad: Large blobs make backups slow and expensive.
48932
+
48933
+ ## Decision Outcome
48934
+
48935
+ Chosen option: object storage, because it provides expiry policies and direct downloads without loading large files into application memory.
48936
+
48937
+ ### Positive Consequences
48938
+
48939
+ - Large exports do not occupy application disks.
48940
+
48941
+ ### Negative Consequences
48942
+
48943
+ - Download authorization requires short-lived signed URLs.
48944
+
48945
+ ## References
48946
+
48947
+ - EX-41 \u2014 example export requirement.
48948
+
48949
+ ## Appendix
48950
+
48951
+ ### Appendix A
48952
+
48953
+ A seven-day lifecycle rule deletes exports even if a user never downloads them. Issue signed URLs only after checking that the requester owns the export.
48954
+
48955
+ ### Appendix B
48956
+
48957
+ An export job records the object key and expiry time so the download endpoint can reject expired requests before generating a URL.
48958
+ `;
48959
+ var init_adr_storage = () => {};
48960
+
48961
+ // templates/examples/prd-onboarding.md
48962
+ var prd_onboarding_default = `# Help new members finish project onboarding
48963
+
48964
+ - **Status:** draft
48965
+ - **Owner:** Product team
48966
+ - **Date:** 2026-01-14
48967
+
48968
+ ## Summary
48969
+
48970
+ Give invited members a short checklist so they can find their project and complete their first task.
48971
+
48972
+ ## Problem and Evidence
48973
+
48974
+ New members frequently ask where to start after accepting an invite. This is a product assumption until interviews confirm the pattern.
48975
+
48976
+ ## Users and Use Cases
48977
+
48978
+ - New project member: find the project, read its purpose, and finish a first task.
48979
+
48980
+ ## Goals
48981
+
48982
+ - Members can find their first task without contacting an administrator.
48983
+
48984
+ ## Non-Goals
48985
+
48986
+ - Training on every product feature.
48987
+
48988
+ ## Proposed Experience
48989
+
48990
+ After joining, a member sees a three-step checklist with links to the project overview and an assigned starter task. Dismissal is reversible from the help menu.
48991
+
48992
+ ## Requirements
48993
+
48994
+ ### Visible next step
48995
+
48996
+ Show one task assigned to the member, or a clear empty state when none exists.
48997
+
48998
+ **Acceptance:** A member with no assigned task sees an actionable empty state, not a blank panel.
48999
+
49000
+ ## Success Measures
49001
+
49002
+ Measure the share of invited members who open a project and complete a task within seven days; record a baseline before launch.
49003
+
49004
+ ## Risks and Dependencies
49005
+
49006
+ - Task assignment data may be delayed: refresh the checklist when assignment changes.
49007
+
49008
+ ## Open Questions
49009
+
49010
+ - Should dismissed checklists reappear for new projects? Product owner to decide.
49011
+
49012
+ ## Rollout
49013
+
49014
+ Enable for a small cohort, compare completion and feedback, then expand or disable.
49015
+ `;
49016
+ var init_prd_onboarding = () => {};
49017
+
49018
+ // templates/examples/review-change.md
49019
+ var review_change_default = `# Review: invitation expiry change
49020
+
49021
+ - **Revision:** example-change-17
49022
+ - **Intent:** Reject expired invitations without creating membership.
49023
+ - **Reviewer:** Example reviewer
49024
+
49025
+ ## Intent Delivery
49026
+
49027
+ The acceptance path checks expiry before membership creation. The resend path was not part of this change.
49028
+
49029
+ ## Readability
49030
+
49031
+ Function names describe outcomes, and the request path is understandable without tracing helpers. Move the expiry guard before token decoding so a mid-level maintainer can see the rejection path at a glance.
49032
+
49033
+ ## Semantic Organization
49034
+
49035
+ Invitation policy belongs in the invitation service, not the HTTP handler. The handler should own transport errors; the service should own token and membership rules.
49036
+
49037
+ ## Maintainability
49038
+
49039
+ The shared expiry helper avoids duplicate rules in request and background paths. No new library or abstraction is needed for this change; formatting and static analysis should remain clean.
49040
+
49041
+ ## Reliability/Testing
49042
+
49043
+ Tests cover expiry at the boundary and a used token. A clock-skew case is missing; add a readable test that checks the server clock source and confirms no membership is created on rejection.
49044
+
49045
+ ## Performance
49046
+
49047
+ One lookup occurs per acceptance; no new unbounded loop is present.
49048
+
49049
+ ## Security
49050
+
49051
+ The response does not reveal whether an address has an account. The recipient is authorized before the token is consumed, tokens are treated as opaque values, and failed acceptance does not create membership.
49052
+
49053
+ ## Findings
49054
+
49055
+ | Severity | Location | Evidence and impact | Required change or question |
49056
+ | -------- | ----------------------- | ------------------------------------------------- | ----------------------------------------------------- |
49057
+ | consider | \`src/invitations.ts:52\` | Clock skew near expiry may reject valid requests. | Add a boundary test and document server clock source. |
49058
+
49059
+ ## Overall Assessment
49060
+
49061
+ No blocking issue found in the reviewed path; address the boundary test before release.
49062
+
49063
+ ## Verification and Limits
49064
+
49065
+ - **Checks run:** \`bun test src/invitations.test.ts\` \u2014 passed in this example.
49066
+ - **Not verified:** Real email delivery and production clock behavior.
49067
+ `;
49068
+ var init_review_change = () => {};
49069
+
49070
+ // templates/examples/intent-request.md
49071
+ var intent_request_default = `# Intent: Faster project search
49072
+
49073
+ ## Objective
49074
+
49075
+ People with many projects need to find a project by name without scrolling through the full list. Support has received requests about this, but we do not yet have a measured search-time baseline.
49076
+
49077
+ ## Requirements/Instructions
49078
+
49079
+ - Let users search accessible projects by name while keeping the existing project list available.
49080
+ - Do not add search for tasks or users.
49081
+
49082
+ ## Constraints
49083
+
49084
+ Search must work from the keyboard and must not reveal projects a user cannot access.
49085
+
49086
+ ## Open Questions
49087
+
49088
+ Should results include archived projects by default?
49089
+ `;
49090
+ var init_intent_request = () => {};
49091
+
49092
+ // templates/examples/component-dialog.md
49093
+ var component_dialog_default = `# ConfirmDeleteDialog
49094
+
49095
+ ## Overview
49096
+
49097
+ A modal asks for confirmation before a destructive action. It appears only when the user chooses Delete.
49098
+
49099
+ ## Design
49100
+
49101
+ ### Layout
49102
+
49103
+ \`\`\`text
49104
+ +------------------------------------------+
49105
+ | Delete project? [X] |
49106
+ |------------------------------------------|
49107
+ | This removes the project and its tasks. |
49108
+ | [Cancel] [Delete] |
49109
+ +------------------------------------------+
49110
+ \`\`\`
49111
+
49112
+ The buttons stack on narrow screens while preserving Cancel before Delete in keyboard order.
49113
+
49114
+ ### Behavior
49115
+
49116
+ - **Default:** Focus the title or Cancel button; explain the affected project.
49117
+ - **Interaction:** Cancel and Escape close without deleting; Delete submits once.
49118
+ - **Loading, empty, and error:** Disable repeat submit; report server failure and allow retry.
49119
+ - **Accessibility:** Use a labelled modal, trap focus while open, and restore focus to Delete trigger on close.
49120
+
49121
+ ## Implementation
49122
+
49123
+ ### Public API
49124
+
49125
+ \`ConfirmDeleteDialog({ projectName, open, onCancel, onConfirm })\` receives the project name and emits confirmation only once per open cycle.
49126
+
49127
+ ### State and Data Flow
49128
+
49129
+ Parent owns \`open\` and performs deletion; the dialog owns only pending/error display.
49130
+
49131
+ ### Styling and Responsiveness
49132
+
49133
+ Use shared spacing tokens and a 28rem maximum width; stack actions below 22rem.
49134
+
49135
+ ### Maintainer Notes
49136
+
49137
+ Use the design system modal primitive for focus management. Test Escape, focus restoration, double-submit prevention, and the server error path.
49138
+ `;
49139
+ var init_component_dialog = () => {};
49140
+
49141
+ // templates/examples/component-navigation.md
49142
+ var component_navigation_default = `# ProjectNavigation
49143
+
49144
+ ## Overview
49145
+
49146
+ A navigation rail lets members switch between project sections. The active section is always identifiable without color alone.
49147
+
49148
+ ## Design
49149
+
49150
+ ### Layout
49151
+
49152
+ \`\`\`text
49153
+ +---------------------+
49154
+ | Project name |
49155
+ |---------------------|
49156
+ | > Overview |
49157
+ | Tasks |
49158
+ | Activity |
49159
+ |---------------------|
49160
+ | Settings |
49161
+ +---------------------+
49162
+ \`\`\`
49163
+
49164
+ At narrow widths, the rail becomes a labelled navigation menu.
49165
+
49166
+ ### Behavior
49167
+
49168
+ - **Default:** Highlight the route that matches the current page.
49169
+ - **Interaction:** Arrow keys move focus within the menu; Enter activates a route.
49170
+ - **Loading, empty, and error:** Render available routes while project metadata loads; show a safe fallback label on failure.
49171
+ - **Accessibility:** Use a \`nav\` landmark, accessible item names, and \`aria-current="page"\` for the active link.
49172
+
49173
+ ## Implementation
49174
+
49175
+ ### Public API
49176
+
49177
+ \`ProjectNavigation({ project, currentRoute, routes })\` receives visible routes already filtered for permissions.
49178
+
49179
+ ### State and Data Flow
49180
+
49181
+ The router owns navigation state; the component derives the active item from \`currentRoute\`.
49182
+
49183
+ ### Styling and Responsiveness
49184
+
49185
+ Use existing navigation tokens; collapse below the compact breakpoint.
49186
+
49187
+ ### Maintainer Notes
49188
+
49189
+ Route links come from the application router; do not duplicate permission checks in display code. Test route changes, keyboard operation, permission-filtered items, and compact layout.
49190
+ `;
49191
+ var init_component_navigation = () => {};
49192
+
49193
+ // templates/examples/pull-request-feature.md
49194
+ var pull_request_feature_default = `## Intent
49195
+
49196
+ Let project owners invite teammates without sharing credentials.
49197
+
49198
+ ## Changes
49199
+
49200
+ - Added a single-use invitation token with an expiry.
49201
+ - Added an acceptance endpoint that creates membership and consumes the token atomically.
49202
+ - Added owner and recipient error handling.
49203
+
49204
+ ## Validation
49205
+
49206
+ - \`bun test src/invitations.test.ts\` \u2014 passed in this example.
49207
+ - Manual check: expired and already-used tokens did not grant membership.
49208
+
49209
+ ## Further Work
49210
+
49211
+ Add invitation resend and cancellation in a separate change.
49212
+
49213
+ ## References
49214
+
49215
+ - Issue: EX-12 (example)
49216
+ `;
49217
+ var init_pull_request_feature = () => {};
49218
+
49219
+ // src/templates/source.ts
49220
+ var catalog, contents, embeddedSource;
49221
+ var init_source = __esm(() => {
49222
+ init_spec_driven_plan();
49223
+ init_software_architecture_design();
49224
+ init_architecture_decision_record();
49225
+ init_product_requirements_document();
49226
+ init_code_review();
49227
+ init_planning_intent();
49228
+ init_ui_component_architecture();
49229
+ init_pull_request_description();
49230
+ init_plan_feature();
49231
+ init_plan_migration();
49232
+ init_design_service();
49233
+ init_adr_storage();
49234
+ init_prd_onboarding();
49235
+ init_review_change();
49236
+ init_intent_request();
49237
+ init_component_dialog();
49238
+ init_component_navigation();
49239
+ init_pull_request_feature();
49240
+ catalog = [
49241
+ { name: "spec-driven-plan", description: "Spec-driven implementation plan" },
49242
+ { name: "software-architecture-design", description: "Software architecture design" },
49243
+ { name: "architecture-decision-record", description: "Architecture decision record" },
49244
+ { name: "product-requirements-document", description: "Product requirements document" },
49245
+ { name: "code-review", description: "Thorough code review" },
49246
+ { name: "planning-intent", description: "Planning intent for a user to fill in" },
49247
+ { name: "ui-component-architecture", description: "UI component design and implementation" },
49248
+ { name: "pull-request-description", description: "Draft pull request description" }
49249
+ ];
49250
+ contents = {
49251
+ "spec-driven-plan.md": spec_driven_plan_default,
49252
+ "software-architecture-design.md": software_architecture_design_default,
49253
+ "architecture-decision-record.md": architecture_decision_record_default,
49254
+ "product-requirements-document.md": product_requirements_document_default,
49255
+ "code-review.md": code_review_default,
49256
+ "planning-intent.md": planning_intent_default,
49257
+ "ui-component-architecture.md": ui_component_architecture_default,
49258
+ "pull-request-description.md": pull_request_description_default,
49259
+ "examples/plan-feature.md": plan_feature_default,
49260
+ "examples/plan-migration.md": plan_migration_default,
49261
+ "examples/design-service.md": design_service_default,
49262
+ "examples/adr-storage.md": adr_storage_default,
49263
+ "examples/prd-onboarding.md": prd_onboarding_default,
49264
+ "examples/review-change.md": review_change_default,
49265
+ "examples/intent-request.md": intent_request_default,
49266
+ "examples/component-dialog.md": component_dialog_default,
49267
+ "examples/component-navigation.md": component_navigation_default,
49268
+ "examples/pull-request-feature.md": pull_request_feature_default
49269
+ };
49270
+ embeddedSource = {
49271
+ catalog,
49272
+ assets: Object.keys(contents),
49273
+ async read(asset) {
49274
+ const content = contents[asset];
49275
+ if (content === undefined)
49276
+ throw new Error(`Missing embedded template asset: ${asset}`);
49277
+ return content;
49278
+ }
49279
+ };
49280
+ });
49281
+
49282
+ // src/templates/service.ts
49283
+ var exports_service = {};
49284
+ __export(exports_service, {
49285
+ TemplateService: () => TemplateService
49286
+ });
49287
+ import { constants as constants2 } from "fs";
49288
+ import { lstat as lstat7, open as open3, rm as rm3 } from "fs/promises";
49289
+ import { isAbsolute as isAbsolute7, join as join6 } from "path";
49290
+
49291
+ class TemplateService {
49292
+ home;
49293
+ source;
49294
+ constructor(options = {}) {
49295
+ this.home = resolveHomeDirectory(options.home);
49296
+ this.source = options.source ?? embeddedSource;
49297
+ }
49298
+ async listTemplates() {
49299
+ await this.installMissing();
49300
+ return [...this.source.catalog];
49301
+ }
49302
+ async scaffoldFromTemplate(name, cwd, path, filename) {
49303
+ if (!this.source.catalog.some((item) => item.name === name)) {
49304
+ throw new UnknownTemplateError(`Unknown template: ${name}`);
49305
+ }
49306
+ if (!path.trim() || isAbsolute7(path)) {
49307
+ throw new TemplateError("path must be a non-empty relative directory inside cwd");
49308
+ }
49309
+ if (!filename.trim() || filename === "." || filename === ".." || /[/\\\0]/.test(filename)) {
49310
+ throw new TemplateError("filename must be a single non-empty file name");
49311
+ }
49312
+ await this.installMissing();
49313
+ const installed = await resolvePath(this.home, join6(".difflab", "templates", `${name}.md`));
49314
+ const contents = await readMarkdown(installed);
49315
+ const destination = path.startsWith(".difflab/") ? await this.projectDestination(cwd, path.slice(".difflab/".length), filename) : await ensurePathDirsExist(join6(path, filename), cwd);
49316
+ try {
49317
+ await createExclusive(destination, contents);
49318
+ } catch (error) {
49319
+ if (hasErrorCode(error, "EEXIST")) {
49320
+ throw new TemplateDestinationExistsError(`Destination already exists: ${destination}`, {
49321
+ cause: error
49322
+ });
49323
+ }
49324
+ throw error;
49325
+ }
49326
+ return destination;
49327
+ }
49328
+ async projectDestination(cwd, path, filename) {
49329
+ if (!isAbsolute7(cwd))
49330
+ throw new TemplateError("cwd must be an absolute path");
49331
+ const git = discoverGitRepository(cwd);
49332
+ const config = await readRepoConfig(git.root);
49333
+ if (!config)
49334
+ throw new TemplateError(`Initialize this repository with difflab init: ${git.root}`);
49335
+ const target = join6(userPaths(this.home).projects, config.project.id, repositorySlug(canonicalGitUrl(git.originUrl)));
49336
+ await checkRepoStore(git.root, target);
49337
+ return ensurePathDirsExist(join6(path, filename), target);
49338
+ }
49339
+ async installMissing() {
49340
+ for (const asset of this.source.assets) {
49341
+ const relative = join6(".difflab", "templates", asset);
49342
+ const destination = await ensurePathDirsExist(relative, this.home);
49343
+ try {
49344
+ if ((await lstat7(destination)).isSymbolicLink()) {
49345
+ throw new TemplateSourceError(`Installed template asset is a symlink: ${destination}`);
49346
+ }
49347
+ await readMarkdown(destination);
49348
+ } catch (error) {
49349
+ if (!hasErrorCode(error, "ENOENT"))
49350
+ throw error;
49351
+ const content = await readSource(this.source, asset);
49352
+ try {
49353
+ await createExclusive(destination, content);
49354
+ } catch (writeError) {
49355
+ if (!hasErrorCode(writeError, "EEXIST"))
49356
+ throw writeError;
49357
+ await readMarkdown(destination);
49358
+ }
49359
+ }
49360
+ }
49361
+ }
49362
+ }
49363
+ async function readSource(source, asset) {
49364
+ try {
49365
+ const content = await source.read(asset);
49366
+ if (!content.trim())
49367
+ throw new TemplateSourceError(`Template is empty: ${asset}`);
49368
+ return content;
49369
+ } catch (error) {
49370
+ if (error instanceof TemplateSourceError)
49371
+ throw error;
49372
+ throw new TemplateSourceError(`Cannot read template asset: ${asset}`, { cause: error });
49373
+ }
49374
+ }
49375
+ async function readMarkdown(path) {
49376
+ let handle;
49377
+ try {
49378
+ handle = await open3(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
49379
+ if (!(await handle.stat()).isFile())
49380
+ throw new TemplateSourceError(`Template is not a file: ${path}`);
49381
+ const content = await handle.readFile("utf8");
49382
+ if (!content.trim())
49383
+ throw new TemplateSourceError(`Template is empty: ${path}`);
49384
+ return content;
49385
+ } catch (error) {
49386
+ if (error instanceof TemplateSourceError)
49387
+ throw error;
49388
+ throw new TemplateSourceError(`Cannot read template asset: ${path}`, { cause: error });
49389
+ } finally {
49390
+ await handle?.close();
49391
+ }
49392
+ }
49393
+ async function createExclusive(path, content) {
49394
+ const handle = await open3(path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
49395
+ try {
49396
+ await handle.writeFile(content, "utf8");
49397
+ } catch (error) {
49398
+ await handle.close();
49399
+ await rm3(path, { force: true });
49400
+ throw error;
49401
+ } finally {
49402
+ await handle.close();
49403
+ }
49404
+ }
49405
+ var TemplateError, UnknownTemplateError, TemplateSourceError, TemplateDestinationExistsError;
49406
+ var init_service = __esm(() => {
49407
+ init_fsx();
49408
+ init_osx();
49409
+ init_pathx();
49410
+ init_gitx2();
49411
+ init_config();
49412
+ init_repo_store();
49413
+ init_user_store();
49414
+ init_source();
49415
+ TemplateError = class TemplateError extends Error {
49416
+ };
49417
+ UnknownTemplateError = class UnknownTemplateError extends TemplateError {
49418
+ };
49419
+ TemplateSourceError = class TemplateSourceError extends TemplateError {
49420
+ };
49421
+ TemplateDestinationExistsError = class TemplateDestinationExistsError extends TemplateError {
49422
+ };
49423
+ });
49424
+
49425
+ // src/templates/tools.ts
49426
+ var templateInfo, templateListOutput, scaffoldOutput, templateTools;
49427
+ var init_tools4 = __esm(() => {
49428
+ init_zod();
49429
+ init_zodx();
49430
+ init_service();
49431
+ templateInfo = object({
49432
+ name: string2(),
49433
+ description: string2()
49434
+ });
49435
+ templateListOutput = object({ result: array(templateInfo) });
49436
+ scaffoldOutput = object({
49437
+ result: object({ name: string2(), path: string2() })
49438
+ });
49439
+ templateTools = [
49440
+ defineTool("template_list", {
49441
+ description: "List eight document templates; install any missing defaults and examples in the user templates directory without overwriting local edits.",
49442
+ inputSchema: strictObject({}),
49443
+ outputSchema: templateListOutput,
49444
+ annotations: { openWorldHint: false }
49445
+ }, async () => new TemplateService().listTemplates(), { toStructuredContent: (result) => ({ result }) }),
49446
+ defineTool("scaffold", {
49447
+ description: "Copy a named installed template into a new file under absolute cwd in relative directory path with the supplied filename. Create missing directories; never overwrite an existing file.",
49448
+ inputSchema: strictObject({
49449
+ name: string2().describe("Hyphenated template name from template_list, such as spec-driven-plan"),
49450
+ cwd: absolutePath.describe("Absolute path to an existing destination base directory"),
49451
+ path: relativePath.describe("Relative directory inside cwd; use . for cwd itself; missing directories are created"),
49452
+ filename: fileName.describe("Name of the new file inside path, such as my-plan.md; no separators")
49453
+ }),
49454
+ outputSchema: scaffoldOutput,
49455
+ annotations: { openWorldHint: false }
49456
+ }, async ({ name, cwd, path, filename }) => ({
49457
+ name,
49458
+ path: await new TemplateService().scaffoldFromTemplate(name, cwd, path, filename)
49459
+ }), { toStructuredContent: (result) => ({ result }) })
49460
+ ];
49461
+ });
49462
+
48196
49463
  // src/mcp/server.ts
48197
49464
  async function serveMcp(version = "0.1.0") {
48198
49465
  const server = new McpServer({ name: "difflab", version });
48199
- for (const tool of [...todoTools, ...projectTools]) {
49466
+ for (const tool of [...todoTools, ...projectTools, ...templateTools, ...logTools]) {
48200
49467
  server.registerTool(tool.name, tool.config, tool.callback);
48201
49468
  }
48202
49469
  await server.connect(new StdioServerTransport);
@@ -48206,6 +49473,8 @@ var init_server3 = __esm(() => {
48206
49473
  init_stdio2();
48207
49474
  init_tools();
48208
49475
  init_tools2();
49476
+ init_tools3();
49477
+ init_tools4();
48209
49478
  });
48210
49479
 
48211
49480
  // src/extensions/processx.ts
@@ -50529,12 +51798,25 @@ function createProgram(write = console.log, version = "0.1.0", options = {}) {
50529
51798
  await Promise.resolve().then(() => init_mcp2());
50530
51799
  await setupMcpClients(client, write);
50531
51800
  });
51801
+ const templates = program.command("templates").description("List and scaffold documents");
51802
+ const service = async () => options.templateService ?? new (await Promise.resolve().then(() => (init_service(), exports_service))).TemplateService;
51803
+ templates.command("list").description("List available document templates").action(async () => {
51804
+ const templatesService = await service();
51805
+ for (const template of await templatesService.listTemplates()) {
51806
+ write(`${template.name} \u2014 ${template.description}`);
51807
+ }
51808
+ });
51809
+ templates.command("scaffold <name> <path> <filename>").description("Copy a template into a new document inside a directory; never overwrite").option("--cwd <directory>", "absolute base directory for the relative destination path").action(async (name, path, filename, options) => {
51810
+ const templatesService = await service();
51811
+ const destination = await templatesService.scaffoldFromTemplate(name, options.cwd ?? process.cwd(), path, filename);
51812
+ write(destination);
51813
+ });
50532
51814
  return program;
50533
51815
  }
50534
51816
 
50535
51817
  // src/index.ts
50536
51818
  init_errors3();
50537
- var version2 = "0.3.0";
51819
+ var version2 = "0.5.0";
50538
51820
  try {
50539
51821
  await createProgram(console.log, version2).parseAsync(process.argv);
50540
51822
  } catch (error) {