holistic 0.5.4 → 0.5.5

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.
package/CHANGELOG.md CHANGED
@@ -1,8 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.5 - 2026-04-10
4
+
5
+ Major Security & Trust Hardening (M005) to eliminate silent automation and improve auditability.
6
+
7
+ - Added **Consent Gating** to `holistic bootstrap`: The CLI now displays a summary of system-modifying actions (daemon, hooks, Claude setup) and requires an explicit `--yes` flag to apply them.
8
+ - Added `holistic doctor` command for repository setup diagnostics and background sync health monitoring.
9
+ - Implemented **Privacy-First Defaults**: Remote portable-state syncing is now disabled by default. Users must explicitly opt-in by setting `"portableState": true` in the repo config.
10
+ - Eliminated **Silent Error Suppression**: Background sync scripts (PowerShell & Bash) now use timestamped logging to `.holistic/system/sync.log`. Failures are now visible in `holistic doctor` and `holistic status`.
11
+ - Hardened **Git-Native Snapshotting**: Refactored repo snapshotting to use `git ls-files`, ensuring $O(\text{changes})$ performance and native `.gitignore` compliance.
12
+ - Gated **Handoff Commits by Default**: Removed automatic Git commits from the `handoff` command. Holistic now prepares a `pending-commit.txt` for manual review, with an optional `--commit` flag for automated workflows.
13
+
3
14
  ## 0.5.4 - 2026-04-09
4
15
 
5
- Security hardening in response to npm AI scanner flags.
16
+ Security hardening in response to socket.dev AI-based package scanner flags.
6
17
 
7
18
  - Removed `-WindowStyle Hidden` from the Windows daemon startup `.cmd` — the daemon now runs in a visible window, consistent with how macOS and Linux handle it.
8
19
  - Downgraded PowerShell execution policy from `-ExecutionPolicy Bypass` to `-ExecutionPolicy RemoteSigned` in all three generation sites (`setup.ts`, `sync.ts`, `bin/holistic.cmd`). `RemoteSigned` is sufficient for locally-generated scripts and does not suppress antivirus or security monitoring.
package/README.md CHANGED
@@ -12,12 +12,32 @@ Your repo remembers, so your next agent doesn't have to guess.
12
12
  Shared memory for AI agents, built into your repo.
13
13
  ```
14
14
 
15
+ [![npm version](https://img.shields.io/npm/v/holistic.svg)](https://www.npmjs.com/package/holistic)
16
+ [![Tests](https://img.shields.io/github/actions/workflow/status/lweiss01/holistic/test.yml?branch=main&label=tests)](https://github.com/lweiss01/holistic/actions)
17
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
18
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D24-339933.svg)](./package.json)
19
+
15
20
  ### One command. Every agent. Zero re-explaining. ✨
16
21
 
17
22
  Holistic gives your AI agents shared memory inside the repo itself. When you switch from Claude to Codex to Gemini, the next agent can see what happened last time, what not to break, and what should happen next.
18
23
 
24
+
19
25
  ---
20
26
 
27
+
28
+ ### Why trust this?
29
+
30
+ - 🔐 No known security vulnerabilities
31
+ - 🧪 60+ automated tests covering core flows
32
+ - 🛠️ Actively maintained (frequent releases)
33
+ - 🔍 Transparent local setup (see `SECURITY.md`)
34
+
35
+ > Early beta, but built to be safe, inspectable, and predictable.
36
+
37
+
38
+ ---
39
+
40
+
21
41
  ## Get started in 30 seconds ⚡
22
42
 
23
43
  Open your project repo in PowerShell, Terminal, Command Prompt, or whatever shell you normally use.
@@ -28,7 +48,7 @@ Run these two commands:
28
48
 
29
49
  ```bash
30
50
  npm install -g holistic
