@aligndottech/cli 0.57.0 → 0.58.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 (140) hide show
  1. package/README.md +31 -18
  2. package/dist/bin.d.ts +3 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +14 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/commands/doctor.d.ts.map +1 -1
  7. package/dist/commands/doctor.js +24 -2
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/import/git.d.ts.map +1 -1
  10. package/dist/commands/import/git.js +8 -4
  11. package/dist/commands/import/git.js.map +1 -1
  12. package/dist/commands/mcp.js +1 -1
  13. package/dist/commands/mcp.js.map +1 -1
  14. package/dist/commands/setup.d.ts.map +1 -1
  15. package/dist/commands/setup.js +26 -12
  16. package/dist/commands/setup.js.map +1 -1
  17. package/dist/commands/use.d.ts +2 -0
  18. package/dist/commands/use.d.ts.map +1 -1
  19. package/dist/commands/use.js +19 -1
  20. package/dist/commands/use.js.map +1 -1
  21. package/dist/commands/why.d.ts.map +1 -1
  22. package/dist/commands/why.js +91 -53
  23. package/dist/commands/why.js.map +1 -1
  24. package/dist/lib/agent-rules.d.ts +11 -4
  25. package/dist/lib/agent-rules.d.ts.map +1 -1
  26. package/dist/lib/agent-rules.js +163 -190
  27. package/dist/lib/agent-rules.js.map +1 -1
  28. package/dist/lib/ask-listing.d.ts +30 -0
  29. package/dist/lib/ask-listing.d.ts.map +1 -0
  30. package/dist/lib/ask-listing.js +60 -0
  31. package/dist/lib/ask-listing.js.map +1 -0
  32. package/dist/lib/capture-report.d.ts.map +1 -1
  33. package/dist/lib/capture-report.js +11 -3
  34. package/dist/lib/capture-report.js.map +1 -1
  35. package/dist/lib/claude-presence.d.ts +12 -0
  36. package/dist/lib/claude-presence.d.ts.map +1 -0
  37. package/dist/lib/claude-presence.js +19 -0
  38. package/dist/lib/claude-presence.js.map +1 -0
  39. package/dist/lib/config.d.ts +2 -0
  40. package/dist/lib/config.d.ts.map +1 -1
  41. package/dist/lib/config.js +4 -0
  42. package/dist/lib/config.js.map +1 -1
  43. package/dist/lib/doctor.d.ts +9 -1
  44. package/dist/lib/doctor.d.ts.map +1 -1
  45. package/dist/lib/doctor.js +19 -1
  46. package/dist/lib/doctor.js.map +1 -1
  47. package/dist/lib/embedding-assets.d.ts +40 -0
  48. package/dist/lib/embedding-assets.d.ts.map +1 -0
  49. package/dist/lib/embedding-assets.js +74 -0
  50. package/dist/lib/embedding-assets.js.map +1 -0
  51. package/dist/lib/embedding-runtime.d.ts +41 -0
  52. package/dist/lib/embedding-runtime.d.ts.map +1 -0
  53. package/dist/lib/embedding-runtime.js +224 -0
  54. package/dist/lib/embedding-runtime.js.map +1 -0
  55. package/dist/lib/fetchers/git.d.ts +3 -0
  56. package/dist/lib/fetchers/git.d.ts.map +1 -1
  57. package/dist/lib/fetchers/git.js +9 -5
  58. package/dist/lib/fetchers/git.js.map +1 -1
  59. package/dist/lib/found-summary.d.ts.map +1 -1
  60. package/dist/lib/found-summary.js +19 -3
  61. package/dist/lib/found-summary.js.map +1 -1
  62. package/dist/lib/gateway-client.d.ts.map +1 -1
  63. package/dist/lib/gateway-client.js +21 -1
  64. package/dist/lib/gateway-client.js.map +1 -1
  65. package/dist/lib/git.d.ts +7 -0
  66. package/dist/lib/git.d.ts.map +1 -1
  67. package/dist/lib/git.js +7 -2
  68. package/dist/lib/git.js.map +1 -1
  69. package/dist/lib/import-defaults.d.ts +11 -4
  70. package/dist/lib/import-defaults.d.ts.map +1 -1
  71. package/dist/lib/import-defaults.js +11 -4
  72. package/dist/lib/import-defaults.js.map +1 -1
  73. package/dist/lib/launch/mcp-child-env.d.ts.map +1 -1
  74. package/dist/lib/launch/mcp-child-env.js +6 -4
  75. package/dist/lib/launch/mcp-child-env.js.map +1 -1
  76. package/dist/lib/local-db.d.ts +13 -0
  77. package/dist/lib/local-db.d.ts.map +1 -1
  78. package/dist/lib/local-db.js +23 -0
  79. package/dist/lib/local-db.js.map +1 -1
  80. package/dist/lib/local-embeddings-native.d.ts +17 -0
  81. package/dist/lib/local-embeddings-native.d.ts.map +1 -0
  82. package/dist/lib/local-embeddings-native.js +78 -0
  83. package/dist/lib/local-embeddings-native.js.map +1 -0
  84. package/dist/lib/local-embeddings.d.ts.map +1 -1
  85. package/dist/lib/local-embeddings.js +46 -47
  86. package/dist/lib/local-embeddings.js.map +1 -1
  87. package/dist/lib/local-gateway-client.d.ts +2 -2
  88. package/dist/lib/local-gateway-client.d.ts.map +1 -1
  89. package/dist/lib/local-gateway-client.js +43 -31
  90. package/dist/lib/local-gateway-client.js.map +1 -1
  91. package/dist/lib/local-thresholds.d.ts +8 -0
  92. package/dist/lib/local-thresholds.d.ts.map +1 -1
  93. package/dist/lib/local-thresholds.js +8 -0
  94. package/dist/lib/local-thresholds.js.map +1 -1
  95. package/dist/lib/model-cache.d.ts +13 -0
  96. package/dist/lib/model-cache.d.ts.map +1 -1
  97. package/dist/lib/model-cache.js +26 -0
  98. package/dist/lib/model-cache.js.map +1 -1
  99. package/dist/lib/next-step.d.ts +2 -0
  100. package/dist/lib/next-step.d.ts.map +1 -1
  101. package/dist/lib/next-step.js +37 -16
  102. package/dist/lib/next-step.js.map +1 -1
  103. package/dist/lib/node-floor.d.ts +6 -0
  104. package/dist/lib/node-floor.d.ts.map +1 -0
  105. package/dist/lib/node-floor.js +15 -0
  106. package/dist/lib/node-floor.js.map +1 -0
  107. package/dist/lib/safe-config-write.d.ts +24 -1
  108. package/dist/lib/safe-config-write.d.ts.map +1 -1
  109. package/dist/lib/safe-config-write.js +113 -26
  110. package/dist/lib/safe-config-write.js.map +1 -1
  111. package/dist/lib/scope-real.d.ts.map +1 -1
  112. package/dist/lib/search-query.d.ts +21 -0
  113. package/dist/lib/search-query.d.ts.map +1 -1
  114. package/dist/lib/search-query.js +1 -1
  115. package/dist/lib/search-query.js.map +1 -1
  116. package/dist/lib/search-rerank.d.ts +23 -0
  117. package/dist/lib/search-rerank.d.ts.map +1 -0
  118. package/dist/lib/search-rerank.js +63 -0
  119. package/dist/lib/search-rerank.js.map +1 -0
  120. package/dist/lib/suppress-sqlite-warning.d.ts +13 -0
  121. package/dist/lib/suppress-sqlite-warning.d.ts.map +1 -0
  122. package/dist/lib/suppress-sqlite-warning.js +21 -0
  123. package/dist/lib/suppress-sqlite-warning.js.map +1 -0
  124. package/dist/lib/sync/real-env.d.ts.map +1 -1
  125. package/dist/lib/sync/spawn-background.d.ts +1 -1
  126. package/dist/lib/sync/spawn-background.d.ts.map +1 -1
  127. package/dist/lib/sync/spawn-background.js +1 -1
  128. package/dist/lib/sync/spawn-background.js.map +1 -1
  129. package/dist/lib/tar-lite.d.ts +18 -0
  130. package/dist/lib/tar-lite.d.ts.map +1 -0
  131. package/dist/lib/tar-lite.js +120 -0
  132. package/dist/lib/tar-lite.js.map +1 -0
  133. package/dist/lib/user-hooks.d.ts.map +1 -1
  134. package/dist/lib/user-hooks.js +13 -3
  135. package/dist/lib/user-hooks.js.map +1 -1
  136. package/dist/lib/verified-download.d.ts +19 -0
  137. package/dist/lib/verified-download.d.ts.map +1 -0
  138. package/dist/lib/verified-download.js +105 -0
  139. package/dist/lib/verified-download.js.map +1 -0
  140. package/package.json +6 -6
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { createHash } from 'node:crypto';
6
6
  import { existsSync, readFileSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
7
8
  import { join } from 'node:path';
8
9
  import { ensurePrivateDir, writePrivateFile } from './private-files.js';
9
10
  /**
@@ -32,4 +33,29 @@ export function modelFileCache(dir) {
32
33
  },
33
34
  };
34
35
  }
36
+ /**
37
+ * Where everything the on-device embedding path caches lives: the model files and the native
38
+ * runtime, for the npm install and the standalone binary alike (one directory to warm in CI,
39
+ * one to delete to start clean).
40
+ *
41
+ * Never inside the install tree. transformers.js defaulted to `node_modules/@huggingface/
42
+ * transformers/.cache`, which is read-only for a root-owned global prefix (`sudo npm i -g`),
43
+ * is wiped by every upgrade, and ignored XDG_CACHE_HOME.
44
+ *
45
+ * `ALIGN_MODEL_CACHE` overrides all of it. Otherwise XDG_CACHE_HOME (or ~/.cache), and
46
+ * %LOCALAPPDATA% on Windows.
47
+ */
48
+ export function modelCacheDir(env = process.env, platform = process.platform, home = homedir()) {
49
+ // Absolute only. The cache holds code the CLI loads (the native runtime), so a relative value,
50
+ // which a repo's .env can supply, would make the current repo's files the thing that gets loaded.
51
+ const override = env['ALIGN_MODEL_CACHE'];
52
+ if (override && /^(\/|[A-Za-z]:[\\/]|\\\\)/.test(override))
53
+ return override;
54
+ if (platform === 'win32') {
55
+ const base = env['LOCALAPPDATA'] || join(home, 'AppData', 'Local');
56
+ return join(base, 'align-cli', 'models');
57
+ }
58
+ const base = env['XDG_CACHE_HOME'] || join(home, '.cache');
59
+ return join(base, 'align-cli', 'models');
60
+ }
35
61
  //# sourceMappingURL=model-cache.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-cache.js","sourceRoot":"","sources":["../../src/lib/model-cache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAExE;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,MAAM,GAAG,CAAC,OAAgB,EAAU,EAAE;QAC1C,MAAM,GAAG,GAAG,OAAO,OAAO,KAAK,QAAQ;YACrC,CAAC,CAAC,OAAO;YACT,CAAC,CAAE,OAA4B,CAAC,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAChF,CAAC,CAAC;IACF,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAgB;YAC1B,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;YAC7B,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzE,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,OAAgB,EAAE,QAAkB;YAC5C,8FAA8F;YAC9F,oCAAoC;YACpC,gBAAgB,CAAC,GAAG,CAAC,CAAC;YACtB,wEAAwE;YACxE,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACvF,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"model-cache.js","sourceRoot":"","sources":["../../src/lib/model-cache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAExE;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,MAAM,GAAG,CAAC,OAAgB,EAAU,EAAE;QAC1C,MAAM,GAAG,GAAG,OAAO,OAAO,KAAK,QAAQ;YACrC,CAAC,CAAC,OAAO;YACT,CAAC,CAAE,OAA4B,CAAC,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAChF,CAAC,CAAC;IACF,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAgB;YAC1B,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;YAC7B,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzE,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,OAAgB,EAAE,QAAkB;YAC5C,8FAA8F;YAC9F,oCAAoC;YACpC,gBAAgB,CAAC,GAAG,CAAC,CAAC;YACtB,wEAAwE;YACxE,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACvF,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,MAA0C,OAAO,CAAC,GAAG,EAAE,WAAmB,OAAO,CAAC,QAAQ,EAAE,OAAe,OAAO,EAAE;IAChJ,+FAA+F;IAC/F,kGAAkG;IAClG,MAAM,QAAQ,GAAG,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC1C,IAAI,QAAQ,IAAI,2BAA2B,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC5E,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,GAAG,CAAC,cAAc,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QACnE,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC3C,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,gBAAgB,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3D,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;AAC3C,CAAC"}
@@ -33,6 +33,8 @@ export declare function orderAgents(w: WiredAgents): string[];
33
33
  export declare function projectAgentsFromWritten(written: string[]): string[];
34
34
  /** Prints only when something was actually wired: a connection claim with nothing behind it is worse than silence. */
35
35
  export declare function agentConnectedLine(agents: string[]): string | undefined;
36
+ /** The first title in the list that makes a grammatical question, as it was given. */
37
+ export declare function pickAskableTitle(titles: readonly string[]): string | undefined;
36
38
  /**
37
39
  * The question to hand the agent. Against a REAL decision the wizard found when there is one,
38
40
  * because "why did we X" is checkable against the repo the user is sitting in. The fallback
@@ -1 +1 @@
1
- {"version":3,"file":"next-step.d.ts","sourceRoot":"","sources":["../../src/lib/next-step.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAG3C;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oEAAoE;IACpE,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,EAAE,CASpD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAEpE;AAED,sHAAsH;AACtH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAGvE;AAmBD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAIpE;AAOD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB,GAAG,MAAM,CAOT;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,oFAAoF;IACpF,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;CAC9B;AAID,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAWD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAe/D"}
1
+ {"version":3,"file":"next-step.d.ts","sourceRoot":"","sources":["../../src/lib/next-step.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAG3C;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oEAAoE;IACpE,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,EAAE,CASpD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAEpE;AAED,sHAAsH;AACtH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAGvE;AAmCD,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGpE;AAOD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB,GAAG,MAAM,CAOT;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,oFAAoF;IACpF,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;CAC9B;AAID,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAWD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAe/D"}
@@ -31,21 +31,43 @@ export function agentConnectedLine(agents) {
31
31
  return `Your agent is connected: ${agents.join(', ')}.`;
32
32
  }
33
33
  /**
34
- * A decision title is only safe to drop into "why did we X?" when X reads as an action
35
- * phrase (what WAS decided), not when it already reads as its own policy statement (what
36
- * MUST happen). "why did we All PRs must pass typecheck...?" is not a sentence - the title
37
- * was the rule, not the topic. Heuristic, not a parser: a handful of quantifier/modal
38
- * openers plus a length cap catch the real failure (ALI-934 preview QA, 2026-09-11) without
39
- * needing to understand the title's grammar.
34
+ * ALI-1779: a decision title is only quoted into "why did we X?" when X is a base-form verb
35
+ * phrase, so the result is a sentence. A raw commit subject is not one: "fix: error throwing in
36
+ * reply (#6299)" has a conventional prefix and a PR number, and "Improved links to MADR" is past
37
+ * tense. The prefix and PR number are stripped, and the rest must open with a verb from the list
38
+ * below; anything else (noun phrases, past tense, a leftover colon, policy statements such as
39
+ * "All PRs must ...", over twelve words) falls back to the generic question. The list is
40
+ * deliberately short and quoting a subject is optional, so a miss costs only a generic question.
40
41
  */
41
- const POLICY_STATEMENT_OPENERS = /^(all|every|no|never|always|nobody|everyone)\b/i;
42
+ const CONVENTIONAL_PREFIX = /^(?:feat|fix|refactor|perf|types?|deps?|security)(?:\([^)]*\))?!?:\s*/i;
43
+ const PR_REFERENCE = /\s*\(#\d+[^)]*\)/g;
42
44
  const MAX_TOPIC_WORDS = 12;
43
- function looksLikeTopic(title) {
44
- if (POLICY_STATEMENT_OPENERS.test(title.trim()))
45
- return false;
46
- if (title.trim().split(/\s+/).length > MAX_TOPIC_WORDS)
47
- return false;
48
- return true;
45
+ const BASE_VERBS = new Set([
46
+ 'add', 'adopt', 'allow', 'avoid', 'change', 'close', 'consolidate', 'delegate', 'deprecate',
47
+ 'disable', 'drop', 'enable', 'enforce', 'ensure', 'extract', 'fix', 'handle', 'ignore', 'implement',
48
+ 'introduce', 'keep', 'make', 'migrate', 'move', 'prefer', 'prevent', 'put', 'refactor',
49
+ 'reject', 'remove', 'rename', 'replace', 'require', 'restrict', 'reuse', 'send',
50
+ 'simplify', 'skip', 'split', 'stop', 'switch', 'throw', 'treat', 'update',
51
+ 'upgrade', 'use', 'validate', 'warn',
52
+ ]);
53
+ /** The quotable phrase inside a commit subject or decision title, or undefined. */
54
+ function askablePhrase(title) {
55
+ if (!title)
56
+ return undefined;
57
+ const phrase = title.trim().replace(CONVENTIONAL_PREFIX, '').replace(PR_REFERENCE, '').trim().replace(/\.$/, '');
58
+ if (phrase === '' || /[:?;#]/.test(phrase))
59
+ return undefined;
60
+ const words = phrase.split(/\s+/);
61
+ if (words.length > MAX_TOPIC_WORDS)
62
+ return undefined;
63
+ const verb = words[0].toLowerCase();
64
+ if (!BASE_VERBS.has(verb))
65
+ return undefined;
66
+ return verb + phrase.slice(words[0].length);
67
+ }
68
+ /** The first title in the list that makes a grammatical question, as it was given. */
69
+ export function pickAskableTitle(titles) {
70
+ return titles.find((t) => askablePhrase(t) !== undefined);
49
71
  }
50
72
  /**
51
73
  * The question to hand the agent. Against a REAL decision the wizard found when there is one,
@@ -55,9 +77,8 @@ function looksLikeTopic(title) {
55
77
  * list tool rather than a similarity search.
56
78
  */
57
79
  export function firstQuestion(firstTitle) {
58
- return firstTitle && looksLikeTopic(firstTitle)
59
- ? `why did we ${firstTitle}?`
60
- : 'what decisions exist in this codebase?';
80
+ const phrase = askablePhrase(firstTitle);
81
+ return phrase ? `why did we ${phrase}?` : 'what decisions exist in this codebase?';
61
82
  }
62
83
  /** Runnable AS PRINTED: bare `align mcp --setup` wires the cloud default, which is not a local user's graph. */
63
84
  function mcpSetupCommand(envName) {
@@ -1 +1 @@
1
- {"version":3,"file":"next-step.js","sourceRoot":"","sources":["../../src/lib/next-step.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"next-step.js","sourceRoot":"","sources":["../../src/lib/next-step.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * What to tell a person on a Node older than the floor, or null when the runtime is fine.
3
+ * Pure, and it must stay free of imports that reach node:sqlite: it runs before the CLI loads.
4
+ */
5
+ export declare function nodeFloorMessage(version: string): string | null;
6
+ //# sourceMappingURL=node-floor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-floor.d.ts","sourceRoot":"","sources":["../../src/lib/node-floor.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO/D"}
@@ -0,0 +1,15 @@
1
+ import { MIN_NODE, nodeMeetsFloor } from './doctor.js';
2
+ /**
3
+ * What to tell a person on a Node older than the floor, or null when the runtime is fine.
4
+ * Pure, and it must stay free of imports that reach node:sqlite: it runs before the CLI loads.
5
+ */
6
+ export function nodeFloorMessage(version) {
7
+ if (nodeMeetsFloor(version))
8
+ return null;
9
+ return [
10
+ `align needs Node ${MIN_NODE} or newer, and this is Node ${version}.`,
11
+ 'Install a current Node from https://nodejs.org, open a new terminal, and run align again.',
12
+ 'No Node to hand? The binary needs none: curl -fsSL https://align.tech/install.sh | sh',
13
+ ].join('\n');
14
+ }
15
+ //# sourceMappingURL=node-floor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-floor.js","sourceRoot":"","sources":["../../src/lib/node-floor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEvD;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,IAAI,cAAc,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,OAAO;QACL,oBAAoB,QAAQ,+BAA+B,OAAO,GAAG;QACrE,2FAA2F;QAC3F,uFAAuF;KACxF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
@@ -1,4 +1,4 @@
1
- import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
1
+ import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  export declare const BACKUP_SUFFIX = ".align-backup";
3
3
  export type SafeWriteStatus = 'written' | 'unchanged' | 'declined' | 'symlink';
4
4
  /** The fs calls the writer makes. A parameter so a test can change the file mid-write. */
@@ -13,6 +13,7 @@ export interface SafeFs {
13
13
  unlinkSync: typeof unlinkSync;
14
14
  mkdirSync: typeof mkdirSync;
15
15
  readdirSync: typeof readdirSync;
16
+ rmdirSync: typeof rmdirSync;
16
17
  }
17
18
  type Json = Record<string, unknown>;
18
19
  /** One value or array element align put in a JSON config. Hashes only, never the value. */
@@ -48,6 +49,11 @@ export interface WrittenConfig {
48
49
  /** 'made': align made the backup. 'foreign': one was already there and is not trusted. 'none': no original to keep. */
49
50
  backup: 'made' | 'foreign' | 'none';
50
51
  backupSha256?: string;
52
+ /**
53
+ * Where the backup lives when it is not beside the file (project files). Snapshots are
54
+ * `<backupAt>.<n>`. Absent: `<file>.align-backup`.
55
+ */
56
+ backupAt?: string;
51
57
  owned?: OwnedItem[];
52
58
  /**
53
59
  * Extra copies of the file taken just before a write that found it changed by someone else
@@ -57,6 +63,11 @@ export interface WrittenConfig {
57
63
  n: number;
58
64
  sha256: string;
59
65
  }>;
66
+ /**
67
+ * Directories align's first write made on the way to this file (outermost first). Undo removes
68
+ * each one only if it is empty by then, so a directory that held anything else stays.
69
+ */
70
+ createdDirs?: string[];
60
71
  /** Text files: the managed block align wrote. */
61
72
  block?: {
62
73
  start: string;
@@ -98,12 +109,24 @@ export interface SafeWriteOptions {
98
109
  };
99
110
  /** TOML only: the table (e.g. ['mcp_servers', 'align-local']) the managed block holds. */
100
111
  tomlTable?: string[];
112
+ /**
113
+ * A file in the user's repo, not in an agent's private config: new files and directories get the
114
+ * default mode (the umask decides) instead of owner-only, since a team shares them.
115
+ */
116
+ projectFile?: boolean;
101
117
  /** The tail of the invalid-JSON message: what to do about it. */
102
118
  invalidJsonAdvice?: string;
103
119
  }
104
120
  export declare class SafeWriteConflictError extends Error {
105
121
  constructor(file: string);
106
122
  }
123
+ /**
124
+ * Where a PROJECT file's backup goes: `<state home>/align-cli/undo-backups/<sha of the full path>/<name>.align-backup`.
125
+ * Out of the repo on purpose, so nothing shows in `git status`. Keyed on the full path, the same key
126
+ * the ledger uses, so a moved repo loses both together and a same-named file elsewhere never collides.
127
+ * XDG_STATE_HOME is honoured; %LOCALAPPDATA% on Windows (case-folded there, as its paths are).
128
+ */
129
+ export declare function projectBackupBase(file: string): string;
107
130
  /** The text form. `compute` gets the current text (null when missing) and returns the new text, or undefined to leave it. */
108
131
  export declare function safeWriteText(file: string, compute: (current: string | null) => string | undefined, opts?: SafeWriteOptions): SafeWriteStatus;
109
132
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"safe-config-write.d.ts","sourceRoot":"","sources":["../../src/lib/safe-config-write.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"safe-config-write.d.ts","sourceRoot":"","sources":["../../src/lib/safe-config-write.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,15 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
- import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
2
+ import { chmodSync, copyFileSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmdirSync, unlinkSync, writeFileSync, } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { parse as parseToml } from 'smol-toml';
6
+ import { stateHomeFor } from './backfill-state.js';
6
7
  /*
7
8
  * The one way align writes to a config file that belongs to someone else (an agent's MCP
8
9
  * config, its hook file). Promises, each of which a plain writeFileSync breaks:
9
- * 1. The FIRST content is kept in `<file>.align-backup` and never overwritten.
10
+ * 1. The FIRST content is kept in `<file>.align-backup` and never overwritten. A PROJECT file
11
+ * (a committed AGENTS.md) is the exception: its copy goes under the user's state directory, so
12
+ * `git status` shows nothing of it and `git add -A` cannot commit it.
10
13
  * 2. The file is re-read after our new content is staged. If another program changed it in
11
14
  * the meantime we recompute once from what is there now, and give up if it moves again.
12
15
  * 3. The new content lands by an exclusive temp file + rename, keeping the old file's mode.
@@ -18,7 +21,7 @@ import { parse as parseToml } from 'smol-toml';
18
21
  export const BACKUP_SUFFIX = '.align-backup';
19
22
  // Built on use, not at import: a test that mocks node:fs with a few names must still be able
20
23
  // to import anything that reaches this module.
21
- const realFs = () => ({ readFileSync, writeFileSync, renameSync, copyFileSync, lstatSync, readlinkSync, chmodSync, unlinkSync, mkdirSync, readdirSync });
24
+ const realFs = () => ({ readFileSync, writeFileSync, renameSync, copyFileSync, lstatSync, readlinkSync, chmodSync, unlinkSync, mkdirSync, readdirSync, rmdirSync });
22
25
  const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
23
26
  const sha = (data) => createHash('sha256').update(data).digest('hex');
24
27
  /** JSON with sorted keys, so a hash does not depend on key order. */
@@ -59,7 +62,9 @@ export function mergeWrittenConfig(prev, next) {
59
62
  firstSha256: prev.firstSha256,
60
63
  backup: prev.backup,
61
64
  ...(prev.backupSha256 ? { backupSha256: prev.backupSha256 } : {}),
65
+ ...(prev.backupAt ? { backupAt: prev.backupAt } : {}),
62
66
  ...(owned.size > 0 ? { owned: [...owned.values()] } : {}),
67
+ ...((prev.createdDirs?.length || next.createdDirs?.length) ? { createdDirs: [...new Set([...(prev.createdDirs ?? []), ...(next.createdDirs ?? [])])] } : {}),
63
68
  ...((prev.snapshots?.length || next.snapshots?.length) ? { snapshots: [...(prev.snapshots ?? []), ...(next.snapshots ?? [])] } : {}),
64
69
  ...(next.block
65
70
  ? { block: { ...next.block, ...(prev.block ? (prev.block.replaced ? { replaced: true } : {}) : next.block.replaced ? { replaced: true } : {}) } }
@@ -141,12 +146,15 @@ function modeOf(fs, file) {
141
146
  }
142
147
  }
143
148
  /** 'made' only when this call created the backup. Anything already at that path is not ours. */
144
- function backupOnce(fs, file) {
145
- const backup = `${file}${BACKUP_SUFFIX}`;
149
+ function backupOnce(fs, file, backup, private_) {
146
150
  if (linkAt(fs, backup) !== 'missing')
147
151
  return { state: 'foreign' };
152
+ if (private_)
153
+ fs.mkdirSync(path.dirname(backup), { recursive: true, mode: 0o700 });
148
154
  try {
149
155
  fs.copyFileSync(file, backup, 1); // COPYFILE_EXCL
156
+ if (private_)
157
+ fs.chmodSync(backup, 0o600);
150
158
  }
151
159
  catch (err) {
152
160
  if (err.code === 'EEXIST')
@@ -160,9 +168,11 @@ function backupOnce(fs, file) {
160
168
  * last write: the backup from before align's first write cannot hold what they added since.
161
169
  * `<file>.align-backup.<n>`, exclusive, 0600 (it may hold secrets), never overwritten.
162
170
  */
163
- function takeSnapshot(fs, file, content, taken) {
171
+ function takeSnapshot(fs, file, base, content, taken) {
172
+ if (base !== `${file}${BACKUP_SUFFIX}`)
173
+ fs.mkdirSync(path.dirname(base), { recursive: true, mode: 0o700 });
164
174
  for (let n = Math.max(0, ...taken) + 1; n <= MAX_SNAPSHOTS; n++) {
165
- const target = `${file}${BACKUP_SUFFIX}.${n}`;
175
+ const target = `${base}.${n}`;
166
176
  if (linkAt(fs, target) !== 'missing')
167
177
  continue;
168
178
  try {
@@ -178,9 +188,9 @@ function takeSnapshot(fs, file, content, taken) {
178
188
  throw new Error(`${file} already has ${taken.length} snapshots from align. Run \`align use --undo\` to clear them, then try again.`);
179
189
  }
180
190
  /** Delete snapshots THIS call made (verified: a regular file whose hash is the one taken). */
181
- function dropSnapshots(fs, file, list) {
191
+ function dropSnapshots(fs, base, list) {
182
192
  for (const { n, sha256: want } of list) {
183
- const target = `${file}${BACKUP_SUFFIX}.${n}`;
193
+ const target = `${base}.${n}`;
184
194
  try {
185
195
  if (fs.lstatSync(target).isFile() && sha(fs.readFileSync(target)) === want)
186
196
  fs.unlinkSync(target);
@@ -190,6 +200,17 @@ function dropSnapshots(fs, file, list) {
190
200
  }
191
201
  }
192
202
  }
203
+ /**
204
+ * Where a PROJECT file's backup goes: `<state home>/align-cli/undo-backups/<sha of the full path>/<name>.align-backup`.
205
+ * Out of the repo on purpose, so nothing shows in `git status`. Keyed on the full path, the same key
206
+ * the ledger uses, so a moved repo loses both together and a same-named file elsewhere never collides.
207
+ * XDG_STATE_HOME is honoured; %LOCALAPPDATA% on Windows (case-folded there, as its paths are).
208
+ */
209
+ export function projectBackupBase(file) {
210
+ const abs = path.resolve(file);
211
+ const key = sha(process.platform === 'win32' ? abs.toLowerCase() : abs).slice(0, 24);
212
+ return path.join(stateHomeFor(process.env, os.homedir(), process.platform), 'align-cli', 'undo-backups', key, `${path.basename(file)}${BACKUP_SUFFIX}`);
213
+ }
193
214
  const MAX_SNAPSHOTS = 200;
194
215
  const STALE_TMP_MS = 60_000;
195
216
  /** A crash between staging and rename leaves `.<name>.<hex>.align-tmp`. Old ones are removed. */
@@ -211,12 +232,12 @@ function sweepStaleTemps(fs, file) {
211
232
  }
212
233
  }
213
234
  /** Stage `text` beside `file` and rename it over. Never leaves the temp file behind on failure. */
214
- function stageAndRename(fs, file, text, mode, beforeRename) {
235
+ function stageAndRename(fs, file, text, mode, beforeRename, newMode = 0o600) {
215
236
  let tmp = '';
216
237
  for (let i = 0; i < 5; i++) {
217
238
  tmp = path.join(path.dirname(file), `.${path.basename(file)}.${randomBytes(4).toString('hex')}.align-tmp`);
218
239
  try {
219
- fs.writeFileSync(tmp, text, { encoding: 'utf8', mode: mode ?? 0o600, flag: 'wx' });
240
+ fs.writeFileSync(tmp, text, { encoding: 'utf8', mode: mode ?? newMode, flag: 'wx' });
220
241
  break;
221
242
  }
222
243
  catch (err) {
@@ -360,6 +381,22 @@ function blockOf(before, next, markers, tomlTable) {
360
381
  }
361
382
  return { start: markers.start, end: markers.end, sha256: sha(region), ...(old !== null && old !== region ? { replaced: true } : {}), ...(table ? { table } : {}) };
362
383
  }
384
+ /** The directories on the way to `dir` that do not exist yet, outermost first. */
385
+ function missingDirs(fs, dir) {
386
+ const missing = [];
387
+ for (let d = dir;; d = path.dirname(d)) {
388
+ try {
389
+ fs.lstatSync(d);
390
+ break;
391
+ }
392
+ catch {
393
+ missing.unshift(d);
394
+ }
395
+ if (path.dirname(d) === d)
396
+ break;
397
+ }
398
+ return missing;
399
+ }
363
400
  function safeWrite(file, compute, opts, describe) {
364
401
  const fs = opts.fs ?? realFs();
365
402
  const note = opts.note ?? ((l) => console.error(l));
@@ -371,7 +408,11 @@ function safeWrite(file, compute, opts, describe) {
371
408
  return 'symlink';
372
409
  }
373
410
  let backup = { state: 'none' };
411
+ // A file already recorded keeps the place its first backup went; a new project file gets a place outside the repo.
412
+ const backupAt = lookup?.(file)?.backupAt ?? (opts.projectFile ? projectBackupBase(file) : undefined);
413
+ const backupBase = backupAt ?? `${file}${BACKUP_SUFFIX}`;
374
414
  const snapshots = [];
415
+ let createdDirs = [];
375
416
  for (let attempt = 0; attempt < 2; attempt++) {
376
417
  const before = readCurrent(fs, file);
377
418
  const next = compute(before);
@@ -379,7 +420,10 @@ function safeWrite(file, compute, opts, describe) {
379
420
  return 'declined';
380
421
  if (next === before)
381
422
  return 'unchanged';
382
- fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
423
+ const madeDirs = missingDirs(fs, path.dirname(file));
424
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: opts.projectFile ? 0o777 : 0o700 });
425
+ if (madeDirs.length > 0)
426
+ createdDirs = madeDirs;
383
427
  sweepStaleTemps(fs, file);
384
428
  const mode = modeOf(fs, file);
385
429
  // The first time align writes a file that exists, the original is kept. After that, if the file
@@ -389,18 +433,18 @@ function safeWrite(file, compute, opts, describe) {
389
433
  if (before !== null) {
390
434
  if (!prev) {
391
435
  if (backup.state === 'none')
392
- backup = backupOnce(fs, file);
436
+ backup = backupOnce(fs, file, backupBase, backupAt !== undefined);
393
437
  }
394
438
  else if (sha(before) !== prev.sha256 && !snapshots.some((x) => x.sha256 === sha(before))) {
395
- snapshots.push(takeSnapshot(fs, file, before, [...(prev.snapshots ?? []).map((x) => x.n), ...snapshots.map((x) => x.n)]));
439
+ snapshots.push(takeSnapshot(fs, file, backupBase, before, [...(prev.snapshots ?? []).map((x) => x.n), ...snapshots.map((x) => x.n)]));
396
440
  }
397
441
  }
398
442
  let staged = false;
399
443
  try {
400
- staged = stageAndRename(fs, file, next, mode, () => readCurrent(fs, file) === before);
444
+ staged = stageAndRename(fs, file, next, mode, () => readCurrent(fs, file) === before, opts.projectFile ? 0o666 : 0o600);
401
445
  }
402
446
  catch (err) {
403
- dropSnapshots(fs, file, snapshots);
447
+ dropSnapshots(fs, backupBase, snapshots);
404
448
  throw err;
405
449
  }
406
450
  if (!staged)
@@ -423,7 +467,7 @@ function safeWrite(file, compute, opts, describe) {
423
467
  ...(block?.replaced ? [block.snapshot ?? 0] : []),
424
468
  ]);
425
469
  const kept = snapshots.filter((x) => referenced.has(x.n));
426
- dropSnapshots(fs, file, snapshots.filter((x) => !referenced.has(x.n)));
470
+ dropSnapshots(fs, backupBase, snapshots.filter((x) => !referenced.has(x.n)));
427
471
  try {
428
472
  recorder?.(file, {
429
473
  created: before === null,
@@ -431,7 +475,9 @@ function safeWrite(file, compute, opts, describe) {
431
475
  firstSha256: sha(next),
432
476
  backup: backup.state,
433
477
  ...(backup.sha256 ? { backupSha256: backup.sha256 } : {}),
478
+ ...(backupAt ? { backupAt } : {}),
434
479
  ...(kept.length > 0 ? { snapshots: kept } : {}),
480
+ ...(createdDirs.length > 0 ? { createdDirs } : {}),
435
481
  ...owned,
436
482
  ...(block ? { block } : {}),
437
483
  });
@@ -442,7 +488,7 @@ function safeWrite(file, compute, opts, describe) {
442
488
  }
443
489
  return 'written';
444
490
  }
445
- dropSnapshots(fs, file, snapshots);
491
+ dropSnapshots(fs, backupBase, snapshots);
446
492
  throw new SafeWriteConflictError(file);
447
493
  }
448
494
  /** The text form. `compute` gets the current text (null when missing) and returns the new text, or undefined to leave it. */
@@ -492,12 +538,20 @@ export function ownedOf(before, after, prev = [], snapshot = 0) {
492
538
  return !ownsElement(here, h);
493
539
  });
494
540
  };
541
+ /**
542
+ * A container that is already in the file but that align's own earlier write made: its
543
+ * createdDepth, so an entry added to it now is still pruned with it. null when it was the user's.
544
+ */
545
+ const madeByAlign = (here) => {
546
+ const depths = prev.filter((i) => i.path.length > here.length && samePath(i.path.slice(0, here.length), here) && i.createdDepth <= here.length).map((i) => i.createdDepth);
547
+ return depths.length > 0 ? Math.min(...depths) : null;
548
+ };
495
549
  const walk = (b, a, p, created) => {
496
550
  for (const [k, av] of Object.entries(a)) {
497
551
  const here = [...p, k];
498
552
  const has = b !== undefined && Object.prototype.hasOwnProperty.call(b, k);
499
553
  const bv = has ? b[k] : undefined;
500
- const createdAt = created ?? (has ? null : here.length);
554
+ const createdAt = created ?? (has ? madeByAlign(here) : here.length);
501
555
  if (has && shaOf(av) === shaOf(bv))
502
556
  continue;
503
557
  if (isObject(av) && Object.keys(av).length > 0 && here.length < 2 && (!has || isObject(bv))) {
@@ -662,17 +716,13 @@ function removeOwned(cur, owned, originalOf, backupPath) {
662
716
  export function undoWrittenConfigs(manifest, fs = realFs()) {
663
717
  const report = { restored: [], removed: [], cleaned: [], skipped: [], done: [] };
664
718
  for (const [file, entry] of Object.entries(manifest)) {
665
- const backupPath = `${file}${BACKUP_SUFFIX}`;
719
+ const backupPath = entry.backupAt ?? `${file}${BACKUP_SUFFIX}`;
666
720
  const link = linkAt(fs, file);
667
721
  if (link && link !== 'missing') {
668
722
  report.skipped.push(`${file}: it is a symlink to ${link.target}, left alone`);
669
723
  continue;
670
724
  }
671
725
  const cur = readCurrent(fs, file);
672
- if (cur === null) {
673
- report.done.push(file); // already gone: nothing of align's is left
674
- continue;
675
- }
676
726
  // Compare-and-swap: the agent may rewrite its own file (Gemini saves settings) between the read
677
727
  // above and our write. Re-read right before every destructive step; if it moved, touch nothing.
678
728
  const moved = () => {
@@ -704,6 +754,17 @@ export function undoWrittenConfigs(manifest, fs = realFs()) {
704
754
  }
705
755
  };
706
756
  const backupTrusted = () => snapText(0) !== null;
757
+ /** Directories align made for this file, innermost first, each only while empty. */
758
+ const dropDirs = () => {
759
+ for (const d of [...(entry.createdDirs ?? [])].reverse()) {
760
+ try {
761
+ fs.rmdirSync(d);
762
+ }
763
+ catch {
764
+ break; /* not empty (or gone): the outer ones hold it too */
765
+ }
766
+ }
767
+ };
707
768
  /** Only after a fully clean undo, and only copies that are still align's own. */
708
769
  const dropBackup = () => {
709
770
  for (const n of [0, ...(entry.snapshots ?? []).map((x) => x.n)]) {
@@ -713,13 +774,29 @@ export function undoWrittenConfigs(manifest, fs = realFs()) {
713
774
  }
714
775
  catch { /* gone */ }
715
776
  }
777
+ // The directory holding an out-of-repo copy: removed only while empty.
778
+ if (entry.backupAt) {
779
+ try {
780
+ fs.rmdirSync(path.dirname(entry.backupAt));
781
+ }
782
+ catch { /* not empty, or gone */ }
783
+ }
716
784
  };
785
+ if (cur === null) {
786
+ dropBackup(); // already gone: nothing of align's is left, and its saved copy must not linger as a stale 'foreign' one
787
+ report.done.push(file);
788
+ continue;
789
+ }
717
790
  // Whole-file only when align wrote this file exactly once and nobody has touched it since.
718
- if (sha(cur) === entry.sha256 && entry.firstSha256 === entry.sha256) {
791
+ // A file align wrote WHOLE (no block, no owned keys: a generated plugin or rule) and created is all
792
+ // align's even after a re-run with other content, as long as it still equals the last write.
793
+ const wholeAlign = entry.created && !entry.block && !(entry.owned && entry.owned.length > 0);
794
+ if (sha(cur) === entry.sha256 && (entry.firstSha256 === entry.sha256 || wholeAlign)) {
719
795
  if (entry.created) {
720
796
  if (moved())
721
797
  continue;
722
798
  fs.unlinkSync(file);
799
+ dropDirs();
723
800
  report.removed.push(file);
724
801
  report.done.push(file);
725
802
  continue;
@@ -741,6 +818,14 @@ export function undoWrittenConfigs(manifest, fs = realFs()) {
741
818
  continue;
742
819
  }
743
820
  }
821
+ // The file is exactly what align wrote, so only the original is missing. A copy kept outside the repo
822
+ // can go missing (a cleaned state directory, a moved repo): say so and leave the file, rather than
823
+ // guess at what to take out. The record stays, so a restored copy lets undo run again.
824
+ const needsOriginal = !!entry.block?.replaced || (entry.owned ?? []).some((i) => i.replaced || i.removed) || (!entry.block && !(entry.owned && entry.owned.length > 0));
825
+ if (entry.backupAt && !entry.created && entry.backup === 'made' && !backupTrusted() && needsOriginal && sha(cur) === entry.sha256 && entry.firstSha256 === entry.sha256) {
826
+ report.skipped.push(`${file}: align's saved copy of the original (${backupPath}) is missing or changed, so align left the file as it is. Take align's lines out by hand`);
827
+ continue;
828
+ }
744
829
  // Surgical: only align's own entries, and the user's own values back where align overwrote them.
745
830
  const mention = entry.backup === 'foreign' ? `; ${backupPath} was not made by align, so it was not used` : '';
746
831
  let next;
@@ -834,11 +919,12 @@ export function undoWrittenConfigs(manifest, fs = realFs()) {
834
919
  }
835
920
  // A file align created that holds nothing but align's own content once that is out: remove it, as
836
921
  // an untouched one is.
837
- if (entry.created && entry.owned && !entry.block && next.trim() === '{}') {
922
+ if (entry.created && ((entry.owned && !entry.block && next.trim() === '{}') || (entry.block && next.trim() === ''))) {
838
923
  if (moved())
839
924
  continue;
840
925
  fs.unlinkSync(file);
841
926
  dropBackup();
927
+ dropDirs();
842
928
  report.removed.push(file);
843
929
  report.done.push(file);
844
930
  continue;
@@ -846,6 +932,7 @@ export function undoWrittenConfigs(manifest, fs = realFs()) {
846
932
  if (next !== cur && !stageAndRename(fs, file, next, modeOf(fs, file), () => !moved()))
847
933
  continue;
848
934
  dropBackup();
935
+ dropDirs();
849
936
  report.cleaned.push(file);
850
937
  report.done.push(file);
851
938
  }