@herbertgao/pi-extensions 2026.8.9 → 2026.8.11

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 (194) hide show
  1. package/README.md +4 -2
  2. package/node_modules/@herbertgao/resume-from/LICENSE +21 -0
  3. package/node_modules/@herbertgao/resume-from/README.md +163 -0
  4. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/adapter.d.ts +25 -0
  5. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/adapter.js +60 -0
  6. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/contract.d.ts +8 -0
  7. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/contract.js +4 -0
  8. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/entries.d.ts +30 -0
  9. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/entries.js +107 -0
  10. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/index.d.ts +3 -0
  11. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/index.js +2 -0
  12. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/layout.d.ts +29 -0
  13. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/layout.js +80 -0
  14. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/readback.d.ts +9 -0
  15. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/readback.js +39 -0
  16. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/reader.d.ts +37 -0
  17. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/reader.js +424 -0
  18. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/redaction.d.ts +26 -0
  19. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/redaction.js +132 -0
  20. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/validation.d.ts +4 -0
  21. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/validation.js +39 -0
  22. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/writer.d.ts +23 -0
  23. package/node_modules/@herbertgao/resume-from/dist/adapters/claude-code/writer.js +306 -0
  24. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/contract.d.ts +15 -0
  25. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/contract.js +4 -0
  26. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/index.d.ts +6 -0
  27. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/index.js +44 -0
  28. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/read.d.ts +28 -0
  29. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/read.js +270 -0
  30. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/readback.d.ts +9 -0
  31. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/readback.js +47 -0
  32. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/redaction.d.ts +26 -0
  33. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/redaction.js +132 -0
  34. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/rollout.d.ts +75 -0
  35. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/rollout.js +185 -0
  36. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/validation.d.ts +7 -0
  37. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/validation.js +78 -0
  38. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/write.d.ts +17 -0
  39. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/write.js +141 -0
  40. package/node_modules/@herbertgao/resume-from/dist/adapters/contract.d.ts +74 -0
  41. package/node_modules/@herbertgao/resume-from/dist/adapters/contract.js +4 -0
  42. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/adapter.d.ts +15 -0
  43. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/adapter.js +209 -0
  44. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/contract.d.ts +24 -0
  45. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/contract.js +4 -0
  46. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/format.d.ts +162 -0
  47. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/format.js +223 -0
  48. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/index.d.ts +7 -0
  49. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/index.js +10 -0
  50. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/parse.d.ts +41 -0
  51. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/parse.js +371 -0
  52. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/redaction.d.ts +26 -0
  53. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/redaction.js +132 -0
  54. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/serialize.d.ts +22 -0
  55. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/serialize.js +87 -0
  56. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/validate.d.ts +14 -0
  57. package/node_modules/@herbertgao/resume-from/dist/adapters/pi/validate.js +134 -0
  58. package/node_modules/@herbertgao/resume-from/dist/bin.d.ts +35 -0
  59. package/node_modules/@herbertgao/resume-from/dist/bin.js +135 -0
  60. package/node_modules/@herbertgao/resume-from/dist/contract.d.ts +10 -0
  61. package/node_modules/@herbertgao/resume-from/dist/contract.js +4 -0
  62. package/node_modules/@herbertgao/resume-from/dist/host/agents.d.ts +29 -0
  63. package/node_modules/@herbertgao/resume-from/dist/host/agents.js +20 -0
  64. package/node_modules/@herbertgao/resume-from/dist/host/cli/args.d.ts +21 -0
  65. package/node_modules/@herbertgao/resume-from/dist/host/cli/args.js +187 -0
  66. package/node_modules/@herbertgao/resume-from/dist/host/cli/contract.d.ts +27 -0
  67. package/node_modules/@herbertgao/resume-from/dist/host/cli/contract.js +4 -0
  68. package/node_modules/@herbertgao/resume-from/dist/host/cli/index.d.ts +2 -0
  69. package/node_modules/@herbertgao/resume-from/dist/host/cli/index.js +1 -0
  70. package/node_modules/@herbertgao/resume-from/dist/host/cli/presentation.d.ts +2 -0
  71. package/node_modules/@herbertgao/resume-from/dist/host/cli/presentation.js +11 -0
  72. package/node_modules/@herbertgao/resume-from/dist/host/cli/render.d.ts +3 -0
  73. package/node_modules/@herbertgao/resume-from/dist/host/cli/render.js +70 -0
  74. package/node_modules/@herbertgao/resume-from/dist/host/cli/runner.d.ts +8 -0
  75. package/node_modules/@herbertgao/resume-from/dist/host/cli/runner.js +125 -0
  76. package/node_modules/@herbertgao/resume-from/dist/host/contract.d.ts +42 -0
  77. package/node_modules/@herbertgao/resume-from/dist/host/contract.js +4 -0
  78. package/node_modules/@herbertgao/resume-from/dist/host/entry.d.ts +52 -0
  79. package/node_modules/@herbertgao/resume-from/dist/host/entry.js +91 -0
  80. package/node_modules/@herbertgao/resume-from/dist/host/index.d.ts +11 -0
  81. package/node_modules/@herbertgao/resume-from/dist/host/index.js +10 -0
  82. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/command.d.ts +18 -0
  83. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/command.js +103 -0
  84. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/contract.d.ts +34 -0
  85. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/contract.js +4 -0
  86. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/index.d.ts +6 -0
  87. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/index.js +4 -0
  88. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/picker.d.ts +16 -0
  89. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/picker.js +35 -0
  90. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/presentation.d.ts +2 -0
  91. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/presentation.js +11 -0
  92. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/register.d.ts +25 -0
  93. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/register.js +11 -0
  94. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/ui.d.ts +20 -0
  95. package/node_modules/@herbertgao/resume-from/dist/host/pi-extension/ui.js +1 -0
  96. package/node_modules/@herbertgao/resume-from/dist/host/profile.d.ts +8 -0
  97. package/node_modules/@herbertgao/resume-from/dist/host/profile.js +31 -0
  98. package/node_modules/@herbertgao/resume-from/dist/host/registry.d.ts +8 -0
  99. package/node_modules/@herbertgao/resume-from/dist/host/registry.js +33 -0
  100. package/node_modules/@herbertgao/resume-from/dist/host/wiring.d.ts +41 -0
  101. package/node_modules/@herbertgao/resume-from/dist/host/wiring.js +67 -0
  102. package/node_modules/@herbertgao/resume-from/dist/import/confirmation.d.ts +6 -0
  103. package/node_modules/@herbertgao/resume-from/dist/import/confirmation.js +27 -0
  104. package/node_modules/@herbertgao/resume-from/dist/import/contract.d.ts +41 -0
  105. package/node_modules/@herbertgao/resume-from/dist/import/contract.js +4 -0
  106. package/node_modules/@herbertgao/resume-from/dist/import/discovery/contract.d.ts +58 -0
  107. package/node_modules/@herbertgao/resume-from/dist/import/discovery/contract.js +4 -0
  108. package/node_modules/@herbertgao/resume-from/dist/import/discovery/errors.d.ts +9 -0
  109. package/node_modules/@herbertgao/resume-from/dist/import/discovery/errors.js +12 -0
  110. package/node_modules/@herbertgao/resume-from/dist/import/discovery/finder.d.ts +10 -0
  111. package/node_modules/@herbertgao/resume-from/dist/import/discovery/finder.js +129 -0
  112. package/node_modules/@herbertgao/resume-from/dist/import/discovery/homes.d.ts +23 -0
  113. package/node_modules/@herbertgao/resume-from/dist/import/discovery/homes.js +83 -0
  114. package/node_modules/@herbertgao/resume-from/dist/import/discovery/index.d.ts +2 -0
  115. package/node_modules/@herbertgao/resume-from/dist/import/discovery/index.js +2 -0
  116. package/node_modules/@herbertgao/resume-from/dist/import/discovery/ordering.d.ts +3 -0
  117. package/node_modules/@herbertgao/resume-from/dist/import/discovery/ordering.js +21 -0
  118. package/node_modules/@herbertgao/resume-from/dist/import/errors.d.ts +22 -0
  119. package/node_modules/@herbertgao/resume-from/dist/import/errors.js +20 -0
  120. package/node_modules/@herbertgao/resume-from/dist/import/index.d.ts +3 -0
  121. package/node_modules/@herbertgao/resume-from/dist/import/index.js +5 -0
  122. package/node_modules/@herbertgao/resume-from/dist/import/landing/contract.d.ts +44 -0
  123. package/node_modules/@herbertgao/resume-from/dist/import/landing/contract.js +4 -0
  124. package/node_modules/@herbertgao/resume-from/dist/import/landing/errors.d.ts +15 -0
  125. package/node_modules/@herbertgao/resume-from/dist/import/landing/errors.js +15 -0
  126. package/node_modules/@herbertgao/resume-from/dist/import/landing/handover.d.ts +5 -0
  127. package/node_modules/@herbertgao/resume-from/dist/import/landing/handover.js +12 -0
  128. package/node_modules/@herbertgao/resume-from/dist/import/landing/index.d.ts +4 -0
  129. package/node_modules/@herbertgao/resume-from/dist/import/landing/index.js +4 -0
  130. package/node_modules/@herbertgao/resume-from/dist/import/landing/lander.d.ts +7 -0
  131. package/node_modules/@herbertgao/resume-from/dist/import/landing/lander.js +133 -0
  132. package/node_modules/@herbertgao/resume-from/dist/import/landing/marker.d.ts +9 -0
  133. package/node_modules/@herbertgao/resume-from/dist/import/landing/marker.js +46 -0
  134. package/node_modules/@herbertgao/resume-from/dist/import/pipeline.d.ts +17 -0
  135. package/node_modules/@herbertgao/resume-from/dist/import/pipeline.js +134 -0
  136. package/node_modules/@herbertgao/resume-from/dist/import/preview/builder.d.ts +6 -0
  137. package/node_modules/@herbertgao/resume-from/dist/import/preview/builder.js +76 -0
  138. package/node_modules/@herbertgao/resume-from/dist/import/preview/contract.d.ts +39 -0
  139. package/node_modules/@herbertgao/resume-from/dist/import/preview/contract.js +4 -0
  140. package/node_modules/@herbertgao/resume-from/dist/import/preview/format.d.ts +19 -0
  141. package/node_modules/@herbertgao/resume-from/dist/import/preview/format.js +45 -0
  142. package/node_modules/@herbertgao/resume-from/dist/import/preview/index.d.ts +2 -0
  143. package/node_modules/@herbertgao/resume-from/dist/import/preview/index.js +1 -0
  144. package/node_modules/@herbertgao/resume-from/dist/import/preview/warnings.d.ts +11 -0
  145. package/node_modules/@herbertgao/resume-from/dist/import/preview/warnings.js +80 -0
  146. package/node_modules/@herbertgao/resume-from/dist/import/transfer/contract.d.ts +48 -0
  147. package/node_modules/@herbertgao/resume-from/dist/import/transfer/contract.js +4 -0
  148. package/node_modules/@herbertgao/resume-from/dist/import/transfer/index.d.ts +2 -0
  149. package/node_modules/@herbertgao/resume-from/dist/import/transfer/index.js +1 -0
  150. package/node_modules/@herbertgao/resume-from/dist/import/transfer/rules.d.ts +3 -0
  151. package/node_modules/@herbertgao/resume-from/dist/import/transfer/rules.js +228 -0
  152. package/node_modules/@herbertgao/resume-from/dist/import/wiring.d.ts +24 -0
  153. package/node_modules/@herbertgao/resume-from/dist/import/wiring.js +27 -0
  154. package/node_modules/@herbertgao/resume-from/dist/index.d.ts +36 -0
  155. package/node_modules/@herbertgao/resume-from/dist/index.js +36 -0
  156. package/node_modules/@herbertgao/resume-from/dist/platform/config/contract.d.ts +35 -0
  157. package/node_modules/@herbertgao/resume-from/dist/platform/config/contract.js +4 -0
  158. package/node_modules/@herbertgao/resume-from/dist/platform/config/defaults.d.ts +16 -0
  159. package/node_modules/@herbertgao/resume-from/dist/platform/config/defaults.js +22 -0
  160. package/node_modules/@herbertgao/resume-from/dist/platform/config/errors.d.ts +9 -0
  161. package/node_modules/@herbertgao/resume-from/dist/platform/config/errors.js +12 -0
  162. package/node_modules/@herbertgao/resume-from/dist/platform/config/index.d.ts +5 -0
  163. package/node_modules/@herbertgao/resume-from/dist/platform/config/index.js +6 -0
  164. package/node_modules/@herbertgao/resume-from/dist/platform/config/loader.d.ts +10 -0
  165. package/node_modules/@herbertgao/resume-from/dist/platform/config/loader.js +47 -0
  166. package/node_modules/@herbertgao/resume-from/dist/platform/config/paths.d.ts +13 -0
  167. package/node_modules/@herbertgao/resume-from/dist/platform/config/paths.js +37 -0
  168. package/node_modules/@herbertgao/resume-from/dist/platform/config/validate.d.ts +9 -0
  169. package/node_modules/@herbertgao/resume-from/dist/platform/config/validate.js +117 -0
  170. package/node_modules/@herbertgao/resume-from/dist/platform/repo/contract.d.ts +30 -0
  171. package/node_modules/@herbertgao/resume-from/dist/platform/repo/contract.js +4 -0
  172. package/node_modules/@herbertgao/resume-from/dist/platform/repo/git.d.ts +21 -0
  173. package/node_modules/@herbertgao/resume-from/dist/platform/repo/git.js +74 -0
  174. package/node_modules/@herbertgao/resume-from/dist/platform/repo/index.d.ts +2 -0
  175. package/node_modules/@herbertgao/resume-from/dist/platform/repo/index.js +1 -0
  176. package/node_modules/@herbertgao/resume-from/dist/platform/repo/reader.d.ts +8 -0
  177. package/node_modules/@herbertgao/resume-from/dist/platform/repo/reader.js +130 -0
  178. package/node_modules/@herbertgao/resume-from/dist/platform/store/contract.d.ts +31 -0
  179. package/node_modules/@herbertgao/resume-from/dist/platform/store/contract.js +4 -0
  180. package/node_modules/@herbertgao/resume-from/dist/platform/store/file-committer.d.ts +3 -0
  181. package/node_modules/@herbertgao/resume-from/dist/platform/store/file-committer.js +391 -0
  182. package/node_modules/@herbertgao/resume-from/dist/platform/store/index.d.ts +2 -0
  183. package/node_modules/@herbertgao/resume-from/dist/platform/store/index.js +2 -0
  184. package/node_modules/@herbertgao/resume-from/dist/platform/tokens/contract.d.ts +12 -0
  185. package/node_modules/@herbertgao/resume-from/dist/platform/tokens/contract.js +4 -0
  186. package/node_modules/@herbertgao/resume-from/dist/platform/tokens/estimator.d.ts +12 -0
  187. package/node_modules/@herbertgao/resume-from/dist/platform/tokens/estimator.js +111 -0
  188. package/node_modules/@herbertgao/resume-from/dist/platform/tokens/index.d.ts +2 -0
  189. package/node_modules/@herbertgao/resume-from/dist/platform/tokens/index.js +1 -0
  190. package/node_modules/@herbertgao/resume-from/dist/session/contract.d.ts +109 -0
  191. package/node_modules/@herbertgao/resume-from/dist/session/contract.js +4 -0
  192. package/node_modules/@herbertgao/resume-from/package.json +94 -0
  193. package/node_modules/@herbertgao/resume-from/shims/pi/extensions/resume-from.js +118 -0
  194. package/package.json +6 -2
