holistic 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/CONTRIBUTING.md +121 -0
  3. package/LICENSE +21 -0
  4. package/README.md +329 -0
  5. package/bin/holistic +17 -0
  6. package/bin/holistic.cmd +23 -0
  7. package/bin/holistic.js +59 -0
  8. package/dist/__tests__/mcp-notification.test.d.ts +5 -0
  9. package/dist/__tests__/mcp-notification.test.d.ts.map +1 -0
  10. package/dist/__tests__/mcp-notification.test.js +255 -0
  11. package/dist/__tests__/mcp-notification.test.js.map +1 -0
  12. package/dist/cli.d.ts +6 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +637 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/core/docs.d.ts +3 -0
  17. package/dist/core/docs.d.ts.map +1 -0
  18. package/dist/core/docs.js +602 -0
  19. package/dist/core/docs.js.map +1 -0
  20. package/dist/core/git-hooks.d.ts +17 -0
  21. package/dist/core/git-hooks.d.ts.map +1 -0
  22. package/dist/core/git-hooks.js +144 -0
  23. package/dist/core/git-hooks.js.map +1 -0
  24. package/dist/core/git.d.ts +12 -0
  25. package/dist/core/git.d.ts.map +1 -0
  26. package/dist/core/git.js +121 -0
  27. package/dist/core/git.js.map +1 -0
  28. package/dist/core/lock.d.ts +2 -0
  29. package/dist/core/lock.d.ts.map +1 -0
  30. package/dist/core/lock.js +40 -0
  31. package/dist/core/lock.js.map +1 -0
  32. package/dist/core/redact.d.ts +3 -0
  33. package/dist/core/redact.d.ts.map +1 -0
  34. package/dist/core/redact.js +13 -0
  35. package/dist/core/redact.js.map +1 -0
  36. package/dist/core/setup.d.ts +35 -0
  37. package/dist/core/setup.d.ts.map +1 -0
  38. package/dist/core/setup.js +654 -0
  39. package/dist/core/setup.js.map +1 -0
  40. package/dist/core/splash.d.ts +9 -0
  41. package/dist/core/splash.d.ts.map +1 -0
  42. package/dist/core/splash.js +35 -0
  43. package/dist/core/splash.js.map +1 -0
  44. package/dist/core/state.d.ts +42 -0
  45. package/dist/core/state.d.ts.map +1 -0
  46. package/dist/core/state.js +744 -0
  47. package/dist/core/state.js.map +1 -0
  48. package/dist/core/sync.d.ts +12 -0
  49. package/dist/core/sync.d.ts.map +1 -0
  50. package/dist/core/sync.js +106 -0
  51. package/dist/core/sync.js.map +1 -0
  52. package/dist/core/types.d.ts +210 -0
  53. package/dist/core/types.d.ts.map +1 -0
  54. package/dist/core/types.js +2 -0
  55. package/dist/core/types.js.map +1 -0
  56. package/dist/daemon.d.ts +7 -0
  57. package/dist/daemon.d.ts.map +1 -0
  58. package/dist/daemon.js +242 -0
  59. package/dist/daemon.js.map +1 -0
  60. package/dist/mcp-server.d.ts +11 -0
  61. package/dist/mcp-server.d.ts.map +1 -0
  62. package/dist/mcp-server.js +266 -0
  63. package/dist/mcp-server.js.map +1 -0
  64. package/docs/handoff-walkthrough.md +119 -0
  65. package/docs/structured-metadata.md +341 -0
  66. package/package.json +67 -0
