@astralyn/pi 0.84.1 → 0.84.3

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 (163) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/cli/args.js +1 -1
  3. package/dist/cli/args.js.map +1 -1
  4. package/dist/cli/config-selector.d.ts.map +1 -1
  5. package/dist/cli/config-selector.js +2 -3
  6. package/dist/cli/config-selector.js.map +1 -1
  7. package/dist/cli/startup-ui.d.ts.map +1 -1
  8. package/dist/cli/startup-ui.js +3 -5
  9. package/dist/cli/startup-ui.js.map +1 -1
  10. package/dist/config.d.ts +5 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +8 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/core/agent-session.d.ts +1 -25
  15. package/dist/core/agent-session.d.ts.map +1 -1
  16. package/dist/core/agent-session.js +29 -408
  17. package/dist/core/agent-session.js.map +1 -1
  18. package/dist/core/compaction-controller.d.ts +89 -0
  19. package/dist/core/compaction-controller.d.ts.map +1 -0
  20. package/dist/core/compaction-controller.js +428 -0
  21. package/dist/core/compaction-controller.js.map +1 -0
  22. package/dist/core/extensions/index.d.ts +1 -1
  23. package/dist/core/extensions/index.d.ts.map +1 -1
  24. package/dist/core/extensions/index.js.map +1 -1
  25. package/dist/core/extensions/loader.js +3 -3
  26. package/dist/core/extensions/loader.js.map +1 -1
  27. package/dist/core/extensions/types.d.ts +1 -15
  28. package/dist/core/extensions/types.d.ts.map +1 -1
  29. package/dist/core/extensions/types.js.map +1 -1
  30. package/dist/core/provider-attribution.js +2 -2
  31. package/dist/core/provider-attribution.js.map +1 -1
  32. package/dist/core/settings-manager.js +1 -1
  33. package/dist/core/settings-manager.js.map +1 -1
  34. package/dist/core/telemetry.d.ts +1 -1
  35. package/dist/core/telemetry.d.ts.map +1 -1
  36. package/dist/core/telemetry.js +1 -1
  37. package/dist/core/telemetry.js.map +1 -1
  38. package/dist/extensions/deepwiki/index.d.ts.map +1 -1
  39. package/dist/extensions/deepwiki/index.js +23 -1
  40. package/dist/extensions/deepwiki/index.js.map +1 -1
  41. package/dist/extensions/deepwiki/render.d.ts +1 -1
  42. package/dist/extensions/deepwiki/render.d.ts.map +1 -1
  43. package/dist/extensions/deepwiki/render.js +4 -2
  44. package/dist/extensions/deepwiki/render.js.map +1 -1
  45. package/dist/extensions/index.d.ts.map +1 -1
  46. package/dist/extensions/index.js +0 -4
  47. package/dist/extensions/index.js.map +1 -1
  48. package/dist/extensions/question/index.d.ts.map +1 -1
  49. package/dist/extensions/question/index.js +0 -1
  50. package/dist/extensions/question/index.js.map +1 -1
  51. package/dist/extensions/subagent/index.d.ts.map +1 -1
  52. package/dist/extensions/subagent/index.js +20 -2
  53. package/dist/extensions/subagent/index.js.map +1 -1
  54. package/dist/extensions/todo/schema.d.ts +1 -1
  55. package/dist/extensions/todo/state.js +5 -5
  56. package/dist/extensions/todo/state.js.map +1 -1
  57. package/dist/modes/interactive/components/extension-selector.d.ts +0 -2
  58. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  59. package/dist/modes/interactive/components/extension-selector.js +1 -1
  60. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  61. package/dist/modes/interactive/components/settings-selector.js +1 -1
  62. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  63. package/dist/modes/interactive/components/tool-chrome.d.ts +28 -0
  64. package/dist/modes/interactive/components/tool-chrome.d.ts.map +1 -0
  65. package/dist/modes/interactive/components/tool-chrome.js +81 -0
  66. package/dist/modes/interactive/components/tool-chrome.js.map +1 -0
  67. package/dist/modes/interactive/components/tool-execution.d.ts +0 -3
  68. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/tool-execution.js +11 -108
  70. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  71. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  72. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  73. package/dist/modes/interactive/interactive-mode.js +35 -52
  74. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  75. package/docs/bundled/README.md +0 -2
  76. package/docs/bundled/extensions/subagent.md +1 -1
  77. package/docs/bundled/tool-presentation.md +2 -2
  78. package/docs/docs.json +0 -8
  79. package/docs/extensions.md +6 -23
  80. package/docs/settings.md +1 -1
  81. package/docs/usage.md +1 -2
  82. package/examples/extensions/README.md +2 -0
  83. package/examples/extensions/plan-mode/README.md +66 -0
  84. package/examples/extensions/plan-mode/index.ts +390 -0
  85. package/examples/extensions/plan-mode/utils.ts +168 -0
  86. package/examples/extensions/subagent/README.md +175 -0
  87. package/examples/extensions/subagent/agents/planner.md +37 -0
  88. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  89. package/examples/extensions/subagent/agents/scout.md +50 -0
  90. package/examples/extensions/subagent/agents/worker.md +24 -0
  91. package/examples/extensions/subagent/agents.ts +126 -0
  92. package/examples/extensions/subagent/index.ts +1009 -0
  93. package/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
  94. package/examples/extensions/subagent/prompts/implement.md +10 -0
  95. package/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
  96. package/npm-shrinkwrap.json +2 -2
  97. package/package.json +1 -1
  98. package/dist/extensions/biu/index.d.ts +0 -19
  99. package/dist/extensions/biu/index.d.ts.map +0 -1
  100. package/dist/extensions/biu/index.js +0 -350
  101. package/dist/extensions/biu/index.js.map +0 -1
  102. package/dist/extensions/biu/prompts.d.ts +0 -26
  103. package/dist/extensions/biu/prompts.d.ts.map +0 -1
  104. package/dist/extensions/biu/prompts.js +0 -260
  105. package/dist/extensions/biu/prompts.js.map +0 -1
  106. package/dist/extensions/biu/workspace.d.ts +0 -122
  107. package/dist/extensions/biu/workspace.d.ts.map +0 -1
  108. package/dist/extensions/biu/workspace.js +0 -404
  109. package/dist/extensions/biu/workspace.js.map +0 -1
  110. package/dist/extensions/rewind/anchor.d.ts +0 -29
  111. package/dist/extensions/rewind/anchor.d.ts.map +0 -1
  112. package/dist/extensions/rewind/anchor.js +0 -35
  113. package/dist/extensions/rewind/anchor.js.map +0 -1
  114. package/dist/extensions/rewind/config.d.ts +0 -16
  115. package/dist/extensions/rewind/config.d.ts.map +0 -1
  116. package/dist/extensions/rewind/config.js +0 -69
  117. package/dist/extensions/rewind/config.js.map +0 -1
  118. package/dist/extensions/rewind/engine.d.ts +0 -93
  119. package/dist/extensions/rewind/engine.d.ts.map +0 -1
  120. package/dist/extensions/rewind/engine.js +0 -778
  121. package/dist/extensions/rewind/engine.js.map +0 -1
  122. package/dist/extensions/rewind/gc.d.ts +0 -35
  123. package/dist/extensions/rewind/gc.d.ts.map +0 -1
  124. package/dist/extensions/rewind/gc.js +0 -169
  125. package/dist/extensions/rewind/gc.js.map +0 -1
  126. package/dist/extensions/rewind/index.d.ts +0 -3
  127. package/dist/extensions/rewind/index.d.ts.map +0 -1
  128. package/dist/extensions/rewind/index.js +0 -287
  129. package/dist/extensions/rewind/index.js.map +0 -1
  130. package/dist/extensions/rewind/menu.d.ts +0 -13
  131. package/dist/extensions/rewind/menu.d.ts.map +0 -1
  132. package/dist/extensions/rewind/menu.js +0 -123
  133. package/dist/extensions/rewind/menu.js.map +0 -1
  134. package/dist/extensions/rewind/paths.d.ts +0 -4
  135. package/dist/extensions/rewind/paths.d.ts.map +0 -1
  136. package/dist/extensions/rewind/paths.js +0 -15
  137. package/dist/extensions/rewind/paths.js.map +0 -1
  138. package/dist/extensions/rewind/restore.d.ts +0 -54
  139. package/dist/extensions/rewind/restore.d.ts.map +0 -1
  140. package/dist/extensions/rewind/restore.js +0 -94
  141. package/dist/extensions/rewind/restore.js.map +0 -1
  142. package/dist/extensions/rewind/snapshot.d.ts +0 -45
  143. package/dist/extensions/rewind/snapshot.d.ts.map +0 -1
  144. package/dist/extensions/rewind/snapshot.js +0 -21
  145. package/dist/extensions/rewind/snapshot.js.map +0 -1
  146. package/dist/extensions/rewind/storage.d.ts +0 -12
  147. package/dist/extensions/rewind/storage.d.ts.map +0 -1
  148. package/dist/extensions/rewind/storage.js +0 -33
  149. package/dist/extensions/rewind/storage.js.map +0 -1
  150. package/dist/extensions/rewind/text.d.ts +0 -7
  151. package/dist/extensions/rewind/text.d.ts.map +0 -1
  152. package/dist/extensions/rewind/text.js +0 -19
  153. package/dist/extensions/rewind/text.js.map +0 -1
  154. package/dist/extensions/rewind/tool-path.d.ts +0 -3
  155. package/dist/extensions/rewind/tool-path.d.ts.map +0 -1
  156. package/dist/extensions/rewind/tool-path.js +0 -9
  157. package/dist/extensions/rewind/tool-path.js.map +0 -1
  158. package/dist/utils/coalescing-terminal.d.ts +0 -132
  159. package/dist/utils/coalescing-terminal.d.ts.map +0 -1
  160. package/dist/utils/coalescing-terminal.js +0 -292
  161. package/dist/utils/coalescing-terminal.js.map +0 -1
  162. package/docs/bundled/extensions/biu.md +0 -63
  163. package/docs/bundled/extensions/rewind.md +0 -86