@@ -0,0 +1,9 @@
1
+ import type { ImportConfig } from "./contract.js";
2
+ /**
3
+ * Completes and checks one parsed configuration file. Absent settings keep their default;
4
+ * a present but unusable one rejects with the field named (FR-56).
5
+ *
6
+ * @param baseDir directory a relative home is resolved against — the file's own directory.
7
+ * @param source the file's path, used as the field of a whole-file complaint.
8
+ */
9
+ export declare function buildConfig(raw: unknown, baseDir: string, source: string): Promise<ImportConfig>;
@@ -0,0 +1,117 @@
1
+ import { defaultConfig } from "./defaults.js";
2
+ import { ConfigLoadError } from "./errors.js";
3
+ import { resolveHomePath } from "./paths.js";
4
+ // Exhaustive by construction: adding an agent (FR-57) fails to compile here until it is listed,
5
+ // instead of silently rejecting the new value at run time.
6
+ const AGENTS = { pi: true, codex: true, "claude-code": true };
7
+ const SETTINGS = {
8
+ extraHomes: true,
9
+ budgetShare: true,
10
+ pinnedRecentTurns: true,
11
+ windowOverrides: true,
12
+ };
13
+ /**
14
+ * Completes and checks one parsed configuration file. Absent settings keep their default;
15
+ * a present but unusable one rejects with the field named (FR-56).
16
+ *
17
+ * @param baseDir directory a relative home is resolved against — the file's own directory.
18
+ * @param source the file's path, used as the field of a whole-file complaint.
19
+ */
20
+ export async function buildConfig(raw, baseDir, source) {
21
+ const settings = asRecord(raw, source, `${source} must hold a JSON object of settings, such as { "budgetShare": 0.5 }.`);
22
+ for (const key of Object.keys(settings)) {
23
+ if (!Object.hasOwn(SETTINGS, key)) {
24
+ throw new ConfigLoadError(key, `${key} is not a setting. The settings are ${Object.keys(SETTINGS).join(", ")}. ` +
25
+ "Correct the spelling or remove the line.");
26
+ }
27
+ }
28
+ const config = defaultConfig();
29
+ if (settings.budgetShare !== undefined) {
30
+ config.budgetShare = asShare("budgetShare", settings.budgetShare);
31
+ }
32
+ if (settings.pinnedRecentTurns !== undefined) {
33
+ config.pinnedRecentTurns = asTurnCount("pinnedRecentTurns", settings.pinnedRecentTurns);
34
+ }
35
+ if (settings.extraHomes !== undefined) {
36
+ config.extraHomes = await asHomeEntries(settings.extraHomes, baseDir);
37
+ }
38
+ if (settings.windowOverrides !== undefined) {
39
+ config.windowOverrides = asWindowOverrides(settings.windowOverrides);
40
+ }
41
+ return config;
42
+ }
43
+ function asShare(field, value) {
44
+ // 0 is refused: it would make FR-33 block every import, which is a mistake, not a choice.
45
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1) {
46
+ throw new ConfigLoadError(field, `${field} must be a number greater than 0 and at most 1, for example 0.5. Found ${show(value)}.`);
47
+ }
48
+ return value;
49
+ }
50
+ function asTurnCount(field, value) {
51
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
52
+ throw new ConfigLoadError(field, `${field} must be a whole number of turns, zero or more, for example 8. Found ${show(value)}.`);
53
+ }
54
+ return value;
55
+ }
56
+ async function asHomeEntries(value, baseDir) {
57
+ const items = asArray(value, "extraHomes");
58
+ const entries = [];
59
+ for (const [index, item] of items.entries()) {
60
+ const field = `extraHomes[${index}]`;
61
+ const entry = asRecord(item, field, `${field} must be an object with an agent and a home, such as ` +
62
+ '{ "agent": "pi", "home": "~/.pi-work" }.');
63
+ entries.push({
64
+ agent: asAgentId(`${field}.agent`, entry.agent),
65
+ home: await resolveHomePath(asPath(`${field}.home`, entry.home), baseDir),
66
+ });
67
+ }
68
+ // Duplicates are kept: deduplication belongs to the search, which alone sees the
69
+ // adapter defaults an extra home may repeat.
70
+ return entries;
71
+ }
72
+ function asWindowOverrides(value) {
73
+ const items = asArray(value, "windowOverrides");
74
+ return items.map((item, index) => {
75
+ const field = `windowOverrides[${index}]`;
76
+ const entry = asRecord(item, field, `${field} must be an object with an agent and a windowTokens count, such as ` +
77
+ '{ "agent": "claude-code", "windowTokens": 200000 }.');
78
+ return {
79
+ agent: asAgentId(`${field}.agent`, entry.agent),
80
+ windowTokens: asWindowTokens(`${field}.windowTokens`, entry.windowTokens),
81
+ };
82
+ });
83
+ }
84
+ function asWindowTokens(field, value) {
85
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
86
+ throw new ConfigLoadError(field, `${field} must be a whole number of tokens greater than zero, for example 200000. ` +
87
+ `Found ${show(value)}.`);
88
+ }
89
+ return value;
90
+ }
91
+ function asAgentId(field, value) {
92
+ if (typeof value !== "string" || !Object.hasOwn(AGENTS, value)) {
93
+ throw new ConfigLoadError(field, `${field} must name a known agent: ${Object.keys(AGENTS).join(", ")}. Found ${show(value)}.`);
94
+ }
95
+ return value;
96
+ }
97
+ function asPath(field, value) {
98
+ if (typeof value !== "string" || value.trim() === "") {
99
+ throw new ConfigLoadError(field, `${field} must be a path to an agent profile directory. Found ${show(value)}.`);
100
+ }
101
+ return value;
102
+ }
103
+ function asArray(value, field) {
104
+ if (!Array.isArray(value)) {
105
+ throw new ConfigLoadError(field, `${field} must be a list. Found ${show(value)}.`);
106
+ }
107
+ return value;
108
+ }
109
+ function asRecord(value, field, message) {
110
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
111
+ throw new ConfigLoadError(field, message);
112
+ }
113
+ return value;
114
+ }
115
+ function show(value) {
116
+ return typeof value === "string" ? JSON.stringify(value) : String(value);
117
+ }
@@ -0,0 +1,30 @@
1
+ /** The repository the command runs in (FR-13). */
2
+ export interface RepoIdentity {
3
+ /** Absolute path of the repository root, or null when the directory is not in a repository. */
4
+ root: string | null;
5
+ /** Current HEAD commit, or null when the repository has no commit yet. */
6
+ head: string | null;
7
+ branch: string | null;
8
+ }
9
+ /** How far the tree moved since the source session ran (FR-38). */
10
+ export interface CommitDistance {
11
+ /** False when the source commit is unknown, or absent from this repository. */
12
+ known: boolean;
13
+ /** Commits on HEAD that the source commit does not have. */
14
+ ahead: number;
15
+ /** Commits the source commit has that HEAD does not. */
16
+ behind: number;
17
+ }
18
+ /** Process controls applied to every git command issued by one reader. */
19
+ export interface RepoReaderOptions {
20
+ /** Maximum duration of one git command. Defaults to a finite module-owned limit. */
21
+ timeoutMs?: number | undefined;
22
+ /** Cancels the current command and every later command issued by this reader. */
23
+ signal?: AbortSignal | undefined;
24
+ }
25
+ /** Reads git state. It never writes to the repository. */
26
+ export interface RepoReader {
27
+ identify(cwd: string): Promise<RepoIdentity>;
28
+ /** Compares HEAD with a commit of a source session (FR-37). */
29
+ distanceFrom(sourceCommit: string): Promise<CommitDistance>;
30
+ }
@@ -0,0 +1,4 @@
1
+ // GENERATED from src/platform/repo/module.md — the Public Contract section is the normative home.
2
+ // Declarations only: no behaviour, no defaults. If this file and module.md disagree,
3
+ // the document wins and this file is corrected.
4
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { RepoReaderOptions } from "./contract.js";
2
+ export interface GitResult {
3
+ /** True when git exited 0. A non-zero exit is an answer ("no such revision"), not a failure. */
4
+ ok: boolean;
5
+ stdout: string;
6
+ stderr: string;
7
+ }
8
+ /**
9
+ * Runs a read-only git command in `cwd`.
10
+ *
11
+ * Rejects only when git could not be run at all — a missing binary, a signal. An exit code is
12
+ * reported in `ok`, because the callers of this module treat "not a repository", "no commits yet"
13
+ * and "unknown revision" as facts to report rather than errors to raise.
14
+ */
15
+ export declare function runGit(cwd: string, args: readonly string[], options?: RepoReaderOptions): Promise<GitResult>;
16
+ interface NormalizedOptions {
17
+ timeoutMs: number;
18
+ signal?: AbortSignal | undefined;
19
+ }
20
+ export declare function normalizeGitOptions(options: RepoReaderOptions): NormalizedOptions;
21
+ export {};
@@ -0,0 +1,74 @@
1
+ // The only place in this module that reaches git. The binary is spawned with an argument array and
2
+ // never through a shell, so no revision string can ever be interpreted as a command.
3
+ import { execFile } from "node:child_process";
4
+ /** 1 MB. Every command here prints a line or two; more than this means something is wrong. */
5
+ const MAX_OUTPUT_BYTES = 1024 * 1024;
6
+ /** Long enough for local repositories while still bounding a wedged filesystem or git process. */
7
+ const DEFAULT_GIT_TIMEOUT_MS = 10_000;
8
+ /**
9
+ * Runs a read-only git command in `cwd`.
10
+ *
11
+ * Rejects only when git could not be run at all — a missing binary, a signal. An exit code is
12
+ * reported in `ok`, because the callers of this module treat "not a repository", "no commits yet"
13
+ * and "unknown revision" as facts to report rather than errors to raise.
14
+ */
15
+ export function runGit(cwd, args, options = {}) {
16
+ const processOptions = normalizeGitOptions(options);
17
+ return new Promise((resolve, reject) => {
18
+ const execOptions = {
19
+ encoding: "utf8",
20
+ env: gitEnv(),
21
+ maxBuffer: MAX_OUTPUT_BYTES,
22
+ timeout: processOptions.timeoutMs,
23
+ windowsHide: true,
24
+ };
25
+ if (processOptions.signal !== undefined)
26
+ execOptions.signal = processOptions.signal;
27
+ execFile("git", ["-C", cwd, ...args], execOptions, (error, stdout, stderr) => {
28
+ if (error === null) {
29
+ resolve({ ok: true, stdout, stderr });
30
+ return;
31
+ }
32
+ // execFile reports an exit code as a number and a genuine failure to run as a string code
33
+ // (ENOENT, EACCES) or a signal.
34
+ const exited = typeof error.code === "number" && !error.signal;
35
+ if (exited) {
36
+ resolve({ ok: false, stdout, stderr });
37
+ return;
38
+ }
39
+ reject(processError(cwd, processOptions, error));
40
+ });
41
+ });
42
+ }
43
+ export function normalizeGitOptions(options) {
44
+ const timeoutMs = options.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
45
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
46
+ throw new RangeError("git timeoutMs must be a positive, finite integer");
47
+ }
48
+ return options.signal === undefined ? { timeoutMs } : { timeoutMs, signal: options.signal };
49
+ }
50
+ function processError(cwd, options, cause) {
51
+ if (options.signal?.aborted === true || cause.name === "AbortError") {
52
+ return new Error(`git command aborted while reading ${cwd}`, { cause });
53
+ }
54
+ if (cause.killed === true && cause.signal !== undefined) {
55
+ return new Error(`git command timed out after ${options.timeoutMs} ms while reading ${cwd}`, {
56
+ cause,
57
+ });
58
+ }
59
+ return new Error(`git could not run while reading ${cwd}: ${cause.message}`, {
60
+ cause,
61
+ });
62
+ }
63
+ function gitEnv() {
64
+ const env = { ...process.env };
65
+ // Never take the index lock: a read must not be able to leave a lock file behind or rewrite the
66
+ // index of a repository the user owns.
67
+ env.GIT_OPTIONAL_LOCKS = "0";
68
+ // These would override `-C` and point git at a repository other than the one being asked about,
69
+ // for example when the tool runs inside a git hook.
70
+ delete env.GIT_DIR;
71
+ delete env.GIT_WORK_TREE;
72
+ delete env.GIT_INDEX_FILE;
73
+ return env;
74
+ }
@@ -0,0 +1,2 @@
1
+ export type { CommitDistance, RepoIdentity, RepoReader, RepoReaderOptions, } from "./contract.js";
2
+ export { createRepoReader } from "./reader.js";
@@ -0,0 +1 @@
1
+ export { createRepoReader } from "./reader.js";
@@ -0,0 +1,8 @@
1
+ import type { RepoReader, RepoReaderOptions } from "./contract.js";
2
+ /**
3
+ * A reader bound to one working directory.
4
+ *
5
+ * `identify` takes the directory to look at; `distanceFrom` compares against the HEAD of `cwd`,
6
+ * which is the repository the command is running in.
7
+ */
8
+ export declare function createRepoReader(cwd?: string, options?: RepoReaderOptions): RepoReader;
@@ -0,0 +1,130 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { normalizeGitOptions, runGit } from "./git.js";
4
+ /** Nothing is known: `ahead` and `behind` are 0 so a caller cannot print a fabricated distance. */
5
+ function unknownDistance() {
6
+ return { known: false, ahead: 0, behind: 0 };
7
+ }
8
+ function noRepository() {
9
+ return { root: null, head: null, branch: null };
10
+ }
11
+ /**
12
+ * Longer than any revision git can resolve: a full commit is 40 characters, a tag or a ref
13
+ * expression a few more. A longer string is a mistake or an attack, and is not sent to git.
14
+ */
15
+ const MAX_REVISION_LENGTH = 256;
16
+ /**
17
+ * A reader bound to one working directory.
18
+ *
19
+ * `identify` takes the directory to look at; `distanceFrom` compares against the HEAD of `cwd`,
20
+ * which is the repository the command is running in.
21
+ */
22
+ export function createRepoReader(cwd = process.cwd(), options = {}) {
23
+ const processOptions = normalizeGitOptions(options);
24
+ const git = (directory, args) => runGit(directory, args, processOptions);
25
+ return {
26
+ identify: (directory) => identify(directory, git),
27
+ distanceFrom: (sourceCommit) => distanceFrom(cwd, sourceCommit, git),
28
+ };
29
+ }
30
+ async function identify(directory, git) {
31
+ const dir = resolve(directory);
32
+ const toplevel = await git(dir, ["rev-parse", "--show-toplevel"]);
33
+ if (!toplevel.ok)
34
+ return noRepository();
35
+ const root = await resolveFully(toplevel.stdout.trim());
36
+ if (root === null)
37
+ return noRepository();
38
+ return {
39
+ root,
40
+ head: await commitOf(dir, "HEAD", git),
41
+ branch: await branch(dir, git),
42
+ };
43
+ }
44
+ async function distanceFrom(cwd, sourceCommit, git) {
45
+ const revision = plausibleRevision(sourceCommit);
46
+ if (revision === null)
47
+ return unknownDistance();
48
+ const dir = resolve(cwd);
49
+ const source = await commitOf(dir, revision, git);
50
+ if (source === null)
51
+ return unknownDistance();
52
+ const from = await commitOf(dir, "HEAD", git);
53
+ if (from === null)
54
+ return unknownDistance();
55
+ // Both sides are commits git itself printed, so the range holds no caller input.
56
+ const counts = await git(dir, [
57
+ "rev-list",
58
+ "--left-right",
59
+ "--count",
60
+ `${from}...${source}`,
61
+ "--",
62
+ ]);
63
+ if (!counts.ok)
64
+ return unknownDistance();
65
+ const parts = counts.stdout.trim().split(/\s+/);
66
+ if (parts.length !== 2)
67
+ return unknownDistance();
68
+ const ahead = Number(parts[0]);
69
+ const behind = Number(parts[1]);
70
+ if (!Number.isInteger(ahead) || !Number.isInteger(behind))
71
+ return unknownDistance();
72
+ return { known: true, ahead, behind };
73
+ }
74
+ /** The commit a revision names, or null when this repository does not have it. */
75
+ async function commitOf(dir, revision, git) {
76
+ // `--end-of-options` stops git reading the revision as an option, and `^{commit}` rejects a
77
+ // revision that exists but is not a commit.
78
+ const result = await git(dir, [
79
+ "rev-parse",
80
+ "--verify",
81
+ "--quiet",
82
+ "--end-of-options",
83
+ `${revision}^{commit}`,
84
+ ]);
85
+ const commit = result.stdout.trim();
86
+ return result.ok && commit !== "" ? commit : null;
87
+ }
88
+ /** The branch HEAD is on, or null when HEAD is detached. An unborn branch still has a name. */
89
+ async function branch(dir, git) {
90
+ const result = await git(dir, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
91
+ const name = result.stdout.trim();
92
+ return result.ok && name !== "" ? name : null;
93
+ }
94
+ /**
95
+ * A revision worth sending to git, trimmed, or null.
96
+ *
97
+ * A string that starts with `-` would be read as an option, an empty one names nothing, and a
98
+ * control character or an absurd length is never a revision. Rejecting them here means the hostile
99
+ * cases of T-REP-11 never reach a process at all.
100
+ */
101
+ function plausibleRevision(sourceCommit) {
102
+ if (typeof sourceCommit !== "string")
103
+ return null;
104
+ const revision = sourceCommit.trim();
105
+ if (revision === "" || revision.length > MAX_REVISION_LENGTH)
106
+ return null;
107
+ if (revision.startsWith("-"))
108
+ return null;
109
+ if (hasControlCharacter(revision))
110
+ return null;
111
+ return revision;
112
+ }
113
+ /** A NUL cannot be passed to a process at all, and no control character belongs in a revision. */
114
+ function hasControlCharacter(value) {
115
+ for (let i = 0; i < value.length; i += 1) {
116
+ const code = value.charCodeAt(i);
117
+ if (code < 0x20 || code === 0x7f)
118
+ return true;
119
+ }
120
+ return false;
121
+ }
122
+ /** The real path of a directory, following symlinks, so paths compare as locations. */
123
+ async function resolveFully(directory) {
124
+ try {
125
+ return await realpath(directory);
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
@@ -0,0 +1,31 @@
1
+ /** Raw file content. */
2
+ export type Bytes = Buffer;
3
+ /** A file to create. Its path must not already exist (FR-49). */
4
+ export interface PendingFile {
5
+ absolutePath: string;
6
+ bytes: Bytes;
7
+ }
8
+ /** Why a commit refused to run, or failed (FR-56). */
9
+ export type CommitRefusal = "path-exists" | "not-writable" | "write-failed";
10
+ /** A commit that succeeded and reports the paths it created (FR-52, FR-53). */
11
+ export interface CommitHandle {
12
+ createdPaths: string[];
13
+ }
14
+ /** Raised when a commit refuses to run or fails. Carries an actionable message (FR-56). */
15
+ export interface CommitError {
16
+ refusal: CommitRefusal;
17
+ /** The path that caused the refusal, when there is one. */
18
+ path: string | null;
19
+ /** What failed, and what the user can do next (FR-56). */
20
+ message: string;
21
+ /** Paths retained for safety or not removed by cleanup, when manual inspection may be required. */
22
+ remainingPaths?: string[];
23
+ }
24
+ /** Atomically adds zero or one file to a home (FR-49, FR-53). */
25
+ export interface FileCommitter {
26
+ /**
27
+ * Creates zero or one file. Rejects with a CommitError before filesystem access when more than one
28
+ * file is supplied, or before writing bytes when the destination already exists.
29
+ */
30
+ commit(root: string, files: PendingFile[]): Promise<CommitHandle>;
31
+ }
@@ -0,0 +1,4 @@
1
+ // GENERATED from src/platform/store/module.md — the Public Contract section is the normative home.
2
+ // Declarations only: no behaviour, no defaults. If this file and module.md disagree,
3
+ // the document wins and this file is corrected.
4
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { FileCommitter } from "./contract.js";
2
+ /** Creates the guarded store. It holds no state between commits. */
3
+ export declare function createFileCommitter(): FileCommitter;