package/dist/cli.js ADDED
@@ -0,0 +1,637 @@
1
+ import path from "node:path";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin as input, stdout as output } from "node:process";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { captureRepoSnapshot, clearPendingCommit, writePendingCommit } from './core/git.js';
6
+ import { writeDerivedDocs } from './core/docs.js';
7
+ import { bootstrapHolistic, initializeHolistic, refreshHolisticHooks } from './core/setup.js';
8
+ import { printSplash, printSplashError, renderSplash } from './core/splash.js';
9
+ import { requestAutoSync } from './core/sync.js';
10
+ import { runDaemonTick } from './daemon.js';
11
+ import { applyHandoff, clearDraftHandoff, checkpointState, computeSessionDiff, continueFromLatest, draftHandoffFile, getResumePayload, getRuntimePaths, loadSessionById, loadState, readDraftHandoff, saveState, startNewSession, withStateLock, } from './core/state.js';
12
+ function parseArgs(argv) {
13
+ const [command = "help", ...rest] = argv;
14
+ const flags = {};
15
+ for (let index = 0; index < rest.length; index += 1) {
16
+ const token = rest[index];
17
+ if (!token.startsWith("--")) {
18
+ continue;
19
+ }
20
+ const flag = token.slice(2);
21
+ const next = rest[index + 1];
22
+ if (!next || next.startsWith("--")) {
23
+ flags[flag] = ["true"];
24
+ continue;
25
+ }
26
+ flags[flag] ??= [];
27
+ flags[flag].push(next);
28
+ index += 1;
29
+ }
30
+ return { command, flags };
31
+ }
32
+ function firstFlag(flags, name, fallback = "") {
33
+ return flags[name]?.[0] ?? fallback;
34
+ }
35
+ function listFlag(flags, name) {
36
+ return flags[name] ?? [];
37
+ }
38
+ function asAgent(value) {
39
+ const validAgents = [
40
+ "codex",
41
+ "claude",
42
+ "antigravity",
43
+ "gemini",
44
+ "copilot",
45
+ "cursor",
46
+ "goose",
47
+ "gsd",
48
+ ];
49
+ if (validAgents.includes(value)) {
50
+ return value;
51
+ }
52
+ return "unknown";
53
+ }
54
+ function printJson(payload) {
55
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
56
+ }
57
+ function printHelp() {
58
+ process.stdout.write(`Holistic CLI
59
+
60
+ Usage:
61
+ holistic init [--install-daemon] [--install-hooks] [--platform win32|darwin|linux] [--interval 30] [--remote origin] [--state-ref refs/holistic/state] [--state-branch holistic/state]
62
+ holistic bootstrap [--platform win32|darwin|linux] [--interval 30] [--remote origin] [--state-ref refs/holistic/state] [--state-branch holistic/state] [--install-daemon false] [--install-hooks false] [--configure-mcp false]
63
+ holistic start [--agent codex|claude|antigravity|gemini|copilot|cursor|goose|gsd] [--continue] [--json]
64
+ holistic resume [--agent codex|claude|antigravity|gemini|copilot|cursor|goose|gsd] [--continue] [--json]
65
+ holistic checkpoint --reason "<reason>" [--goal "<goal>"] [--status "<status>"] [--plan "<step>"]...
66
+ holistic checkpoint --fixed "<bug>" [--fix-files "<file>"] [--fix-risk "<what reintroduces it>"]
67
+ holistic handoff [--draft] [--summary "<summary>"] [--next "<step>"]...
68
+ holistic start-new --goal "<goal>" [--title "<title>"] [--plan "<step>"]...
69
+ holistic status
70
+ holistic diff --from "<session-id>" --to "<session-id>" [--format text|json]
71
+ holistic serve
72
+ holistic watch [--agent codex|claude|antigravity|gemini|copilot|cursor|goose|gsd] [--interval 60]
73
+
74
+ 'holistic start' is an alias for 'holistic resume'.
75
+ `);
76
+ }
77
+ export function renderResumeOutput(body) {
78
+ return `${renderSplash({ message: "loading project recap..." })}${body}`;
79
+ }
80
+ function reportHookWarnings(warnings) {
81
+ for (const warning of warnings) {
82
+ process.stderr.write(`${warning}\n`);
83
+ }
84
+ }
85
+ function refreshHooksBeforeCommand(rootDir) {
86
+ const hookResult = refreshHolisticHooks(rootDir);
87
+ reportHookWarnings(hookResult.warnings);
88
+ }
89
+ function persistLocked(rootDir, state, paths) {
90
+ writeDerivedDocs(paths, state);
91
+ state.repoSnapshot = captureRepoSnapshot(rootDir);
92
+ saveState(paths, state, { locked: true });
93
+ return state;
94
+ }
95
+ function mutateState(rootDir, mutator) {
96
+ const paths = getRuntimePaths(rootDir);
97
+ return withStateLock(paths, () => {
98
+ const { state, paths: lockedPaths } = loadState(rootDir);
99
+ const nextState = mutator(state, lockedPaths);
100
+ return persistLocked(rootDir, nextState, lockedPaths);
101
+ });
102
+ }
103
+ function runtimeScript(name) {
104
+ const currentFile = fileURLToPath(import.meta.url);
105
+ const extension = path.extname(currentFile);
106
+ const runtimeDir = path.dirname(currentFile);
107
+ const useStripTypes = extension === ".ts";
108
+ return {
109
+ scriptPath: path.resolve(runtimeDir, `${name}${useStripTypes ? ".ts" : ".js"}`),
110
+ useStripTypes,
111
+ };
112
+ }
113
+ async function ask(question, fallback = "", rl) {
114
+ const shouldClose = !rl;
115
+ const readline = rl || createInterface({ input, output });
116
+ const suffix = fallback ? ` [${fallback}]` : "";
117
+ const answer = await readline.question(`${question}${suffix}: `);
118
+ if (shouldClose) {
119
+ readline.close();
120
+ }
121
+ return answer.trim() || fallback;
122
+ }
123
+ async function promptList(question, fallback, rl) {
124
+ const joined = fallback.join(" | ");
125
+ const answer = await ask(`${question} (separate with |)`, joined, rl);
126
+ return answer
127
+ .split("|")
128
+ .map((item) => item.trim())
129
+ .filter(Boolean);
130
+ }
131
+ function pickList(primary, fallback) {
132
+ return primary && primary.length > 0 ? primary : (fallback ?? []);
133
+ }
134
+ function pickText(primary, fallback) {
135
+ return primary && primary.length > 0 ? primary : (fallback ?? "");
136
+ }
137
+ function draftMatchesSession(draft, sessionId) {
138
+ return Boolean(draft && draft.sourceSessionId === sessionId);
139
+ }
140
+ export function finalizeDraftHandoffInput(session, input) {
141
+ return {
142
+ ...input,
143
+ summary: pickText(input.summary, session.latestStatus || session.currentGoal),
144
+ done: pickList(input.done, session.workDone),
145
+ tried: pickList(input.tried, session.triedItems),
146
+ next: pickList(input.next, session.nextSteps),
147
+ assumptions: pickList(input.assumptions, session.assumptions),
148
+ blockers: pickList(input.blockers, session.blockers),
149
+ references: pickList(input.references, session.references),
150
+ impacts: pickList(input.impacts, session.impactNotes),
151
+ regressions: pickList(input.regressions, session.regressionRisks),
152
+ status: pickText(input.status, session.latestStatus),
153
+ };
154
+ }
155
+ function formatDurationDays(durationMs) {
156
+ return (durationMs / (1000 * 60 * 60 * 24)).toFixed(1);
157
+ }
158
+ export function renderDiff(fromSession, toSession, diff) {
159
+ const lines = [];
160
+ lines.push("Holistic Session Diff");
161
+ lines.push("");
162
+ lines.push(`FROM: ${fromSession.title} (${fromSession.id})`);
163
+ lines.push(`TO: ${toSession.title} (${toSession.id})`);
164
+ lines.push(`Time span: ${diff.timeSpan.from} -> ${diff.timeSpan.to} (${formatDurationDays(diff.timeSpan.durationMs)} days)`);
165
+ if (diff.goalChanged) {
166
+ lines.push("");
167
+ lines.push("Goal Changed:");
168
+ lines.push(` FROM: ${diff.fromGoal}`);
169
+ lines.push(` TO: ${diff.toGoal}`);
170
+ }
171
+ if (diff.newWork.length > 0) {
172
+ lines.push("");
173
+ lines.push("New Work:");
174
+ for (const item of diff.newWork) {
175
+ lines.push(` + ${item}`);
176
+ }
177
+ }
178
+ if (diff.newRegressions.length > 0) {
179
+ lines.push("");
180
+ lines.push("New Regression Risks:");
181
+ for (const item of diff.newRegressions) {
182
+ lines.push(` + ${item}`);
183
+ }
184
+ }
185
+ if (diff.clearedRegressions.length > 0) {
186
+ lines.push("");
187
+ lines.push("Cleared Regression Risks:");
188
+ for (const item of diff.clearedRegressions) {
189
+ lines.push(` - ${item}`);
190
+ }
191
+ }
192
+ if (diff.newBlockers.length > 0) {
193
+ lines.push("");
194
+ lines.push("New Blockers:");
195
+ for (const item of diff.newBlockers) {
196
+ lines.push(` + ${item}`);
197
+ }
198
+ }
199
+ if (diff.clearedBlockers.length > 0) {
200
+ lines.push("");
201
+ lines.push("Cleared Blockers:");
202
+ for (const item of diff.clearedBlockers) {
203
+ lines.push(` - ${item}`);
204
+ }
205
+ }
206
+ lines.push("");
207
+ lines.push(`File changes: +${diff.fileChanges.new.length} new, -${diff.fileChanges.removed.length} removed`);
208
+ if (diff.fileChanges.new.length > 0) {
209
+ for (const file of diff.fileChanges.new) {
210
+ lines.push(` + ${file}`);
211
+ }
212
+ }
213
+ if (diff.fileChanges.removed.length > 0) {
214
+ for (const file of diff.fileChanges.removed) {
215
+ lines.push(` - ${file}`);
216
+ }
217
+ }
218
+ return lines.join("\n") + "\n";
219
+ }
220
+ export function renderStatus(state) {
221
+ const lines = [];
222
+ lines.push("Holistic Status");
223
+ lines.push("");
224
+ if (!state.activeSession) {
225
+ lines.push("No active session.");
226
+ if (state.lastHandoff) {
227
+ lines.push("");
228
+ lines.push(`Last handoff: ${state.lastHandoff.summary}`);
229
+ lines.push(`Next action: ${state.lastHandoff.nextAction}`);
230
+ }
231
+ if (state.pendingWork.length > 0) {
232
+ lines.push("");
233
+ lines.push(`Pending work: ${state.pendingWork.length} item(s)`);
234
+ for (const item of state.pendingWork.slice(0, 3)) {
235
+ lines.push(` - ${item.title}`);
236
+ }
237
+ }
238
+ return lines.join("\n") + "\n";
239
+ }
240
+ const session = state.activeSession;
241
+ lines.push(`Session: ${session.id}`);
242
+ lines.push(`Title: ${session.title}`);
243
+ lines.push(`Agent: ${session.agent}`);
244
+ lines.push(`Branch: ${session.branch}`);
245
+ lines.push(`Started: ${session.startedAt}`);
246
+ lines.push(`Checkpoints: ${session.checkpointCount}`);
247
+ lines.push("");
248
+ lines.push(`Goal: ${session.currentGoal}`);
249
+ lines.push(`Status: ${session.latestStatus}`);
250
+ if (session.nextSteps.length > 0) {
251
+ lines.push("");
252
+ lines.push("Next steps:");
253
+ for (const step of session.nextSteps) {
254
+ lines.push(` - ${step}`);
255
+ }
256
+ }
257
+ if (session.blockers.length > 0) {
258
+ lines.push("");
259
+ lines.push("Blockers:");
260
+ for (const blocker of session.blockers) {
261
+ lines.push(` - ${blocker}`);
262
+ }
263
+ }
264
+ if (session.regressionRisks.length > 0) {
265
+ lines.push("");
266
+ lines.push(`Regression watch: ${session.regressionRisks.length} item(s)`);
267
+ }
268
+ lines.push("");
269
+ lines.push(`Changed files: ${session.changedFiles.length}`);
270
+ return lines.join("\n") + "\n";
271
+ }
272
+ async function handleInit(rootDir, parsed) {
273
+ printSplash({
274
+ message: "initializing shared memory layer...",
275
+ });
276
+ const platformFlag = firstFlag(parsed.flags, "platform", process.platform);
277
+ const platform = platformFlag === "windows" ? "win32" : platformFlag === "macos" ? "darwin" : platformFlag === "linux" ? "linux" : platformFlag;
278
+ const intervalSeconds = Number.parseInt(firstFlag(parsed.flags, "interval", "30"), 10);
279
+ const result = initializeHolistic(rootDir, {
280
+ installDaemon: firstFlag(parsed.flags, "install-daemon") === "true",
281
+ installGitHooks: firstFlag(parsed.flags, "install-hooks") === "true",
282
+ platform: platform,
283
+ intervalSeconds,
284
+ remote: firstFlag(parsed.flags, "remote", "origin"),
285
+ stateRef: firstFlag(parsed.flags, "state-ref"),
286
+ stateBranch: firstFlag(parsed.flags, "state-branch"),
287
+ });
288
+ reportHookWarnings(result.gitHookWarnings);
289
+ const statusItems = [];
290
+ statusItems.push("session state restored");
291
+ statusItems.push("project memory loaded");
292
+ if (result.gitHooksInstalled) {
293
+ statusItems.push("git hooks installed");
294
+ }
295
+ if (result.installed) {
296
+ statusItems.push("daemon configured");
297
+ }
298
+ statusItems.push("handoff ready");
299
+ printSplash({
300
+ showStatus: true,
301
+ statusItems,
302
+ });
303
+ process.stdout.write(`System files: ${result.systemDir}
304
+ Config: ${result.configFile}
305
+ Platform: ${result.platform}
306
+ Daemon install: ${result.installed ? `enabled at ${result.startupTarget}` : "not installed"}
307
+ `);
308
+ if (result.gitHooksInstalled) {
309
+ process.stdout.write(`Git hooks installed: ${result.gitHooks.join(", ")}\n`);
310
+ }
311
+ return 0;
312
+ }
313
+ async function handleBootstrap(rootDir, parsed) {
314
+ printSplash({
315
+ message: "bootstrapping holistic on this machine...",
316
+ });
317
+ const platformFlag = firstFlag(parsed.flags, "platform", process.platform);
318
+ const platform = platformFlag === "windows" ? "win32" : platformFlag === "macos" ? "darwin" : platformFlag === "linux" ? "linux" : platformFlag;
319
+ const intervalSeconds = Number.parseInt(firstFlag(parsed.flags, "interval", "30"), 10);
320
+ const result = bootstrapHolistic(rootDir, {
321
+ installDaemon: firstFlag(parsed.flags, "install-daemon", "true") !== "false",
322
+ installGitHooks: firstFlag(parsed.flags, "install-hooks", "true") !== "false",
323
+ configureMcp: firstFlag(parsed.flags, "configure-mcp", "true") !== "false",
324
+ platform: platform,
325
+ intervalSeconds,
326
+ remote: firstFlag(parsed.flags, "remote", "origin"),
327
+ stateRef: firstFlag(parsed.flags, "state-ref"),
328
+ stateBranch: firstFlag(parsed.flags, "state-branch"),
329
+ });
330
+ reportHookWarnings(result.gitHookWarnings);
331
+ const statusItems = [];
332
+ if (result.installed) {
333
+ statusItems.push("daemon installed");
334
+ }
335
+ if (result.gitHooksInstalled) {
336
+ statusItems.push("git hooks installed");
337
+ }
338
+ if (result.mcpConfigured) {
339
+ statusItems.push("Claude Desktop MCP configured");
340
+ }
341
+ statusItems.push("bootstrap complete");
342
+ printSplash({
343
+ showStatus: true,
344
+ statusItems,
345
+ });
346
+ process.stdout.write(`System files: ${result.systemDir}
347
+ Config: ${result.configFile}
348
+ Platform: ${result.platform}
349
+ Daemon install: ${result.installed ? `enabled at ${result.startupTarget}` : "not installed"}
350
+ Git hooks: ${result.gitHooksInstalled ? result.gitHooks.join(", ") : "not installed"}
351
+ MCP config: ${result.mcpConfigured ? result.mcpConfigFile : "skipped"}
352
+ Checks: ${result.checks.join(", ")}
353
+ `);
354
+ return 0;
355
+ }
356
+ async function handleResume(rootDir, parsed) {
357
+ refreshHooksBeforeCommand(rootDir);
358
+ const agent = asAgent(firstFlag(parsed.flags, "agent", "unknown"));
359
+ if (firstFlag(parsed.flags, "continue") === "true") {
360
+ const nextState = mutateState(rootDir, (state) => continueFromLatest(rootDir, state, agent));
361
+ const payload = getResumePayload(nextState, agent);
362
+ if (firstFlag(parsed.flags, "json") === "true") {
363
+ printJson(payload);
364
+ return 0;
365
+ }
366
+ process.stdout.write(renderResumeOutput(`Holistic resume\n\n${payload.recap.map((line) => `- ${line}`).join("\n")}\n\nChoices: ${payload.choices.join(", ")}\nAdapter doc: ${payload.adapterDoc}\nRecommended command: ${payload.recommendedCommand}\nLong-term history: ${nextState.docIndex.historyDoc}\nRegression watch: ${nextState.docIndex.regressionDoc}\nZero-touch architecture: ${nextState.docIndex.zeroTouchDoc}\n`));
367
+ return 0;
368
+ }
369
+ const { state } = loadState(rootDir);
370
+ const payload = getResumePayload(state, agent);
371
+ if (firstFlag(parsed.flags, "json") === "true") {
372
+ printJson(payload);
373
+ return 0;
374
+ }
375
+ process.stdout.write(renderResumeOutput(`Holistic resume\n\n${payload.recap.map((line) => `- ${line}`).join("\n")}\n\nChoices: ${payload.choices.join(", ")}\nAdapter doc: ${payload.adapterDoc}\nRecommended command: ${payload.recommendedCommand}\nLong-term history: ${state.docIndex.historyDoc}\nRegression watch: ${state.docIndex.regressionDoc}\nZero-touch architecture: ${state.docIndex.zeroTouchDoc}\n`));
376
+ return 0;
377
+ }
378
+ async function handleCheckpoint(rootDir, parsed) {
379
+ refreshHooksBeforeCommand(rootDir);
380
+ const nextState = mutateState(rootDir, (state) => {
381
+ const regressions = listFlag(parsed.flags, "regression");
382
+ const fixed = firstFlag(parsed.flags, "fixed");
383
+ if (fixed) {
384
+ const fixFiles = firstFlag(parsed.flags, "fix-files");
385
+ const fixRisk = firstFlag(parsed.flags, "fix-risk");
386
+ let fixEntry = `[FIX] ${fixed}`;
387
+ if (fixFiles) {
388
+ fixEntry += ` | files: ${fixFiles}`;
389
+ }
390
+ if (fixRisk) {
391
+ fixEntry += ` | risk: ${fixRisk}`;
392
+ }
393
+ regressions.push(fixEntry);
394
+ }
395
+ const input = {
396
+ agent: asAgent(firstFlag(parsed.flags, "agent", state.activeSession?.agent ?? "unknown")),
397
+ reason: firstFlag(parsed.flags, "reason", fixed ? `fix: ${fixed}` : "manual"),
398
+ goal: firstFlag(parsed.flags, "goal"),
399
+ title: firstFlag(parsed.flags, "title"),
400
+ status: firstFlag(parsed.flags, "status"),
401
+ plan: listFlag(parsed.flags, "plan"),
402
+ done: listFlag(parsed.flags, "done"),
403
+ tried: listFlag(parsed.flags, "tried"),
404
+ next: listFlag(parsed.flags, "next"),
405
+ assumptions: listFlag(parsed.flags, "assumption"),
406
+ blockers: listFlag(parsed.flags, "blocker"),
407
+ references: listFlag(parsed.flags, "ref"),
408
+ impacts: listFlag(parsed.flags, "impact"),
409
+ regressions,
410
+ };
411
+ return checkpointState(rootDir, state, input);
412
+ });
413
+ process.stdout.write(`Checkpoint saved for ${nextState.activeSession?.id ?? "session"}.\nBranch: ${nextState.activeSession?.branch ?? "unknown"}\nChanged files: ${nextState.activeSession?.changedFiles.length ?? 0}\nHistory doc: ${nextState.docIndex.historyDoc}\nRegression watch: ${nextState.docIndex.regressionDoc}\n`);
414
+ requestAutoSync(rootDir, "checkpoint");
415
+ return 0;
416
+ }
417
+ async function handleStartNew(rootDir, parsed) {
418
+ refreshHooksBeforeCommand(rootDir);
419
+ const agent = asAgent(firstFlag(parsed.flags, "agent", "unknown"));
420
+ const rl = createInterface({ input, output });
421
+ try {
422
+ const goal = firstFlag(parsed.flags, "goal") || await ask("New session goal", "Capture the next task", rl);
423
+ const title = firstFlag(parsed.flags, "title");
424
+ const plan = listFlag(parsed.flags, "plan");
425
+ const finalPlan = plan.length > 0 ? plan : await promptList("Initial plan steps", ["Read HOLISTIC.md", "Confirm the next concrete step"], rl);
426
+ const nextState = mutateState(rootDir, (state) => startNewSession(rootDir, state, agent, goal, finalPlan, title));
427
+ process.stdout.write(`Started ${nextState.activeSession?.id} for goal: ${nextState.activeSession?.currentGoal}\n`);
428
+ return 0;
429
+ }
430
+ finally {
431
+ rl.close();
432
+ }
433
+ }
434
+ async function handleHandoff(rootDir, parsed) {
435
+ refreshHooksBeforeCommand(rootDir);
436
+ const { state, paths } = loadState(rootDir);
437
+ if (!state.activeSession) {
438
+ process.stderr.write("No active session to hand off.\n");
439
+ return 1;
440
+ }
441
+ const requestedDraft = firstFlag(parsed.flags, "draft") === "true";
442
+ const storedDraft = readDraftHandoff(paths);
443
+ const matchingDraft = draftMatchesSession(storedDraft, state.activeSession.id) ? storedDraft : null;
444
+ if (requestedDraft && !matchingDraft) {
445
+ process.stderr.write(`No matching auto-drafted handoff found for ${state.activeSession.id}.\n`);
446
+ return 1;
447
+ }
448
+ const draftInput = matchingDraft?.handoff;
449
+ if (matchingDraft && !requestedDraft) {
450
+ process.stdout.write(`Found auto-drafted handoff (${matchingDraft.reason}) at ${draftHandoffFile(paths)}. Using it as the review baseline.\n`);
451
+ }
452
+ const rl = createInterface({ input, output });
453
+ try {
454
+ const handoffInput = {
455
+ summary: pickText(firstFlag(parsed.flags, "summary"), draftInput?.summary),
456
+ done: pickList(listFlag(parsed.flags, "done"), draftInput?.done),
457
+ tried: pickList(listFlag(parsed.flags, "tried"), draftInput?.tried),
458
+ next: pickList(listFlag(parsed.flags, "next"), draftInput?.next),
459
+ assumptions: pickList(listFlag(parsed.flags, "assumption"), draftInput?.assumptions),
460
+ blockers: pickList(listFlag(parsed.flags, "blocker"), draftInput?.blockers),
461
+ references: pickList(listFlag(parsed.flags, "ref"), draftInput?.references),
462
+ impacts: pickList(listFlag(parsed.flags, "impact"), draftInput?.impacts),
463
+ regressions: pickList(listFlag(parsed.flags, "regression"), draftInput?.regressions),
464
+ status: pickText(firstFlag(parsed.flags, "status"), draftInput?.status),
465
+ };
466
+ if (requestedDraft) {
467
+ Object.assign(handoffInput, finalizeDraftHandoffInput(state.activeSession, handoffInput));
468
+ }
469
+ else {
470
+ if (!handoffInput.summary) {
471
+ handoffInput.summary = await ask("Handoff summary", state.activeSession.latestStatus || state.activeSession.currentGoal, rl);
472
+ }
473
+ if (handoffInput.done?.length === 0) {
474
+ handoffInput.done = await promptList("Work completed", draftInput?.done ?? state.activeSession.workDone, rl);
475
+ }
476
+ if (handoffInput.tried?.length === 0) {
477
+ handoffInput.tried = await promptList("What was tried", draftInput?.tried ?? state.activeSession.triedItems, rl);
478
+ }
479
+ if (handoffInput.next?.length === 0) {
480
+ handoffInput.next = await promptList("What should happen next", draftInput?.next ?? state.activeSession.nextSteps, rl);
481
+ }
482
+ if (handoffInput.impacts?.length === 0) {
483
+ handoffInput.impacts = await promptList("Overall impact on the project", draftInput?.impacts ?? state.activeSession.impactNotes, rl);
484
+ }
485
+ if (handoffInput.regressions?.length === 0) {
486
+ handoffInput.regressions = await promptList("Regression risks to guard", draftInput?.regressions ?? state.activeSession.regressionRisks, rl);
487
+ }
488
+ if (handoffInput.assumptions?.length === 0) {
489
+ handoffInput.assumptions = await promptList("Important assumptions", draftInput?.assumptions ?? state.activeSession.assumptions, rl);
490
+ }
491
+ if (handoffInput.blockers?.length === 0) {
492
+ handoffInput.blockers = await promptList("Known blockers", draftInput?.blockers ?? state.activeSession.blockers, rl);
493
+ }
494
+ if (handoffInput.references?.length === 0) {
495
+ handoffInput.references = await promptList("References and docs", draftInput?.references ?? state.activeSession.references, rl);
496
+ }
497
+ }
498
+ const nextState = mutateState(rootDir, (latestState, paths) => {
499
+ const result = applyHandoff(rootDir, latestState, handoffInput);
500
+ if (result.pendingCommit) {
501
+ writePendingCommit(paths, result.pendingCommit.message);
502
+ }
503
+ clearDraftHandoff(paths);
504
+ return result;
505
+ });
506
+ process.stdout.write(`Handoff complete.\nSummary: ${nextState.lastHandoff?.summary ?? "n/a"}\nPending git commit: ${nextState.pendingCommit?.message ?? "none"}\nHistory doc: ${nextState.docIndex.historyDoc}\nRegression watch: ${nextState.docIndex.regressionDoc}\n`);
507
+ requestAutoSync(rootDir, "handoff");
508
+ return 0;
509
+ }
510
+ finally {
511
+ rl.close();
512
+ }
513
+ }
514
+ async function handleStatus(rootDir) {
515
+ refreshHooksBeforeCommand(rootDir);
516
+ const { state } = loadState(rootDir);
517
+ process.stdout.write(renderStatus(state));
518
+ return 0;
519
+ }
520
+ async function handleDiff(rootDir, parsed) {
521
+ const from = firstFlag(parsed.flags, "from");
522
+ const to = firstFlag(parsed.flags, "to");
523
+ const format = firstFlag(parsed.flags, "format", "text");
524
+ if (!from || !to) {
525
+ process.stderr.write("Error: --from and --to session IDs are required.\n");
526
+ return 1;
527
+ }
528
+ const { state, paths } = loadState(rootDir);
529
+ const fromSession = loadSessionById(state, paths, from);
530
+ const toSession = loadSessionById(state, paths, to);
531
+ if (!fromSession || !toSession) {
532
+ process.stderr.write("Error: One or both sessions could not be found.\n");
533
+ return 1;
534
+ }
535
+ const diff = computeSessionDiff(fromSession, toSession);
536
+ if (format === "json") {
537
+ printJson({
538
+ from: fromSession,
539
+ to: toSession,
540
+ diff,
541
+ });
542
+ return 0;
543
+ }
544
+ process.stdout.write(renderDiff(fromSession, toSession, diff));
545
+ return 0;
546
+ }
547
+ async function handleServe(rootDir) {
548
+ refreshHooksBeforeCommand(rootDir);
549
+ printSplashError({
550
+ message: "starting MCP server on stdio...",
551
+ });
552
+ const runtime = runtimeScript("mcp-server");
553
+ const moduleUrl = pathToFileURL(runtime.scriptPath).href;
554
+ const mcpModule = await import(moduleUrl);
555
+ if (typeof mcpModule.runMcpServer !== "function") {
556
+ process.stderr.write("Unable to start Holistic MCP server.\n");
557
+ return 1;
558
+ }
559
+ await mcpModule.runMcpServer(process.env.HOLISTIC_REPO ?? rootDir);
560
+ return 0;
561
+ }
562
+ async function handleMarkCommit(rootDir, parsed) {
563
+ const message = firstFlag(parsed.flags, "message", "docs(holistic): handoff");
564
+ const nextState = mutateState(rootDir, (state, paths) => {
565
+ if (state.lastHandoff) {
566
+ state.lastHandoff.committedAt = new Date().toISOString();
567
+ }
568
+ state.pendingCommit = null;
569
+ clearPendingCommit(paths);
570
+ return state;
571
+ });
572
+ process.stdout.write(`Marked handoff commit complete: ${message || nextState.lastHandoff?.summary || "docs(holistic): handoff"}\n`);
573
+ return 0;
574
+ }
575
+ async function handleWatch(rootDir, parsed) {
576
+ refreshHooksBeforeCommand(rootDir);
577
+ const intervalSeconds = Number.parseInt(firstFlag(parsed.flags, "interval", "60"), 10);
578
+ const agent = asAgent(firstFlag(parsed.flags, "agent", "unknown"));
579
+ process.stdout.write(`Watching repo every ${intervalSeconds}s for checkpoint-worthy changes.\n`);
580
+ const timer = setInterval(() => {
581
+ const result = runDaemonTick(rootDir, agent);
582
+ if (result.changed) {
583
+ process.stdout.write(`${new Date().toISOString()} ${result.summary}\n`);
584
+ }
585
+ }, intervalSeconds * 1000);
586
+ const stop = () => {
587
+ clearInterval(timer);
588
+ process.stdout.write("Stopped watch mode.\n");
589
+ process.exit(0);
590
+ };
591
+ process.on("SIGINT", stop);
592
+ process.on("SIGTERM", stop);
593
+ return await new Promise(() => undefined);
594
+ }
595
+ async function main() {
596
+ const parsed = parseArgs(process.argv.slice(2));
597
+ const rootDir = process.cwd();
598
+ switch (parsed.command) {
599
+ case "init":
600
+ return handleInit(rootDir, parsed);
601
+ case "bootstrap":
602
+ return handleBootstrap(rootDir, parsed);
603
+ case "start":
604
+ case "resume":
605
+ return handleResume(rootDir, parsed);
606
+ case "checkpoint":
607
+ return handleCheckpoint(rootDir, parsed);
608
+ case "handoff":
609
+ return handleHandoff(rootDir, parsed);
610
+ case "start-new":
611
+ return handleStartNew(rootDir, parsed);
612
+ case "status":
613
+ return handleStatus(rootDir);
614
+ case "diff":
615
+ return handleDiff(rootDir, parsed);
616
+ case "serve":
617
+ return handleServe(rootDir);
618
+ case "watch":
619
+ return handleWatch(rootDir, parsed);
620
+ case "internal-mark-commit":
621
+ return handleMarkCommit(rootDir, parsed);
622
+ default:
623
+ printHelp();
624
+ return 0;
625
+ }
626
+ }
627
+ const isEntrypoint = process.argv[1] ? pathToFileURL(process.argv[1]).href === import.meta.url : false;
628
+ if (isEntrypoint) {
629
+ main().then((code) => {
630
+ process.exit(code);
631
+ }).catch((error) => {
632
+ const message = error instanceof Error ? error.stack || error.message : String(error);
633
+ process.stderr.write(`${message}\n`);
634
+ process.exit(1);
635
+ });
636
+ }
637
+ //# sourceMappingURL=cli.js.map