31
- holistic bootstrap
51
+ holistic bootstrap --yes
32
52
  ```
33
53
 
34
54
  After that, open the repo in your agent app and use this startup prompt:
@@ -41,8 +61,10 @@ That is enough to get the basic Holistic workflow working.
41
61
 
42
62
  If you want the fuller install and setup details, jump to [Quick start](#quick-start-).
43
63
 
64
+
44
65
  ---
45
66
 
67
+
46
68
  ## The problem 😵
47
69
 
48
70
  If you use more than one AI coding assistant, the workflow usually falls apart:
@@ -55,8 +77,10 @@ If you use more than one AI coding assistant, the workflow usually falls apart:
55
77
 
56
78
  Holistic fixes that by making the repo the source of truth.
57
79
 
80
+
58
81
  ---
59
82
 
83
+
60
84
  ## What it feels like with HOLISTIC 🌿
61
85
 
62
86
  Run one setup command on a machine:
@@ -76,8 +100,10 @@ Most days, you do not need to keep a terminal process open or manually re-brief
76
100
 
77
101
  `holistic bootstrap` is a machine setup command, not just a repo setup command. By default it can install local startup helpers and configure Claude Desktop MCP on that machine.
78
102
 
103
+
79
104
  ---
80
105
 
106
+
81
107
  ## How it works 🧭
82
108
 
83
109
  ```text
@@ -96,8 +122,10 @@ Holistic checkpoints and handoffs keep repo memory current
96
122
  The next agent picks up without a long re-explanation
97
123
  ```
98
124
 
125
+
99
126
  ---
100
127
 
128
+
101
129
  ## Quick start 🚀
102
130
 
103
131
  ### Install 📦
@@ -136,7 +164,7 @@ npm link
136
164
 
137
165
  ```bash
138
166
  cd my-project
139
- holistic bootstrap --remote origin
167
+ holistic bootstrap --remote origin --yes
140
168
  git add .gitattributes HOLISTIC.md AGENTS.md CLAUDE.md GEMINI.md
141
169
  git add .holistic/config.json .holistic/state.json
142
170
  git add .holistic/context/
@@ -170,8 +198,10 @@ holistic bootstrap --install-daemon false --configure-mcp false
170
198
 
171
199
  The portable repo memory is meant to be committed and synced. Machine-local helper scripts are generated for each machine and stay local.
172
200
 
201
+
173
202
  ---
174
203
 
204
+
175
205
  ## Daily workflow 🔄
176
206
 
177
207
  One-time machine setup:
@@ -200,8 +230,10 @@ If `holistic` is not on `PATH` in a given shell, every bootstrapped repo also ha
200
230
  - Windows: `.\.holistic\system\holistic.cmd <command>`
201
231
  - macOS/Linux: `./.holistic/system/holistic <command>`
202
232
 
233
+
203
234
  ---
204
235
 
236
+
205
237
  ## Regression protection 🛡️
206
238
 
207
239
  When an agent fixes something delicate, lock it in:
@@ -215,8 +247,10 @@ holistic checkpoint \
215
247
 
216
248
  Future agents will see that warning in the repo docs before they touch the risky area again.
217
249
 
250
+
218
251
  ---
219
252
 
253
+
220
254
  ## Works with multiple agent apps 🤝
221
255
 
222
256
  Holistic is model-agnostic. It works through repo files first, and can expose a thin MCP server where supported.
@@ -238,8 +272,10 @@ Use this table to decide whether startup should be automatic (MCP/tooling hook)
238
272
  | Other VS Code forks | `AGENTS.md` and repo docs | ❌ No | Treat as manual-start and run `/holistic` |
239
273
  | Web tools | repo docs pasted manually | ❌ No | Manual copy/paste recap from Holistic docs |
240
274
 
275
+
241
276
  ---
242
277
 
278
+
243
279
  ## What lives in your repo 🗂️
244
280
 