@@ -1,404 +0,0 @@
1
- /**
2
- * biu/workspace.ts — filesystem view of the Biu workspace.
3
- *
4
- * The workspace lives under <agentDir>/biu/<safe-cwd>/ and holds SPEC.md,
5
- * tasks/TASK-*.md, Summary.md, and archived/. Workflow state is carried by
6
- * Markdown frontmatter and derived on demand by loadBiuSnapshot(); nothing
7
- * here owns state. The model addresses workspace files through the biu://
8
- * scheme, resolved by resolveBiuUri().
9
- */
10
- import { mkdir, readdir, readFile, rename, rm, stat } from "node:fs/promises";
11
- import { join } from "node:path";
12
- import { getAgentDir } from "../../config.js";
13
- import { applyEditsToNormalizedContent, normalizeToLF } from "../../core/tools/edit-diff.js";
14
- import { parseFrontmatter } from "../../utils/frontmatter.js";
15
- import { cwdToSafeDirName, resolvePath } from "../../utils/paths.js";
16
- export const BIU_URI_SCHEME = "biu://";
17
- export const BIU_SPEC_FILE_NAME = "SPEC.md";
18
- export const BIU_SUMMARY_FILE_NAME = "Summary.md";
19
- export const BIU_TASKS_DIRECTORY_NAME = "tasks";
20
- export const BIU_ARCHIVED_DIRECTORY_NAME = "archived";
21
- export const BIU_MAX_SHORTNAME_LENGTH = 60;
22
- /** Snapshot bound: tasks beyond this count are reported in problems, not listed. */
23
- export const BIU_MAX_SNAPSHOT_TASKS = 100;
24
- export const BIU_STAGES = ["plan", "execute", "archive"];
25
- export const BIU_TASK_STATUSES = ["ready", "in_progress", "completed"];
26
- export const BIU_SPEC_STATUSES = ["draft", "ready"];
27
- export const BIU_EXECUTION_MODES = ["direct", "tasks"];
28
- export function getBiuPaths(cwd, agentDir = getAgentDir()) {
29
- const root = join(agentDir, "biu", cwdToSafeDirName(resolvePath(cwd)));
30
- return {
31
- root,
32
- specFile: join(root, BIU_SPEC_FILE_NAME),
33
- summaryFile: join(root, BIU_SUMMARY_FILE_NAME),
34
- tasksDir: join(root, BIU_TASKS_DIRECTORY_NAME),
35
- archivedDir: join(root, BIU_ARCHIVED_DIRECTORY_NAME),
36
- };
37
- }
38
- /** Create the workspace directories when missing. Idempotent. */
39
- export async function ensureBiuWorkspace(cwd, agentDir = getAgentDir()) {
40
- const paths = getBiuPaths(cwd, agentDir);
41
- await Promise.all([mkdir(paths.tasksDir, { recursive: true }), mkdir(paths.archivedDir, { recursive: true })]);
42
- return paths;
43
- }
44
- export function isBiuUri(path) {
45
- return path.startsWith(BIU_URI_SCHEME);
46
- }
47
- /**
48
- * Resolve a biu:// URI to an absolute path inside the workspace for the given
49
- * cwd. `biu://` alone resolves to the workspace root. Rejects rooted rests and
50
- * any ".." segment, so a resolved path can never escape the workspace.
51
- */
52
- export function resolveBiuUri(uri, cwd, agentDir = getAgentDir()) {
53
- if (!isBiuUri(uri))
54
- return { ok: false, reason: `Not a biu:// path: ${uri}` };
55
- const rest = uri.slice(BIU_URI_SCHEME.length).replace(/\\/g, "/");
56
- if (rest.startsWith("/") || /^[A-Za-z]:/.test(rest) || rest.startsWith("~")) {
57
- return {
58
- ok: false,
59
- reason: `Invalid biu:// path ${JSON.stringify(uri)}: use a workspace-relative path like ${BIU_URI_SCHEME}${BIU_SPEC_FILE_NAME}.`,
60
- };
61
- }
62
- const segments = rest.split("/").filter((segment) => segment !== "" && segment !== ".");
63
- if (segments.some((segment) => segment === "..")) {
64
- return { ok: false, reason: `Invalid biu:// path ${JSON.stringify(uri)}: ".." segments are not allowed.` };
65
- }
66
- const paths = getBiuPaths(cwd, agentDir);
67
- return { ok: true, path: segments.length === 0 ? paths.root : join(paths.root, ...segments) };
68
- }
69
- /** Format a workspace-relative location as a biu:// URI for prompts and messages. */
70
- export function toBiuUri(...segments) {
71
- return `${BIU_URI_SCHEME}${segments.join("/")}`;
72
- }
73
- /** Whether the content's frontmatter says status: ready. Parse failures count as not ready. */
74
- function isSpecContentReady(content) {
75
- try {
76
- return parseFrontmatter(content).frontmatter.status === "ready";
77
- }
78
- catch {
79
- return false;
80
- }
81
- }
82
- /** Normalize edit-tool input into Edit[], accepting the legacy top-level oldText/newText shape. */
83
- function toSpecEdits(input) {
84
- const edits = [];
85
- if (Array.isArray(input.edits)) {
86
- for (const entry of input.edits) {
87
- if (!entry || typeof entry !== "object")
88
- continue;
89
- const candidate = entry;
90
- if (typeof candidate.oldText === "string" && typeof candidate.newText === "string") {
91
- edits.push({ oldText: candidate.oldText, newText: candidate.newText });
92
- }
93
- }
94
- }
95
- if (typeof input.oldText === "string" && typeof input.newText === "string") {
96
- edits.push({ oldText: input.oldText, newText: input.newText });
97
- }
98
- return edits;
99
- }
100
- /**
101
- * Whether a write/edit tool call would flip the SPEC's frontmatter status from
102
- * non-ready to ready — the transition that requires user approval. Edits are
103
- * simulated with the edit tool's own replacement semantics. Any failure
104
- * (unmatched oldText, invalid YAML) returns false: the gate steps aside and
105
- * lets the real tool report the error.
106
- */
107
- export function detectSpecReadyTransition(currentContent, toolName, input) {
108
- if (currentContent !== null && isSpecContentReady(currentContent))
109
- return false;
110
- if (toolName === "write") {
111
- return typeof input.content === "string" && isSpecContentReady(input.content);
112
- }
113
- if (currentContent === null)
114
- return false;
115
- const edits = toSpecEdits(input);
116
- if (edits.length === 0)
117
- return false;
118
- try {
119
- const { newContent } = applyEditsToNormalizedContent(normalizeToLF(currentContent), edits, BIU_SPEC_FILE_NAME);
120
- return isSpecContentReady(newContent);
121
- }
122
- catch {
123
- return false;
124
- }
125
- }
126
- function isMissingFileError(error) {
127
- return error instanceof Error && "code" in error && error.code === "ENOENT";
128
- }
129
- function describeError(error) {
130
- return error instanceof Error ? error.message : String(error);
131
- }
132
- async function pathExists(path) {
133
- try {
134
- await stat(path);
135
- return true;
136
- }
137
- catch (error) {
138
- if (isMissingFileError(error))
139
- return false;
140
- throw error;
141
- }
142
- }
143
- function asTrimmedString(value) {
144
- if (typeof value !== "string")
145
- return null;
146
- const trimmed = value.trim();
147
- return trimmed || null;
148
- }
149
- function parseStatus(value, allowed) {
150
- return typeof value === "string" && allowed.includes(value) ? value : "unknown";
151
- }
152
- /** Strip an optional .md extension so depends_on accepts "TASK-x" and "TASK-x.md" alike. */
153
- function toTaskId(reference) {
154
- return reference.endsWith(".md") ? reference.slice(0, -3) : reference;
155
- }
156
- async function readSpecInfo(specFile, problems) {
157
- let raw;
158
- try {
159
- raw = await readFile(specFile, "utf8");
160
- }
161
- catch (error) {
162
- if (!isMissingFileError(error))
163
- problems.push(`${BIU_SPEC_FILE_NAME}: ${describeError(error)}`);
164
- return { exists: false, status: "unknown", title: null, baselineCommit: null, execution: null };
165
- }
166
- try {
167
- const { frontmatter } = parseFrontmatter(raw);
168
- const status = parseStatus(frontmatter.status, BIU_SPEC_STATUSES);
169
- if (status === "unknown") {
170
- problems.push(`${BIU_SPEC_FILE_NAME}: frontmatter "status" must be one of ${BIU_SPEC_STATUSES.join(", ")}.`);
171
- }
172
- let execution = null;
173
- if (frontmatter.execution !== undefined) {
174
- const parsed = parseStatus(frontmatter.execution, BIU_EXECUTION_MODES);
175
- if (parsed === "unknown") {
176
- problems.push(`${BIU_SPEC_FILE_NAME}: frontmatter "execution" must be one of ${BIU_EXECUTION_MODES.join(", ")}.`);
177
- }
178
- else {
179
- execution = parsed;
180
- }
181
- }
182
- return {
183
- exists: true,
184
- status,
185
- title: asTrimmedString(frontmatter.title),
186
- baselineCommit: asTrimmedString(frontmatter.baseline_commit),
187
- execution,
188
- };
189
- }
190
- catch (error) {
191
- problems.push(`${BIU_SPEC_FILE_NAME}: invalid frontmatter (${describeError(error)})`);
192
- return { exists: true, status: "unknown", title: null, baselineCommit: null, execution: null };
193
- }
194
- }
195
- async function readTaskInfo(tasksDir, fileName, problems) {
196
- const id = toTaskId(fileName);
197
- const relative = `${BIU_TASKS_DIRECTORY_NAME}/${fileName}`;
198
- let raw;
199
- try {
200
- raw = await readFile(join(tasksDir, fileName), "utf8");
201
- }
202
- catch (error) {
203
- problems.push(`${relative}: ${describeError(error)}`);
204
- return { id, title: id, status: "unknown", dependsOn: [] };
205
- }
206
- try {
207
- const { frontmatter } = parseFrontmatter(raw);
208
- const status = parseStatus(frontmatter.status, BIU_TASK_STATUSES);
209
- if (status === "unknown") {
210
- problems.push(`${relative}: frontmatter "status" must be one of ${BIU_TASK_STATUSES.join(", ")}.`);
211
- }
212
- const dependsOn = Array.isArray(frontmatter.depends_on)
213
- ? frontmatter.depends_on.filter((dep) => typeof dep === "string").map(toTaskId)
214
- : [];
215
- return { id, title: asTrimmedString(frontmatter.title) ?? id, status, dependsOn };
216
- }
217
- catch (error) {
218
- problems.push(`${relative}: invalid frontmatter (${describeError(error)})`);
219
- return { id, title: id, status: "unknown", dependsOn: [] };
220
- }
221
- }
222
- function countTasks(tasks) {
223
- const counts = { total: tasks.length, ready: 0, inProgress: 0, completed: 0, unknown: 0 };
224
- for (const task of tasks) {
225
- if (task.status === "ready")
226
- counts.ready++;
227
- else if (task.status === "in_progress")
228
- counts.inProgress++;
229
- else if (task.status === "completed")
230
- counts.completed++;
231
- else
232
- counts.unknown++;
233
- }
234
- return counts;
235
- }
236
- /** What to work on next: the in-progress task, else the first ready task whose dependencies are all completed. */
237
- export function getBiuFocus(tasks) {
238
- const active = tasks.find((task) => task.status === "in_progress");
239
- if (active)
240
- return { kind: "active", task: active };
241
- const byId = new Map(tasks.map((task) => [task.id, task]));
242
- const next = tasks.find((task) => task.status === "ready" && task.dependsOn.every((dep) => byId.get(dep)?.status === "completed"));
243
- if (next)
244
- return { kind: "next", task: next };
245
- if (tasks.length > 0 && tasks.every((task) => task.status === "completed"))
246
- return { kind: "allDone" };
247
- return { kind: "none" };
248
- }
249
- function deriveStage(spec, tasks, summaryExists) {
250
- if (summaryExists)
251
- return "archive";
252
- if (!spec.exists || spec.status !== "ready")
253
- return "plan";
254
- if (tasks.length > 0 && tasks.every((task) => task.status === "completed"))
255
- return "archive";
256
- return "execute";
257
- }
258
- /**
259
- * Build the workspace snapshot by reading frontmatter from SPEC.md and
260
- * tasks/*.md. Content problems (unreadable files, malformed frontmatter) are
261
- * reported in `problems` and degrade to "unknown" instead of throwing.
262
- */
263
- export async function loadBiuSnapshot(cwd, agentDir = getAgentDir()) {
264
- const paths = getBiuPaths(cwd, agentDir);
265
- const problems = [];
266
- const spec = await readSpecInfo(paths.specFile, problems);
267
- let summaryExists = false;
268
- try {
269
- summaryExists = await pathExists(paths.summaryFile);
270
- }
271
- catch (error) {
272
- problems.push(`${BIU_SUMMARY_FILE_NAME}: ${describeError(error)}`);
273
- }
274
- let taskFiles = [];
275
- try {
276
- taskFiles = (await readdir(paths.tasksDir)).filter((name) => name.endsWith(".md")).sort();
277
- }
278
- catch (error) {
279
- if (!isMissingFileError(error))
280
- problems.push(`${BIU_TASKS_DIRECTORY_NAME}/: ${describeError(error)}`);
281
- }
282
- if (taskFiles.length > BIU_MAX_SNAPSHOT_TASKS) {
283
- problems.push(`${BIU_TASKS_DIRECTORY_NAME}/: ${taskFiles.length} task files exceed the snapshot limit of ${BIU_MAX_SNAPSHOT_TASKS}; only the first ${BIU_MAX_SNAPSHOT_TASKS} are listed.`);
284
- taskFiles = taskFiles.slice(0, BIU_MAX_SNAPSHOT_TASKS);
285
- }
286
- const tasks = [];
287
- for (const fileName of taskFiles) {
288
- tasks.push(await readTaskInfo(paths.tasksDir, fileName, problems));
289
- }
290
- return {
291
- stage: deriveStage(spec, tasks, summaryExists),
292
- spec,
293
- summaryExists,
294
- tasks,
295
- counts: countTasks(tasks),
296
- focus: getBiuFocus(tasks),
297
- problems,
298
- };
299
- }
300
- export function sanitizeShortname(shortname) {
301
- const printable = Array.from(shortname.trim())
302
- .filter((ch) => {
303
- const code = ch.codePointAt(0) ?? 0;
304
- return code >= 0x20 && code !== 0x7f;
305
- })
306
- .join("");
307
- const cleaned = printable
308
- .replace(/[<>:"/\\|?*]+/g, "-")
309
- .replace(/\s+/g, "-")
310
- .replace(/-{2,}/g, "-")
311
- .replace(/^[-.]+|[-.]+$/g, "");
312
- return cleaned.slice(0, BIU_MAX_SHORTNAME_LENGTH).replace(/^[-.]+|[-.]+$/g, "");
313
- }
314
- /** Move one file or directory, returning false when the source does not exist. */
315
- async function moveIfExists(from, to) {
316
- if (!(await pathExists(from)))
317
- return false;
318
- await rename(from, to);
319
- return true;
320
- }
321
- /**
322
- * Close the current cycle: move SPEC.md, Summary.md, and tasks/ into
323
- * archived/YYYY-MM-DD-<shortname>/. If any move fails, already-moved files are
324
- * rolled back so the cycle is never left half-archived; when a rollback itself
325
- * fails, the recovery data stays in the archive directory and the error says
326
- * where. Requires SPEC.md and Summary.md to exist so an empty or unfinished
327
- * draft is never archived silently.
328
- *
329
- * `move` is injectable for tests; rollback always uses the real move.
330
- */
331
- export async function archiveBiuCycle(cwd, shortname, agentDir = getAgentDir(), now = new Date(), move = moveIfExists) {
332
- const paths = getBiuPaths(cwd, agentDir);
333
- const safeName = sanitizeShortname(shortname);
334
- if (!safeName)
335
- throw new Error(`Archive shortname ${JSON.stringify(shortname)} is empty after sanitization.`);
336
- if (!(await pathExists(paths.specFile))) {
337
- throw new Error("Cannot archive: SPEC.md does not exist in the Biu workspace.");
338
- }
339
- if (!(await pathExists(paths.summaryFile))) {
340
- throw new Error("Cannot archive: Summary.md does not exist yet. Draft and confirm the summary first.");
341
- }
342
- await mkdir(paths.archivedDir, { recursive: true });
343
- const date = now.toISOString().slice(0, 10);
344
- const baseName = `${date}-${safeName}`;
345
- let existing;
346
- try {
347
- existing = await readdir(paths.archivedDir);
348
- }
349
- catch {
350
- existing = [];
351
- }
352
- let archiveName = baseName;
353
- for (let suffix = 2; existing.includes(archiveName); suffix++) {
354
- archiveName = `${baseName}-${String(suffix).padStart(2, "0")}`;
355
- }
356
- const archivedPath = join(paths.archivedDir, archiveName);
357
- await mkdir(archivedPath, { recursive: false });
358
- const moves = [
359
- { from: paths.specFile, to: join(archivedPath, BIU_SPEC_FILE_NAME) },
360
- { from: paths.summaryFile, to: join(archivedPath, BIU_SUMMARY_FILE_NAME) },
361
- { from: paths.tasksDir, to: join(archivedPath, BIU_TASKS_DIRECTORY_NAME) },
362
- ];
363
- const moved = [];
364
- try {
365
- for (const entry of moves) {
366
- if (await move(entry.from, entry.to))
367
- moved.push(entry);
368
- }
369
- }
370
- catch (error) {
371
- throw await rollbackArchive(error, archivedPath, moved);
372
- }
373
- // Recreate the empty tasks/ directory for the next cycle. Best-effort: the
374
- // next ensureBiuWorkspace call recreates it anyway.
375
- await mkdir(paths.tasksDir, { recursive: true }).catch(() => { });
376
- return { archivedPath, archiveName };
377
- }
378
- /**
379
- * Reverse the already-moved files and clean the archive directory. Returns
380
- * the original error unchanged when the rollback fully succeeded, otherwise
381
- * a chained error whose message says exactly what remains inconsistent.
382
- */
383
- async function rollbackArchive(error, archivedPath, moved) {
384
- const restoreErrors = [];
385
- for (const entry of [...moved].reverse()) {
386
- try {
387
- await moveIfExists(entry.to, entry.from);
388
- }
389
- catch (restoreError) {
390
- restoreErrors.push(`moving ${entry.to} back failed: ${describeError(restoreError)}`);
391
- }
392
- }
393
- if (restoreErrors.length > 0) {
394
- return new Error(`${describeError(error)} Rollback incomplete, recovery data left in ${archivedPath}: ${restoreErrors.join("; ")}`, { cause: error });
395
- }
396
- try {
397
- await rm(archivedPath, { recursive: true, force: true });
398
- }
399
- catch (cleanupError) {
400
- return new Error(`${describeError(error)} Rollback succeeded, but removing the empty ${archivedPath} directory failed: ${describeError(cleanupError)}`, { cause: error });
401
- }
402
- return error instanceof Error ? error : new Error(describeError(error), { cause: error });
403
- }
404
- //# sourceMappingURL=workspace.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../../src/extensions/biu/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * biu/workspace.ts — filesystem view of the Biu workspace.\n *\n * The workspace lives under <agentDir>/biu/<safe-cwd>/ and holds SPEC.md,\n * tasks/TASK-*.md, Summary.md, and archived/. Workflow state is carried by\n * Markdown frontmatter and derived on demand by loadBiuSnapshot(); nothing\n * here owns state. The model addresses workspace files through the biu://\n * scheme, resolved by resolveBiuUri().\n */\nimport { mkdir, readdir, readFile, rename, rm, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { applyEditsToNormalizedContent, type Edit, normalizeToLF } from \"../../core/tools/edit-diff.ts\";\nimport { parseFrontmatter } from \"../../utils/frontmatter.ts\";\nimport { cwdToSafeDirName, resolvePath } from \"../../utils/paths.ts\";\n\nexport const BIU_URI_SCHEME = \"biu://\";\nexport const BIU_SPEC_FILE_NAME = \"SPEC.md\";\nexport const BIU_SUMMARY_FILE_NAME = \"Summary.md\";\nexport const BIU_TASKS_DIRECTORY_NAME = \"tasks\";\nexport const BIU_ARCHIVED_DIRECTORY_NAME = \"archived\";\nexport const BIU_MAX_SHORTNAME_LENGTH = 60;\n/** Snapshot bound: tasks beyond this count are reported in problems, not listed. */\nexport const BIU_MAX_SNAPSHOT_TASKS = 100;\n\nexport const BIU_STAGES = [\"plan\", \"execute\", \"archive\"] as const;\nexport type BiuStage = (typeof BIU_STAGES)[number];\n\nexport const BIU_TASK_STATUSES = [\"ready\", \"in_progress\", \"completed\"] as const;\nexport type BiuTaskStatus = (typeof BIU_TASK_STATUSES)[number];\n\nexport const BIU_SPEC_STATUSES = [\"draft\", \"ready\"] as const;\nexport type BiuSpecStatus = (typeof BIU_SPEC_STATUSES)[number];\n\nexport const BIU_EXECUTION_MODES = [\"direct\", \"tasks\"] as const;\nexport type BiuExecutionMode = (typeof BIU_EXECUTION_MODES)[number];\n\nexport interface BiuPaths {\n\troot: string;\n\tspecFile: string;\n\tsummaryFile: string;\n\ttasksDir: string;\n\tarchivedDir: string;\n}\n\nexport function getBiuPaths(cwd: string, agentDir: string = getAgentDir()): BiuPaths {\n\tconst root = join(agentDir, \"biu\", cwdToSafeDirName(resolvePath(cwd)));\n\treturn {\n\t\troot,\n\t\tspecFile: join(root, BIU_SPEC_FILE_NAME),\n\t\tsummaryFile: join(root, BIU_SUMMARY_FILE_NAME),\n\t\ttasksDir: join(root, BIU_TASKS_DIRECTORY_NAME),\n\t\tarchivedDir: join(root, BIU_ARCHIVED_DIRECTORY_NAME),\n\t};\n}\n\n/** Create the workspace directories when missing. Idempotent. */\nexport async function ensureBiuWorkspace(cwd: string, agentDir: string = getAgentDir()): Promise<BiuPaths> {\n\tconst paths = getBiuPaths(cwd, agentDir);\n\tawait Promise.all([mkdir(paths.tasksDir, { recursive: true }), mkdir(paths.archivedDir, { recursive: true })]);\n\treturn paths;\n}\n\nexport function isBiuUri(path: string): boolean {\n\treturn path.startsWith(BIU_URI_SCHEME);\n}\n\nexport type BiuUriResolution = { ok: true; path: string } | { ok: false; reason: string };\n\n/**\n * Resolve a biu:// URI to an absolute path inside the workspace for the given\n * cwd. `biu://` alone resolves to the workspace root. Rejects rooted rests and\n * any \"..\" segment, so a resolved path can never escape the workspace.\n */\nexport function resolveBiuUri(uri: string, cwd: string, agentDir: string = getAgentDir()): BiuUriResolution {\n\tif (!isBiuUri(uri)) return { ok: false, reason: `Not a biu:// path: ${uri}` };\n\tconst rest = uri.slice(BIU_URI_SCHEME.length).replace(/\\\\/g, \"/\");\n\tif (rest.startsWith(\"/\") || /^[A-Za-z]:/.test(rest) || rest.startsWith(\"~\")) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\treason: `Invalid biu:// path ${JSON.stringify(uri)}: use a workspace-relative path like ${BIU_URI_SCHEME}${BIU_SPEC_FILE_NAME}.`,\n\t\t};\n\t}\n\tconst segments = rest.split(\"/\").filter((segment) => segment !== \"\" && segment !== \".\");\n\tif (segments.some((segment) => segment === \"..\")) {\n\t\treturn { ok: false, reason: `Invalid biu:// path ${JSON.stringify(uri)}: \"..\" segments are not allowed.` };\n\t}\n\tconst paths = getBiuPaths(cwd, agentDir);\n\treturn { ok: true, path: segments.length === 0 ? paths.root : join(paths.root, ...segments) };\n}\n\n/** Format a workspace-relative location as a biu:// URI for prompts and messages. */\nexport function toBiuUri(...segments: string[]): string {\n\treturn `${BIU_URI_SCHEME}${segments.join(\"/\")}`;\n}\n\n/** Whether the content's frontmatter says status: ready. Parse failures count as not ready. */\nfunction isSpecContentReady(content: string): boolean {\n\ttry {\n\t\treturn parseFrontmatter(content).frontmatter.status === \"ready\";\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** Normalize edit-tool input into Edit[], accepting the legacy top-level oldText/newText shape. */\nfunction toSpecEdits(input: Record<string, unknown>): Edit[] {\n\tconst edits: Edit[] = [];\n\tif (Array.isArray(input.edits)) {\n\t\tfor (const entry of input.edits) {\n\t\t\tif (!entry || typeof entry !== \"object\") continue;\n\t\t\tconst candidate = entry as Record<string, unknown>;\n\t\t\tif (typeof candidate.oldText === \"string\" && typeof candidate.newText === \"string\") {\n\t\t\t\tedits.push({ oldText: candidate.oldText, newText: candidate.newText });\n\t\t\t}\n\t\t}\n\t}\n\tif (typeof input.oldText === \"string\" && typeof input.newText === \"string\") {\n\t\tedits.push({ oldText: input.oldText, newText: input.newText });\n\t}\n\treturn edits;\n}\n\n/**\n * Whether a write/edit tool call would flip the SPEC's frontmatter status from\n * non-ready to ready — the transition that requires user approval. Edits are\n * simulated with the edit tool's own replacement semantics. Any failure\n * (unmatched oldText, invalid YAML) returns false: the gate steps aside and\n * lets the real tool report the error.\n */\nexport function detectSpecReadyTransition(\n\tcurrentContent: string | null,\n\ttoolName: \"write\" | \"edit\",\n\tinput: Record<string, unknown>,\n): boolean {\n\tif (currentContent !== null && isSpecContentReady(currentContent)) return false;\n\tif (toolName === \"write\") {\n\t\treturn typeof input.content === \"string\" && isSpecContentReady(input.content);\n\t}\n\tif (currentContent === null) return false;\n\tconst edits = toSpecEdits(input);\n\tif (edits.length === 0) return false;\n\ttry {\n\t\tconst { newContent } = applyEditsToNormalizedContent(normalizeToLF(currentContent), edits, BIU_SPEC_FILE_NAME);\n\t\treturn isSpecContentReady(newContent);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport interface BiuSpecInfo {\n\texists: boolean;\n\t/** \"unknown\" when the file exists but frontmatter is missing or invalid. */\n\tstatus: BiuSpecStatus | \"unknown\";\n\ttitle: string | null;\n\tbaselineCommit: string | null;\n\t/** Execution path chosen in the plan stage; null when absent or invalid. */\n\texecution: BiuExecutionMode | null;\n}\n\nexport interface BiuTaskInfo {\n\t/** Task id = file name without the .md extension, e.g. \"TASK-alias\". */\n\tid: string;\n\ttitle: string;\n\t/** \"unknown\" when frontmatter is missing or invalid; treated as neither ready nor completed. */\n\tstatus: BiuTaskStatus | \"unknown\";\n\tdependsOn: string[];\n}\n\nexport interface BiuTaskCounts {\n\ttotal: number;\n\tready: number;\n\tinProgress: number;\n\tcompleted: number;\n\tunknown: number;\n}\n\nexport type BiuFocus =\n\t| { kind: \"active\"; task: BiuTaskInfo }\n\t| { kind: \"next\"; task: BiuTaskInfo }\n\t| { kind: \"allDone\" }\n\t| { kind: \"none\" };\n\nexport interface BiuSnapshot {\n\tstage: BiuStage;\n\tspec: BiuSpecInfo;\n\tsummaryExists: boolean;\n\ttasks: BiuTaskInfo[];\n\tcounts: BiuTaskCounts;\n\tfocus: BiuFocus;\n\t/** Human-readable notes about unreadable or malformed workspace files. */\n\tproblems: string[];\n}\n\nfunction isMissingFileError(error: unknown): boolean {\n\treturn error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n}\n\nfunction describeError(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n\ttry {\n\t\tawait stat(path);\n\t\treturn true;\n\t} catch (error) {\n\t\tif (isMissingFileError(error)) return false;\n\t\tthrow error;\n\t}\n}\n\nfunction asTrimmedString(value: unknown): string | null {\n\tif (typeof value !== \"string\") return null;\n\tconst trimmed = value.trim();\n\treturn trimmed || null;\n}\n\nfunction parseStatus<T extends string>(value: unknown, allowed: readonly T[]): T | \"unknown\" {\n\treturn typeof value === \"string\" && (allowed as readonly string[]).includes(value) ? (value as T) : \"unknown\";\n}\n\n/** Strip an optional .md extension so depends_on accepts \"TASK-x\" and \"TASK-x.md\" alike. */\nfunction toTaskId(reference: string): string {\n\treturn reference.endsWith(\".md\") ? reference.slice(0, -3) : reference;\n}\n\nasync function readSpecInfo(specFile: string, problems: string[]): Promise<BiuSpecInfo> {\n\tlet raw: string;\n\ttry {\n\t\traw = await readFile(specFile, \"utf8\");\n\t} catch (error) {\n\t\tif (!isMissingFileError(error)) problems.push(`${BIU_SPEC_FILE_NAME}: ${describeError(error)}`);\n\t\treturn { exists: false, status: \"unknown\", title: null, baselineCommit: null, execution: null };\n\t}\n\ttry {\n\t\tconst { frontmatter } = parseFrontmatter(raw);\n\t\tconst status = parseStatus(frontmatter.status, BIU_SPEC_STATUSES);\n\t\tif (status === \"unknown\") {\n\t\t\tproblems.push(`${BIU_SPEC_FILE_NAME}: frontmatter \"status\" must be one of ${BIU_SPEC_STATUSES.join(\", \")}.`);\n\t\t}\n\t\tlet execution: BiuExecutionMode | null = null;\n\t\tif (frontmatter.execution !== undefined) {\n\t\t\tconst parsed = parseStatus(frontmatter.execution, BIU_EXECUTION_MODES);\n\t\t\tif (parsed === \"unknown\") {\n\t\t\t\tproblems.push(\n\t\t\t\t\t`${BIU_SPEC_FILE_NAME}: frontmatter \"execution\" must be one of ${BIU_EXECUTION_MODES.join(\", \")}.`,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\texecution = parsed;\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\texists: true,\n\t\t\tstatus,\n\t\t\ttitle: asTrimmedString(frontmatter.title),\n\t\t\tbaselineCommit: asTrimmedString(frontmatter.baseline_commit),\n\t\t\texecution,\n\t\t};\n\t} catch (error) {\n\t\tproblems.push(`${BIU_SPEC_FILE_NAME}: invalid frontmatter (${describeError(error)})`);\n\t\treturn { exists: true, status: \"unknown\", title: null, baselineCommit: null, execution: null };\n\t}\n}\n\nasync function readTaskInfo(tasksDir: string, fileName: string, problems: string[]): Promise<BiuTaskInfo> {\n\tconst id = toTaskId(fileName);\n\tconst relative = `${BIU_TASKS_DIRECTORY_NAME}/${fileName}`;\n\tlet raw: string;\n\ttry {\n\t\traw = await readFile(join(tasksDir, fileName), \"utf8\");\n\t} catch (error) {\n\t\tproblems.push(`${relative}: ${describeError(error)}`);\n\t\treturn { id, title: id, status: \"unknown\", dependsOn: [] };\n\t}\n\ttry {\n\t\tconst { frontmatter } = parseFrontmatter(raw);\n\t\tconst status = parseStatus(frontmatter.status, BIU_TASK_STATUSES);\n\t\tif (status === \"unknown\") {\n\t\t\tproblems.push(`${relative}: frontmatter \"status\" must be one of ${BIU_TASK_STATUSES.join(\", \")}.`);\n\t\t}\n\t\tconst dependsOn = Array.isArray(frontmatter.depends_on)\n\t\t\t? frontmatter.depends_on.filter((dep): dep is string => typeof dep === \"string\").map(toTaskId)\n\t\t\t: [];\n\t\treturn { id, title: asTrimmedString(frontmatter.title) ?? id, status, dependsOn };\n\t} catch (error) {\n\t\tproblems.push(`${relative}: invalid frontmatter (${describeError(error)})`);\n\t\treturn { id, title: id, status: \"unknown\", dependsOn: [] };\n\t}\n}\n\nfunction countTasks(tasks: BiuTaskInfo[]): BiuTaskCounts {\n\tconst counts: BiuTaskCounts = { total: tasks.length, ready: 0, inProgress: 0, completed: 0, unknown: 0 };\n\tfor (const task of tasks) {\n\t\tif (task.status === \"ready\") counts.ready++;\n\t\telse if (task.status === \"in_progress\") counts.inProgress++;\n\t\telse if (task.status === \"completed\") counts.completed++;\n\t\telse counts.unknown++;\n\t}\n\treturn counts;\n}\n\n/** What to work on next: the in-progress task, else the first ready task whose dependencies are all completed. */\nexport function getBiuFocus(tasks: BiuTaskInfo[]): BiuFocus {\n\tconst active = tasks.find((task) => task.status === \"in_progress\");\n\tif (active) return { kind: \"active\", task: active };\n\tconst byId = new Map(tasks.map((task) => [task.id, task]));\n\tconst next = tasks.find(\n\t\t(task) => task.status === \"ready\" && task.dependsOn.every((dep) => byId.get(dep)?.status === \"completed\"),\n\t);\n\tif (next) return { kind: \"next\", task: next };\n\tif (tasks.length > 0 && tasks.every((task) => task.status === \"completed\")) return { kind: \"allDone\" };\n\treturn { kind: \"none\" };\n}\n\nfunction deriveStage(spec: BiuSpecInfo, tasks: BiuTaskInfo[], summaryExists: boolean): BiuStage {\n\tif (summaryExists) return \"archive\";\n\tif (!spec.exists || spec.status !== \"ready\") return \"plan\";\n\tif (tasks.length > 0 && tasks.every((task) => task.status === \"completed\")) return \"archive\";\n\treturn \"execute\";\n}\n\n/**\n * Build the workspace snapshot by reading frontmatter from SPEC.md and\n * tasks/*.md. Content problems (unreadable files, malformed frontmatter) are\n * reported in `problems` and degrade to \"unknown\" instead of throwing.\n */\nexport async function loadBiuSnapshot(cwd: string, agentDir: string = getAgentDir()): Promise<BiuSnapshot> {\n\tconst paths = getBiuPaths(cwd, agentDir);\n\tconst problems: string[] = [];\n\n\tconst spec = await readSpecInfo(paths.specFile, problems);\n\n\tlet summaryExists = false;\n\ttry {\n\t\tsummaryExists = await pathExists(paths.summaryFile);\n\t} catch (error) {\n\t\tproblems.push(`${BIU_SUMMARY_FILE_NAME}: ${describeError(error)}`);\n\t}\n\n\tlet taskFiles: string[] = [];\n\ttry {\n\t\ttaskFiles = (await readdir(paths.tasksDir)).filter((name) => name.endsWith(\".md\")).sort();\n\t} catch (error) {\n\t\tif (!isMissingFileError(error)) problems.push(`${BIU_TASKS_DIRECTORY_NAME}/: ${describeError(error)}`);\n\t}\n\tif (taskFiles.length > BIU_MAX_SNAPSHOT_TASKS) {\n\t\tproblems.push(\n\t\t\t`${BIU_TASKS_DIRECTORY_NAME}/: ${taskFiles.length} task files exceed the snapshot limit of ${BIU_MAX_SNAPSHOT_TASKS}; only the first ${BIU_MAX_SNAPSHOT_TASKS} are listed.`,\n\t\t);\n\t\ttaskFiles = taskFiles.slice(0, BIU_MAX_SNAPSHOT_TASKS);\n\t}\n\tconst tasks: BiuTaskInfo[] = [];\n\tfor (const fileName of taskFiles) {\n\t\ttasks.push(await readTaskInfo(paths.tasksDir, fileName, problems));\n\t}\n\n\treturn {\n\t\tstage: deriveStage(spec, tasks, summaryExists),\n\t\tspec,\n\t\tsummaryExists,\n\t\ttasks,\n\t\tcounts: countTasks(tasks),\n\t\tfocus: getBiuFocus(tasks),\n\t\tproblems,\n\t};\n}\n\nexport function sanitizeShortname(shortname: string): string {\n\tconst printable = Array.from(shortname.trim())\n\t\t.filter((ch) => {\n\t\t\tconst code = ch.codePointAt(0) ?? 0;\n\t\t\treturn code >= 0x20 && code !== 0x7f;\n\t\t})\n\t\t.join(\"\");\n\tconst cleaned = printable\n\t\t.replace(/[<>:\"/\\\\|?*]+/g, \"-\")\n\t\t.replace(/\\s+/g, \"-\")\n\t\t.replace(/-{2,}/g, \"-\")\n\t\t.replace(/^[-.]+|[-.]+$/g, \"\");\n\treturn cleaned.slice(0, BIU_MAX_SHORTNAME_LENGTH).replace(/^[-.]+|[-.]+$/g, \"\");\n}\n\n/** Signature for archive file moves; injectable for tests. */\nexport type BiuMoveFile = (from: string, to: string) => Promise<boolean>;\n\n/** Move one file or directory, returning false when the source does not exist. */\nasync function moveIfExists(from: string, to: string): Promise<boolean> {\n\tif (!(await pathExists(from))) return false;\n\tawait rename(from, to);\n\treturn true;\n}\n\nexport interface BiuArchiveResult {\n\tarchivedPath: string;\n\tarchiveName: string;\n}\n\n/**\n * Close the current cycle: move SPEC.md, Summary.md, and tasks/ into\n * archived/YYYY-MM-DD-<shortname>/. If any move fails, already-moved files are\n * rolled back so the cycle is never left half-archived; when a rollback itself\n * fails, the recovery data stays in the archive directory and the error says\n * where. Requires SPEC.md and Summary.md to exist so an empty or unfinished\n * draft is never archived silently.\n *\n * `move` is injectable for tests; rollback always uses the real move.\n */\nexport async function archiveBiuCycle(\n\tcwd: string,\n\tshortname: string,\n\tagentDir: string = getAgentDir(),\n\tnow: Date = new Date(),\n\tmove: BiuMoveFile = moveIfExists,\n): Promise<BiuArchiveResult> {\n\tconst paths = getBiuPaths(cwd, agentDir);\n\tconst safeName = sanitizeShortname(shortname);\n\tif (!safeName) throw new Error(`Archive shortname ${JSON.stringify(shortname)} is empty after sanitization.`);\n\tif (!(await pathExists(paths.specFile))) {\n\t\tthrow new Error(\"Cannot archive: SPEC.md does not exist in the Biu workspace.\");\n\t}\n\tif (!(await pathExists(paths.summaryFile))) {\n\t\tthrow new Error(\"Cannot archive: Summary.md does not exist yet. Draft and confirm the summary first.\");\n\t}\n\n\tawait mkdir(paths.archivedDir, { recursive: true });\n\tconst date = now.toISOString().slice(0, 10);\n\tconst baseName = `${date}-${safeName}`;\n\tlet existing: string[];\n\ttry {\n\t\texisting = await readdir(paths.archivedDir);\n\t} catch {\n\t\texisting = [];\n\t}\n\tlet archiveName = baseName;\n\tfor (let suffix = 2; existing.includes(archiveName); suffix++) {\n\t\tarchiveName = `${baseName}-${String(suffix).padStart(2, \"0\")}`;\n\t}\n\tconst archivedPath = join(paths.archivedDir, archiveName);\n\tawait mkdir(archivedPath, { recursive: false });\n\n\tconst moves: Array<{ from: string; to: string }> = [\n\t\t{ from: paths.specFile, to: join(archivedPath, BIU_SPEC_FILE_NAME) },\n\t\t{ from: paths.summaryFile, to: join(archivedPath, BIU_SUMMARY_FILE_NAME) },\n\t\t{ from: paths.tasksDir, to: join(archivedPath, BIU_TASKS_DIRECTORY_NAME) },\n\t];\n\tconst moved: Array<{ from: string; to: string }> = [];\n\ttry {\n\t\tfor (const entry of moves) {\n\t\t\tif (await move(entry.from, entry.to)) moved.push(entry);\n\t\t}\n\t} catch (error) {\n\t\tthrow await rollbackArchive(error, archivedPath, moved);\n\t}\n\t// Recreate the empty tasks/ directory for the next cycle. Best-effort: the\n\t// next ensureBiuWorkspace call recreates it anyway.\n\tawait mkdir(paths.tasksDir, { recursive: true }).catch(() => {});\n\treturn { archivedPath, archiveName };\n}\n\n/**\n * Reverse the already-moved files and clean the archive directory. Returns\n * the original error unchanged when the rollback fully succeeded, otherwise\n * a chained error whose message says exactly what remains inconsistent.\n */\nasync function rollbackArchive(\n\terror: unknown,\n\tarchivedPath: string,\n\tmoved: Array<{ from: string; to: string }>,\n): Promise<Error> {\n\tconst restoreErrors: string[] = [];\n\tfor (const entry of [...moved].reverse()) {\n\t\ttry {\n\t\t\tawait moveIfExists(entry.to, entry.from);\n\t\t} catch (restoreError) {\n\t\t\trestoreErrors.push(`moving ${entry.to} back failed: ${describeError(restoreError)}`);\n\t\t}\n\t}\n\tif (restoreErrors.length > 0) {\n\t\treturn new Error(\n\t\t\t`${describeError(error)} Rollback incomplete, recovery data left in ${archivedPath}: ${restoreErrors.join(\"; \")}`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n\ttry {\n\t\tawait rm(archivedPath, { recursive: true, force: true });\n\t} catch (cleanupError) {\n\t\treturn new Error(\n\t\t\t`${describeError(error)} Rollback succeeded, but removing the empty ${archivedPath} directory failed: ${describeError(cleanupError)}`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n\treturn error instanceof Error ? error : new Error(describeError(error), { cause: error });\n}\n"]}
@@ -1,29 +0,0 @@
1
- /**
2
- * anchor.ts — pure helper for choosing a snapshot frame's turn anchor.
3
- *
4
- * A frame records the work tree as of the moment its agent run started
5
- * (beginTurn at before_agent_start). The entry representing that moment is the
6
- * FIRST user entry the run appended — not the last user entry on the branch:
7
- * steering / follow-up messages consumed inside the same run append later user
8
- * entries, and anchoring to those would attach "start of run" file state to a
9
- * mid-run conversation point.
10
- *
11
- * No Pi imports, so the module stays node-testable (see storage.ts).
12
- */
13
- /** Structural view of a branch entry (SessionEntry fits loosely). */
14
- export interface BranchEntryLike {
15
- id: string;
16
- type: string;
17
- message?: {
18
- role?: string;
19
- };
20
- }
21
- /**
22
- * The id of the first user message after `scanStartId` in `branch`
23
- * (root → leaf order). `scanStartId === null` scans from the root (fresh
24
- * session). Returns undefined when `scanStartId` is not on the branch (the
25
- * leaf moved across branches since it was recorded) — anchoring against an
26
- * unrelated prefix would mislabel the frame, so callers should discard it.
27
- */
28
- export declare function firstUserEntryIdAfter(branch: readonly BranchEntryLike[], scanStartId: string | null): string | undefined;
29
- //# sourceMappingURL=anchor.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"anchor.d.ts","sourceRoot":"","sources":["../../../src/extensions/rewind/anchor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,qEAAqE;AACrE,MAAM,WAAW,eAAe;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,WAAW,EAAE,MAAM,GAAG,IAAI,GACxB,MAAM,GAAG,SAAS,CAYpB"}
@@ -1,35 +0,0 @@
1
- /**
2
- * anchor.ts — pure helper for choosing a snapshot frame's turn anchor.
3
- *
4
- * A frame records the work tree as of the moment its agent run started
5
- * (beginTurn at before_agent_start). The entry representing that moment is the
6
- * FIRST user entry the run appended — not the last user entry on the branch:
7
- * steering / follow-up messages consumed inside the same run append later user
8
- * entries, and anchoring to those would attach "start of run" file state to a
9
- * mid-run conversation point.
10
- *
11
- * No Pi imports, so the module stays node-testable (see storage.ts).
12
- */
13
- /**
14
- * The id of the first user message after `scanStartId` in `branch`
15
- * (root → leaf order). `scanStartId === null` scans from the root (fresh
16
- * session). Returns undefined when `scanStartId` is not on the branch (the
17
- * leaf moved across branches since it was recorded) — anchoring against an
18
- * unrelated prefix would mislabel the frame, so callers should discard it.
19
- */
20
- export function firstUserEntryIdAfter(branch, scanStartId) {
21
- let start = 0;
22
- if (scanStartId !== null) {
23
- const idx = branch.findIndex((e) => e.id === scanStartId);
24
- if (idx < 0)
25
- return undefined;
26
- start = idx + 1;
27
- }
28
- for (let i = start; i < branch.length; i++) {
29
- const e = branch[i];
30
- if (e.type === "message" && e.message?.role === "user")
31
- return e.id;
32
- }
33
- return undefined;
34
- }
35
- //# sourceMappingURL=anchor.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"anchor.js","sourceRoot":"","sources":["../../../src/extensions/rewind/anchor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACpC,MAAkC,EAClC,WAA0B;IAE1B,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC;QAC1D,IAAI,GAAG,GAAG,CAAC;YAAE,OAAO,SAAS,CAAC;QAC9B,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC;IACjB,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,OAAO,EAAE,IAAI,KAAK,MAAM;YAAE,OAAO,CAAC,CAAC,EAAE,CAAC;IACrE,CAAC;IACD,OAAO,SAAS,CAAC;AAClB,CAAC","sourcesContent":["/**\n * anchor.ts — pure helper for choosing a snapshot frame's turn anchor.\n *\n * A frame records the work tree as of the moment its agent run started\n * (beginTurn at before_agent_start). The entry representing that moment is the\n * FIRST user entry the run appended — not the last user entry on the branch:\n * steering / follow-up messages consumed inside the same run append later user\n * entries, and anchoring to those would attach \"start of run\" file state to a\n * mid-run conversation point.\n *\n * No Pi imports, so the module stays node-testable (see storage.ts).\n */\n\n/** Structural view of a branch entry (SessionEntry fits loosely). */\nexport interface BranchEntryLike {\n\tid: string;\n\ttype: string;\n\tmessage?: { role?: string };\n}\n\n/**\n * The id of the first user message after `scanStartId` in `branch`\n * (root → leaf order). `scanStartId === null` scans from the root (fresh\n * session). Returns undefined when `scanStartId` is not on the branch (the\n * leaf moved across branches since it was recorded) — anchoring against an\n * unrelated prefix would mislabel the frame, so callers should discard it.\n */\nexport function firstUserEntryIdAfter(\n\tbranch: readonly BranchEntryLike[],\n\tscanStartId: string | null,\n): string | undefined {\n\tlet start = 0;\n\tif (scanStartId !== null) {\n\t\tconst idx = branch.findIndex((e) => e.id === scanStartId);\n\t\tif (idx < 0) return undefined;\n\t\tstart = idx + 1;\n\t}\n\tfor (let i = start; i < branch.length; i++) {\n\t\tconst e = branch[i];\n\t\tif (e.type === \"message\" && e.message?.role === \"user\") return e.id;\n\t}\n\treturn undefined;\n}\n"]}
@@ -1,16 +0,0 @@
1
- export interface RewindConfig {
2
- /** Master switch. When false, no backups are taken and rewind is inert. */
3
- enabled: boolean;
4
- /** Backups for sessions whose dir is older than this are GC'd. 0 = keep forever. */
5
- retentionDays: number;
6
- /** Cap on retained snapshots per session. */
7
- maxSnapshots: number;
8
- }
9
- export declare const DEFAULT_CONFIG: RewindConfig;
10
- /** Cached config (reads disk once, then serves memory until reload/save). */
11
- export declare function loadRewindConfig(): RewindConfig;
12
- /** Force a disk re-read (call on session_start so external edits take effect). */
13
- export declare function reloadRewindConfig(): RewindConfig;
14
- /** Persist and update the in-memory cache so the next turn sees the change. */
15
- export declare function saveRewindConfig(config: RewindConfig): boolean;
16
- //# sourceMappingURL=config.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/extensions/rewind/config.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,YAAY;IAC5B,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC;IACjB,oFAAoF;IACpF,aAAa,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,cAAc,EAAE,YAI5B,CAAC;AA8BF,6EAA6E;AAC7E,wBAAgB,gBAAgB,IAAI,YAAY,CAG/C;AAED,kFAAkF;AAClF,wBAAgB,kBAAkB,IAAI,YAAY,CAGjD;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAW9D"}
@@ -1,69 +0,0 @@
1
- /**
2
- * config.ts — load/save the rewind extension's settings at
3
- * <rewindDir>/config.json. Tolerant parse, atomic-ish write, sensible defaults
4
- * so a missing/corrupt file never breaks the session.
5
- *
6
- * Settings are user-editable via the /rewind menu (menu.ts).
7
- */
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
- import { dirname } from "node:path";
10
- import { rewindConfigPath } from "./paths.js";
11
- export const DEFAULT_CONFIG = {
12
- enabled: true,
13
- retentionDays: 30,
14
- maxSnapshots: 100,
15
- };
16
- /** In-process cache so lifecycle hooks do not re-read config.json every turn. */
17
- let cached = null;
18
- function clampInt(value, min, max, fallback) {
19
- if (typeof value !== "number" || !Number.isFinite(value))
20
- return fallback;
21
- return Math.min(max, Math.max(min, Math.round(value)));
22
- }
23
- function normalize(raw) {
24
- if (!raw || typeof raw !== "object")
25
- return { ...DEFAULT_CONFIG };
26
- const r = raw;
27
- return {
28
- enabled: typeof r.enabled === "boolean" ? r.enabled : DEFAULT_CONFIG.enabled,
29
- retentionDays: clampInt(r.retentionDays, 0, 3650, DEFAULT_CONFIG.retentionDays),
30
- maxSnapshots: clampInt(r.maxSnapshots, 1, 1000, DEFAULT_CONFIG.maxSnapshots),
31
- };
32
- }
33
- function readConfigFromDisk() {
34
- const configPath = rewindConfigPath();
35
- if (!existsSync(configPath))
36
- return { ...DEFAULT_CONFIG };
37
- try {
38
- return normalize(JSON.parse(readFileSync(configPath, "utf8")));
39
- }
40
- catch {
41
- return { ...DEFAULT_CONFIG };
42
- }
43
- }
44
- /** Cached config (reads disk once, then serves memory until reload/save). */
45
- export function loadRewindConfig() {
46
- if (!cached)
47
- cached = readConfigFromDisk();
48
- return cached;
49
- }
50
- /** Force a disk re-read (call on session_start so external edits take effect). */
51
- export function reloadRewindConfig() {
52
- cached = readConfigFromDisk();
53
- return cached;
54
- }
55
- /** Persist and update the in-memory cache so the next turn sees the change. */
56
- export function saveRewindConfig(config) {
57
- const next = normalize(config);
58
- const configPath = rewindConfigPath();
59
- try {
60
- mkdirSync(dirname(configPath), { recursive: true });
61
- writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
62
- cached = next;
63
- return true;
64
- }
65
- catch {
66
- return false;
67
- }
68
- }
69
- //# sourceMappingURL=config.js.map