245
281
  ```text
@@ -265,23 +301,24 @@ my-project/
265
301
 
266
302
  The portable repo memory (config, state, context, sessions) is meant to be committed and synced. Machine-local helper scripts and repo-local CLI fallbacks under `.holistic/system/` are generated for each machine and stay local (already in `.gitignore`).
267
303
 
304
+
268
305
  ---
269
306
 
307
+
270
308
  ## Commands
271
309
 
272
- | Command | What it does |
273
- |---|---|
274
310
  | `holistic init` | Base repo setup and scaffolding |
275
- | `holistic bootstrap` | One-step machine setup for repo files, hooks, and by-default local daemon/MCP integration setup |
276
- | `holistic repair` | Regenerates `.holistic/system/` helpers from the current repo config use this after a path move or broken bootstrap |
277
- | `holistic resume / start --agent <name>` | Loads project recap and prints the current state — `start` is an alias for `resume` |
278
- | `holistic start-new --goal "..."` | Starts a fresh session with a new goal |
311
+ | `holistic bootstrap` | One-step machine setup. Required `--yes` to apply system changes. |
312
+ | `holistic doctor` | Runs health checks on machine setup and sync logs |
313
+ | `holistic repair` | Regenerates `.holistic/system/` helpers |
314
+ | `holistic resume / start --agent <name>` | Loads project recap and prints state |
315
+ | `holistic start-new` | Starts a fresh session |
279
316
  | `holistic checkpoint --reason "..."` | Saves progress and context |
280
- | `holistic handoff` | Ends a session with a handoff |
281
- | `holistic status` | Shows the current state |
317
+ | `holistic handoff` | Ends a session with a handoff message |
318
+ | `holistic status` | Shows current state and recent sync activity |
282
319
  | `holistic diff --from <id> --to <id>` | Compares two sessions |
283
- | `holistic search --id <session-id>` | Finds and reactivates an archived session by ID |
284
- | `holistic serve` | Runs the thin MCP server and prints a startup banner to `stderr` |
320
+ | `holistic search --id <session-id>` | Finds and reactivates an archived session |
321
+ | `holistic serve` | Runs the thin MCP server |
285
322
  | `holistic watch` | Foreground daemon mode for automatic checkpoints |
286
323
 
287
324
  ### Slash command helper text (agent-facing)
@@ -302,8 +339,10 @@ holistic handoff \
302
339
  --blocker "Need refresh token endpoint from backend team"
303
340
  ```
304
341
 
342
+
305
343
  ---
306
344
 
345
+
307
346
  ## Architecture
308
347
 
309
348
  Holistic is intentionally repo-first, not machine-first.
@@ -342,8 +381,10 @@ When you do run `holistic serve` manually in a terminal, Holistic prints its ASC
342
381
  }
343
382
  ```
344
383
 
384
+
345
385
  ---
346
386
 
387
+
347
388
  ## Why this matters
348
389
 
349
390
  If you are already using more than one AI coding assistant, you already have the continuity problem.
@@ -356,35 +397,49 @@ Holistic gives you:
356
397
  - A durable record of what changed and why
357
398
  - Agents that can get to work quickly
358
399
 
400
+
359
401
  ---
360
402
 
403
+
361
404
  ## Beta Feedback Welcome 🙏
362
405
 
363
406
  Holistic is in early beta. If you hit rough edges, unexpected behavior, or have ideas for improvement, please [open an issue](https://github.com/lweiss01/holistic/issues). Early adopter feedback directly shapes the direction of the project.
364
407
 
365
408
  For support and troubleshooting, see [SUPPORT.md](./SUPPORT.md).
366
409
 
410
+
367
411
  ---
368
412
 
369
- ## Security & Privacy 🔒
370
413
 
371
- Holistic installs a background daemon and writes helper scripts to your machine. This is fully disclosed and by design — it is what makes zero-touch continuity work.
414
+ ## Security, Privacy, and Trust 🔒
415
+
416
+ Holistic is designed to be **transparent, audit-safe, and consent-first**. It is a shared memory layer that stays in your repo, not a cloud service that watches your screen.
372
417
 
373
- Short version of what it does:
418
+ ### Trust Architecture:
419
+ - **Explicit Consent**: `holistic bootstrap` will show you exactly what changes it wants to make to your machine (daemon installation, git hooks, Claude setup) and requires an explicit `--yes` to proceed.
420
+ - **Privacy First**: Remote syncing is **disabled by default**. Set `"portableState": true` in `.holistic/config.json` only if you want to share memory across devices using a hidden git ref.
421
+ - **Traceable Activity**: Background sync operations (PowerShell/Bash) are no longer silent. All activity is logged with timestamps and error details to `.holistic/system/sync.log`.
422
+ - **Health Checks**: Use `holistic doctor` at any time to audit your machine-local setup and verify sync log health.
423
+ - **Git-Native Snapshotting**: The repo snapshot logic uses native `git ls-files`, ensuring that your `.gitignore` rules are perfectly respected and performance stays $O(\text{repo size})$.
424
+ - **Zero Shell Injection**: Internal commit logic has been stripped of shell wrappers to eliminate command injection risks.
425
+
426
+ ### What it does:
374
427
  - Writes session state into `.holistic/` inside your repo (committed files you control)
375
428
  - Installs a daemon via standard OS autostart (Windows Startup folder, macOS LaunchAgents, Linux systemd user)
376
429
  - Can push Holistic state to a hidden git ref on your configured remote (opt-in, your repo only)
377
430
 
378
- Short version of what it does NOT do:
431
+ ### What it does NOT do:
379
432
  - Does not read or transmit file contents outside your repo
380
433
  - Does not access credentials or tokens
381
434
  - Does not phone home to any external service
382
- - Does not use `-ExecutionPolicy Bypass` or hidden PowerShell windows
435
+ - Does not use obfuscated scripts or hidden windows
436
+
437
+ For the full technical disclosure, see [SECURITY.md](./SECURITY.md).
383
438
 
384
- For the full disclosure, see [SECURITY.md](./SECURITY.md).
385
439
 
386
440
  ---
387
441
 
442
+
388
443
  ## Quick links
389
444
 
390
445
  - [Walkthrough](./docs/handoff-walkthrough.md)
@@ -394,6 +449,8 @@ For the full disclosure, see [SECURITY.md](./SECURITY.md).
394
449
  - [License](./LICENSE)
395
450
 
396
451
 
452
+
397
453
  ---
398
454
 
455
+
399
456
  <p align="center"><em>Built for people who use more than one AI assistant and are tired of paying the context tax.</em></p>
package/bin/holistic CHANGED
@@ -1,17 +1,10 @@
1
- #!/usr/bin/env sh
2
- SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
3
- node --experimental-strip-types "$SCRIPT_DIR/../src/cli.ts" "$@"
4
- status=$?
5
- if [ "$status" -ne 0 ]; then
6
- exit "$status"
7
- fi
8
- if [ "$1" = "handoff" ] && [ -f "$PWD/.holistic/context/pending-commit.txt" ]; then
9
- message=$(head -n 1 "$PWD/.holistic/context/pending-commit.txt")
10
- if [ -n "$message" ]; then
11
- git add -- HOLISTIC.md AGENTS.md .holistic && git commit -m "$message"
12
- if [ "$?" -eq 0 ] && [ -f "$PWD/.holistic/system/sync-state.sh" ]; then
13
- /bin/sh "$PWD/.holistic/system/sync-state.sh" || true
14
- node --experimental-strip-types "$SCRIPT_DIR/../src/cli.ts" internal-mark-commit --message "$message"
15
- fi
16
- fi
17
- fi
1
+ #!/usr/bin/env sh
2
+ SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
3
+ node --experimental-strip-types "$SCRIPT_DIR/../src/cli.ts" "$@"
4
+ status=$?
5
+ if [ "$status" -ne 0 ]; then
6
+ exit "$status"
7
+ fi
8
+
9
+ # Holistic: Sync logic is now handled inside the Node CLI or gated behind explicit flags.
10
+ # This wrapper is kept simple to avoid shell variable expansion vulnerabilities.
package/bin/holistic.cmd CHANGED
@@ -3,27 +3,6 @@ setlocal
3
3
  set SCRIPT_DIR=%~dp0
4
4
  node --experimental-strip-types "%SCRIPT_DIR%..\src\cli.ts" %*
5
5
  if errorlevel 1 exit /b %errorlevel%
6
- if /I "%~1"=="handoff" (
7
- set PENDING_FILE=%CD%\.holistic\context\pending-commit.txt
8
- if exist "%PENDING_FILE%" (
9
- set /p COMMIT_MSG=<"%PENDING_FILE%"
10
- if defined COMMIT_MSG (
11
- rem Sanitize COMMIT_MSG: strip characters that can break cmd.exe argument quoting
12
- set COMMIT_MSG=%COMMIT_MSG:&= %
13
- set COMMIT_MSG=%COMMIT_MSG:|= %
14
- set COMMIT_MSG=%COMMIT_MSG:>= %
15
- set COMMIT_MSG=%COMMIT_MSG:<= %
16
- set COMMIT_MSG=%COMMIT_MSG:"=%
17
- git add -- HOLISTIC.md AGENTS.md .holistic
18
- if not errorlevel 1 (
19
- git commit -m "%COMMIT_MSG%"
20
- if not errorlevel 1 (
21
- if exist "%CD%\.holistic\system\sync-state.ps1" (
22
- powershell -NoProfile -ExecutionPolicy RemoteSigned -File "%CD%\.holistic\system\sync-state.ps1"
23
- )
24
- node --experimental-strip-types "%SCRIPT_DIR%..\src\cli.ts" internal-mark-commit --message "%COMMIT_MSG%"
25
- )
26
- )
27
- )
28
- )
29
- )
6
+
7
+ rem Holistic: Sync logic is now handled inside the Node CLI or gated behind explicit flags.
8
+ rem This wrapper is kept simple to avoid shell variable injection vulnerabilities.
package/dist/cli.d.ts CHANGED
@@ -3,5 +3,5 @@ export declare function renderHelpText(): string;
3
3
  export declare function renderResumeOutput(body: string): string;
4
4
  export declare function finalizeDraftHandoffInput(session: SessionRecord, input: HandoffInput): HandoffInput;
5
5
  export declare function renderDiff(fromSession: SessionRecord, toSession: SessionRecord, diff: SessionDiff): string;
6
- export declare function renderStatus(state: HolisticState): string;
6
+ export declare function renderStatus(rootDir: string, state: HolisticState): string;
7
7
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAA4C,YAAY,EAAE,aAAa,EAAgB,WAAW,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA2EvJ,wBAAgB,cAAc,IAAI,MAAM,CA2BvC;AAMD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AA4ED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,GAAG,YAAY,CAcnG;AAMD,wBAAgB,UAAU,CAAC,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAoE1G;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CA4DzD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAA4C,YAAY,EAAE,aAAa,EAAgB,WAAW,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA2EvJ,wBAAgB,cAAc,IAAI,MAAM,CA4BvC;AAMD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AA4ED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,GAAG,YAAY,CAcnG;AAMD,wBAAgB,UAAU,CAAC,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAoE1G;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,CA8D1E"}
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { renderRepoLocalCliCommands } from './core/cli-fallback.js';
7
7
  import { captureRepoSnapshot, clearPendingCommit, commitPendingChanges, writePendingCommit } from './core/git.js';
8
8
  import { writeDerivedDocs } from './core/docs.js';
9
- import { bootstrapHolistic, initializeHolistic, refreshHolisticHooks, repairHolistic } from './core/setup.js';
9
+ import { bootstrapHolistic, getSetupStatus, initializeHolistic, refreshHolisticHooks, repairHolistic } from './core/setup.js';
10
10
  import { printSplash, printSplashError, renderSplash } from './core/splash.js';
11
11
  import { requestAutoSync } from './core/sync.js';
12
12
  import { runDaemonTick } from './daemon.js';
@@ -73,14 +73,15 @@ export function renderHelpText() {
73
73
 
74
74
  Usage:
75
75
  holistic init [--install-daemon] [--install-hooks] [--platform win32|darwin|linux] [--interval 30] [--remote origin] [--state-ref refs/holistic/state] [--state-branch holistic/state]
76
- 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]
76
+ holistic bootstrap [--platform win32|darwin|linux] [--interval 30] [--yes]
77
+ holistic doctor
77
78
  holistic repair [--platform win32|darwin|linux] [--refresh-hooks false] [--install-daemon true] [--configure-mcp true]
78
79
  holistic start [--agent codex|claude|antigravity|gemini|copilot|cursor|goose|gsd|gsd2] [--continue] [--json]
79
80
  holistic resume [--agent codex|claude|antigravity|gemini|copilot|cursor|goose|gsd|gsd2] [--continue] [--json]
80
- holistic checkpoint --reason "<reason>" [--goal "<goal>"] [--status "<status>"] [--plan "<step>"]... [--completion-kind natural-breakpoint|task-complete|slice-complete|milestone-complete] [--completion-source agent|system] [--completion-recorded-at <iso8601>]
81
+ holistic checkpoint --reason "<reason>" [--status "<status>"] [--plan "<step>"]... [--completion-kind natural-breakpoint|task-complete|slice-complete|milestone-complete] [--completion-source agent|system] [--completion-recorded-at <iso8601>]
81
82
  holistic checkpoint --fixed "<bug>" [--fix-files "<file>"] [--fix-risk "<what reintroduces it>"]
82
- holistic handoff [--draft] [--summary "<summary>"] [--next "<step>"]...
83
- holistic start-new --goal "<goal>" [--title "<title>"] [--plan "<step>"]...
83
+ holistic handoff [--draft] [--summary "<summary>"] [--next "<step>"]... [--commit]
84
+ holistic start-new [--goal "<goal>"] [--title "<title>"] [--plan "<step>"]...
84
85
  holistic status
85
86
  holistic diff --from "<session-id>" --to "<session-id>" [--format text|json]
86
87
  holistic search --id "<session-id>" [--format text|json]
@@ -245,10 +246,12 @@ export function renderDiff(fromSession, toSession, diff) {
245
246
  }
246
247
  return lines.join("\n") + "\n";
247
248
  }
248
- export function renderStatus(state) {
249
+ export function renderStatus(rootDir, state) {
249
250
  const lines = [];
250
251
  lines.push("Holistic Status");
251
252
  lines.push("");
253
+ lines.push(renderSyncStatus(rootDir));
254
+ lines.push("");
252
255
  if (!state.activeSession) {
253
256
  lines.push("No active session.");
254
257
  if (state.lastHandoff) {
@@ -256,7 +259,7 @@ export function renderStatus(state) {
256
259
  lines.push(`Last handoff: ${state.lastHandoff.summary}`);
257
260
  lines.push(`Next action: ${state.lastHandoff.nextAction}`);
258
261
  }
259
- if (state.pendingWork.length > 0) {
262
+ if (state.pendingWork && state.pendingWork.length > 0) {
260
263
  lines.push("");
261
264
  lines.push(`Pending work: ${state.pendingWork.length} item(s)`);
262
265
  for (const item of state.pendingWork.slice(0, 3)) {
@@ -341,12 +344,27 @@ Repo-local CLI: ${initFallback}
341
344
  return 0;
342
345
  }
343
346
  async function handleBootstrap(rootDir, parsed) {
344
- printSplash({
345
- message: "bootstrapping holistic on this machine...",
346
- });
347
347
  const platformFlag = firstFlag(parsed.flags, "platform", process.platform);
348
348
  const platform = platformFlag === "windows" ? "win32" : platformFlag === "macos" ? "darwin" : platformFlag === "linux" ? "linux" : platformFlag;
349
349
  const intervalSeconds = Number.parseInt(firstFlag(parsed.flags, "interval", "30"), 10);
350
+ const confirmed = firstFlag(parsed.flags, "yes") === "true";
351
+ const status = getSetupStatus(rootDir, {
352
+ platform: platform,
353
+ intervalSeconds,
354
+ });
355
+ const pending = status.filter(s => s.status !== "ok");
356
+ if (pending.length > 0 && !confirmed) {
357
+ printSplash({
358
+ message: "bootstrap pre-flight: pending actions",
359
+ });
360
+ process.stdout.write("\nHolistic needs to make the following changes to your system:\n\n");
361
+ printSetupStatusTable(status);
362
+ process.stdout.write("\nRun with --yes to apply these changes.\n");
363
+ return 1;
364
+ }
365
+ printSplash({
366
+ message: "bootstrapping holistic on this machine...",
367
+ });
350
368
  const result = bootstrapHolistic(rootDir, {
351
369
  installDaemon: firstFlag(parsed.flags, "install-daemon", "true") !== "false",
352
370
  installGitHooks: firstFlag(parsed.flags, "install-hooks", "true") !== "false",
@@ -382,9 +400,37 @@ Git hooks: ${result.gitHooksInstalled ? result.gitHooks.join(", ") : "not instal
382
400
  MCP config: ${result.mcpConfigured ? result.mcpConfigFile : "skipped"}
383
401
  Checks: ${result.checks.join(", ")}
384
402
  Repo-local CLI: ${bootstrapFallback}
403
+
404
+ [TIP] To enable remote syncing (Portable State), set "portableState": true in .holistic/config.json
385
405
  `);
386
406
  return 0;
387
407
  }
408
+ async function handleDoctor(rootDir, parsed) {
409
+ printSplash({
410
+ message: "running holistic health check...",
411
+ });
412
+ const status = getSetupStatus(rootDir);
413
+ process.stdout.write("\nSystem Configuration:\n\n");
414
+ printSetupStatusTable(status);
415
+ process.stdout.write("\nSync Diagnostics:\n\n");
416
+ process.stdout.write(renderSyncStatus(rootDir));
417
+ const healthy = status.every(s => s.status === "ok");
418
+ if (healthy) {
419
+ process.stdout.write("\n\u2713 Holistic is healthy and correctly configured on this machine.\n");
420
+ }
421
+ else {
422
+ process.stdout.write("\n\u26A0 Some components require attention. Run 'holistic bootstrap' to fix.\n");
423
+ }
424
+ return 0;
425
+ }
426
+ function printSetupStatusTable(status) {
427
+ for (const s of status) {
428
+ const icon = s.status === "ok" ? "\u2713" : s.status === "missing" ? "\u25CB" : s.status === "outdated" ? "\u21BB" : s.status === "info" ? "\u2139" : "!";
429
+ const label = `${icon} ${s.component.padEnd(20)}`;
430
+ process.stdout.write(`${label} | ${s.status.padEnd(10)} | ${s.details}\n`);
431
+ process.stdout.write(` ${s.description}\n\n`);
432
+ }
433
+ }
388
434
  async function handleRepair(rootDir, parsed) {
389
435
  printSplash({
390
436
  message: "repairing holistic machine-local helpers...",
@@ -515,23 +561,16 @@ async function handleCheckpoint(rootDir, parsed) {
515
561
  async function handleStartNew(rootDir, parsed) {
516
562
  refreshHooksBeforeCommand(rootDir);
517
563
  const agent = asAgent(firstFlag(parsed.flags, "agent", "unknown"));
518
- const rl = createInterface({ input, output });
519
- try {
520
- const goal = firstFlag(parsed.flags, "goal") || await ask("New session goal", "Capture the next task", rl);
521
- const title = firstFlag(parsed.flags, "title");
522
- const plan = listFlag(parsed.flags, "plan");
523
- const finalPlan = plan.length > 0 ? plan : await promptList("Initial plan steps", ["Read HOLISTIC.md", "Confirm the next concrete step"], rl);
524
- const mutateResult = mutateState(rootDir, (state) => startNewSession(rootDir, state, agent, goal, finalPlan, title));
525
- if (!mutateResult.success || !mutateResult.state) {
526
- process.stderr.write(`Error: Failed to start session: ${mutateResult.error}\n`);
527
- return 1;
528
- }
529
- process.stdout.write(`Started ${mutateResult.state.activeSession?.id} for goal: ${mutateResult.state.activeSession?.currentGoal}\n`);
530
- return 0;
531
- }
532
- finally {
533
- rl.close();
564
+ const goal = firstFlag(parsed.flags, "goal");
565
+ const title = firstFlag(parsed.flags, "title");
566
+ const plan = listFlag(parsed.flags, "plan");
567
+ const mutateResult = mutateState(rootDir, (state) => startNewSession(rootDir, state, agent, goal, plan, title));
568
+ if (!mutateResult.success || !mutateResult.state) {
569
+ process.stderr.write(`Error: Failed to start session: ${mutateResult.error}\n`);
570
+ return 1;
534
571
  }
572
+ process.stdout.write(`Started ${mutateResult.state.activeSession?.id} for goal: ${mutateResult.state.activeSession?.currentGoal}\n`);
573
+ return 0;
535
574
  }
536
575
  async function handleHandoff(rootDir, parsed) {
537
576
  refreshHooksBeforeCommand(rootDir);
@@ -610,22 +649,33 @@ async function handleHandoff(rootDir, parsed) {
610
649
  process.exit(1);
611
650
  }
612
651
  const nextState = mutateResult.state;
652
+ const shouldCommit = firstFlag(parsed.flags, "commit") === "true";
613
653
  let commitResult = null;
614
- if (nextState.pendingCommit) {
654
+ if (nextState.pendingCommit && shouldCommit) {
615
655
  commitResult = commitPendingChanges(rootDir, nextState.pendingCommit.message, nextState.pendingCommit.files);
616
656
  if (commitResult.success) {
617
- const clearResult = mutateState(rootDir, (latestState, paths) => {
657
+ mutateState(rootDir, (latestState, paths) => {
618
658
  clearPendingCommit(paths);
619
659
  return {
620
660
  ...latestState,
621
661
  pendingCommit: null,
622
662
  };
623
663
  });
624
- if (!clearResult.success) {
625
- process.stderr.write(`Warning: Failed to clear pending commit state: ${clearResult.error}\n`);
626
- }
627
664
  }
628
665
  }
666
+ if (nextState.pendingCommit && !shouldCommit) {
667
+ process.stdout.write(`
668
+ Handoff prepared successfully!
669
+ Docs updated: ${nextState.pendingCommit.files.join(", ")}
670
+
671
+ To complete the handoff, review your changes and commit:
672
+ git add .
673
+ git commit -F .holistic/context/pending-commit.txt
674
+
675
+ Use 'holistic handoff --commit' next time to automate this step safely.
676
+ `);
677
+ return 0;
678
+ }
629
679
  process.stdout.write(`Handoff complete.\nSummary: ${nextState.lastHandoff?.summary ?? "n/a"}\n`);
630
680
  if (commitResult) {
631
681
  if (commitResult.success) {
@@ -686,9 +736,7 @@ function renderSyncStatus(rootDir) {
686
736
  async function handleStatus(rootDir) {
687
737
  refreshHooksBeforeCommand(rootDir);
688
738
  const { state } = loadState(rootDir);
689
- process.stdout.write(renderStatus(state));
690
- process.stdout.write("\n");
691
- process.stdout.write(renderSyncStatus(rootDir));
739
+ process.stdout.write(renderStatus(rootDir, state));
692
740
  return 0;
693
741
  }
694
742
  async function handleDiff(rootDir, parsed) {
@@ -822,6 +870,8 @@ async function main() {
822
870
  return handleInit(rootDir, parsed);
823
871
  case "bootstrap":
824
872
  return handleBootstrap(rootDir, parsed);
873
+ case "doctor":
874
+ return handleDoctor(rootDir, parsed);
825
875
  case "repair":
826
876
  return handleRepair(rootDir, parsed);
827
877
  case "start":