@vibecompass/vibecompass 0.3.0 → 0.3.1

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/README.md CHANGED
@@ -20,7 +20,7 @@ Shipped today:
20
20
  - `vibecompass end-session` as a discoverable alias for `close-session`
21
21
  - `vibecompass sync-agents` for generated `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, and `.github/copilot-instructions.md` views
22
22
  - `vibecompass docs-review --guided` — print a comprehensive review prompt for your chosen LLM to run
23
- - `vibecompass docs-review --submit-hosted` — submit the review request to the hosted app (execution paused; details below)
23
+ - `vibecompass docs-review --submit-hosted` — submit the review request to the hosted app and poll for proposal/artifact output
24
24
  - `vibecompass docs-review --run-local --provider anthropic` — local provider adapter that saves review output locally
25
25
  - `vibecompass docs-review --apply-output` — apply accepted architecture-doc blocks into canonical `architecture/` docs
26
26
  - `vibecompass docs-review --complete` — mark the review accepted after the docs land
@@ -153,15 +153,17 @@ VibeCompass dogfoods: read project memory first, inspect source evidence,
153
153
  write component docs with review metadata, preserve the starter overview, and
154
154
  emit accepted docs as `vibecompass-architecture-doc` blocks.
155
155
 
156
- The package never calls an AI itself — that's intentional. You stay in
157
- control of which provider runs the review and what gets saved.
156
+ Plain `--guided` never calls an AI itself. You stay in control of which
157
+ provider runs the review and what gets saved. Use `--run-local --provider
158
+ anthropic` when you want the package to execute the same review through your
159
+ local Anthropic key.
158
160
 
159
161
  ### Other ways to run a review
160
162
 
161
163
  - `--submit-hosted` sends the review request to the hosted app and records a
162
- run ID. The hosted worker that actually runs the review and writes docs
163
- isn't live yet, so the submission is durably accepted but no scan runs
164
- against it.
164
+ run ID. Hosted execution creates proposals and docs-review artifacts; for
165
+ local-primary projects, canonical local files change only after
166
+ pull-preview/export/apply and a confirming push.
165
167
  - `--run-local --provider anthropic` calls Anthropic directly with your
166
168
  `ANTHROPIC_API_KEY` and saves the raw output to
167
169
  `state/docs-review-output.md`.
@@ -243,7 +245,23 @@ npx -y @vibecompass/vibecompass sync-agents --root .compass
243
245
  Content outside those markers is preserved. Existing files without markers are
244
246
  reported as warnings and left untouched.
245
247
 
246
- Create a `local-primary` root when you also want the hosted sync binding written into `project.yaml`:
248
+ Connect hosted VibeCompass later when you want browser views, team sync, or hosted docs-review:
249
+
250
+ ```bash
251
+ npx -y @vibecompass/vibecompass connect-hosted \
252
+ --sync-api-url https://vibecompass.dev \
253
+ --sync-project-id vc_proj_example \
254
+ --sync-credential-env-var VIBECOMPASS_SYNC_TOKEN
255
+ ```
256
+
257
+ Then set the secret locally and push the first baseline:
258
+
259
+ ```bash
260
+ export VIBECOMPASS_SYNC_TOKEN='your-sync-token'
261
+ npx -y @vibecompass/vibecompass push
262
+ ```
263
+
264
+ Create a `local-primary` root non-interactively when you also want the hosted sync binding written into `project.yaml` during init:
247
265
 
248
266
  ```bash
249
267
  npx -y @vibecompass/vibecompass init \
@@ -269,14 +287,8 @@ That creates:
269
287
  manifest.json
270
288
  ```
271
289
 
272
- Next step after init:
273
-
274
- ```bash
275
- export VIBECOMPASS_SYNC_TOKEN='your-sync-token'
276
- ```
277
-
278
- `vibecompass` does not yet perform hosted sync itself. The sync fields in
279
- `project.yaml` are the non-secret binding for the upcoming push/pull flows.
290
+ The sync fields in `project.yaml` are non-secret. The token value stays in
291
+ your environment and is never written to canonical project memory.
280
292
 
281
293
  ## CLI
282
294
 
@@ -296,9 +308,9 @@ Options:
296
308
  - `--guided`: ask placement and setup questions interactively
297
309
  - `--repo <id=remote>`: repeatable repo descriptor
298
310
  - `--repo-branch <id=branch>`: optional per-repo default branch
299
- - `--sync-api-url <url>`: hosted sync API URL for `local-primary`
300
- - `--sync-project-id <id>`: hosted sync project ID for `local-primary`
301
- - `--sync-credential-env-var <name>`: local env-var reference for `local-primary`
311
+ - `--sync-api-url <url>`: hosted sync API URL for `local-primary` or `hosted-only`
312
+ - `--sync-project-id <id>`: hosted sync project ID for `local-primary` or `hosted-only`
313
+ - `--sync-credential-env-var <name>`: local env-var reference for `local-primary` or `hosted-only`
302
314
  - `--with-workflow`: generate `context.md` plus workflow guide files
303
315
  - `--with-claude`: create a starter `CLAUDE.md` if it does not already exist
304
316
  - `--with-agents`: create a starter `AGENTS.md` if it does not already exist
@@ -315,6 +327,31 @@ Guided init can also record workflow defaults in
315
327
  `project.yaml.metadata.workflow`, including whether close-session should
316
328
  include a Git publish step and which remote name to use for that step.
317
329
 
330
+ Guided init is inference-first for the common single-repo case: it reads
331
+ `package.json`, detects the current Git `origin`, asks you to confirm the
332
+ detected repo, and maps a friendly setup goal to the stored project mode.
333
+ Hosted credentials are deferred by default; run `vibecompass connect-hosted`
334
+ when you are ready to connect the local root to `vibecompass.dev`.
335
+
336
+ ### `vibecompass connect-hosted`
337
+
338
+ ```text
339
+ vibecompass connect-hosted [options]
340
+ ```
341
+
342
+ Options:
343
+
344
+ - `--root <path>`: project-memory root; defaults to `.compass`
345
+ - `--sync-api-url <url>`: hosted sync API URL
346
+ - `--sync-project-id <id>`: hosted sync project ID
347
+ - `--sync-credential-env-var <name>`: local env-var reference
348
+
349
+ Without all three sync flags, `connect-hosted` prompts for the missing fields.
350
+ It updates only the non-secret `project.yaml.sync` binding; you still set the
351
+ actual token in your environment before running hosted commands.
352
+ Running it again replaces the existing hosted binding in `project.yaml`, which
353
+ is the intended rotation path after you create a new sync credential.
354
+
318
355
  ### `vibecompass start-session`
319
356
 
320
357
  ```text
@@ -417,7 +454,7 @@ Options:
417
454
 
418
455
  - `--root <path>`: project-memory root; defaults to `.compass`
419
456
  - `--guided`: print a comprehensive review prompt for your chosen LLM to run (recommended)
420
- - `--submit-hosted`: submit the review request to the hosted app (execution paused; submission durable)
457
+ - `--submit-hosted`: submit the review request to the hosted app
421
458
  - `--run-local`: run the generated review request with a local provider
422
459
  - `--provider <name>`: local provider for `--run-local`; currently `anthropic`
423
460
  - `--run-local-anthropic`: compatibility alias for `--run-local --provider anthropic`
@@ -443,9 +480,11 @@ Project-mode requirements:
443
480
  hosted binding, so hosted precedence does not apply when that flag is set.
444
481
 
445
482
  `--submit-hosted` uses `sync.credential_env_var` and records the hosted run
446
- ID in `state/docs-review.json`. Submission is durable today; the hosted
447
- worker that runs the review and writes canonical docs is not yet live, so
448
- no scan executes against the submitted request. Local files stay untouched.
483
+ ID in `state/docs-review.json`. Hosted execution creates review proposals and
484
+ artifacts; for `local-primary` roots, local files stay untouched until you
485
+ pull-preview, export, apply locally, and push to confirm the accepted output.
486
+ Missing or stale hosted baselines fail with package-facing next-step guidance
487
+ instead of running model work against the wrong source of truth.
449
488
 
450
489
  `--run-local-anthropic` remains as a compatibility alias. New scripts should
451
490
  use `--run-local --provider anthropic`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecompass/vibecompass",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Local-first project-memory core for VibeCompass",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -2,11 +2,17 @@
2
2
 
3
3
  import { realpathSync } from 'node:fs';
4
4
  import { pathToFileURL } from 'node:url';
5
- import { initializeProjectMemory } from './init.js';
5
+ import { connectHostedProjectMemory, initializeProjectMemory } from './init.js';
6
6
  import { preflightDocsReview } from './docs-review.js';
7
- import { resolveInitCliOptions } from './setup.js';
7
+ import { resolveConnectHostedCliOptions, resolveInitCliOptions } from './setup.js';
8
8
  import { closeProjectSession, listProjectSessions, startProjectSession, switchProjectSession } from './session.js';
9
9
  import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
10
+ import {
11
+ applyPullExport,
12
+ pullExportProjectMemory,
13
+ pullPreviewProjectMemory,
14
+ pushProjectMemory,
15
+ } from './sync.js';
10
16
 
11
17
  export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
12
18
  const parsed = parseCliArgs(argv);
@@ -41,7 +47,20 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
41
47
  );
42
48
 
43
49
  if (result.syncEnvVar) {
44
- io.stdout.write(`Next step: set ${result.syncEnvVar} locally before your first sync.\n`);
50
+ const rootFlag = formatOptionalRootFlag(initPlan.initOptions.rootDir);
51
+ io.stdout.write(`Hosted binding: configured for ${initPlan.initOptions.mode}\n`);
52
+ io.stdout.write(`Next step: set ${result.syncEnvVar} locally before your first hosted command.\n`);
53
+ if (initPlan.initOptions.mode === 'local-primary') {
54
+ io.stdout.write(`Then run: vibecompass push${rootFlag}\n`);
55
+ io.stdout.write(`Hosted docs-review: vibecompass docs-review --submit-hosted${rootFlag}\n`);
56
+ } else if (initPlan.initOptions.mode === 'hosted-only') {
57
+ io.stdout.write(`Hosted docs-review: vibecompass docs-review --submit-hosted${rootFlag}\n`);
58
+ }
59
+ } else if (initPlan.initOptions.mode === 'hosted-only') {
60
+ io.stdout.write('Hosted-only projects need a hosted sync binding before hosted commands.\n');
61
+ io.stdout.write('After creating a sync credential, rerun init with --force and --sync-api-url/--sync-project-id/--sync-credential-env-var, or add project.yaml.sync manually.\n');
62
+ } else if (initPlan.initOptions.mode === 'local-primary') {
63
+ io.stdout.write('Optional hosted setup later: vibecompass connect-hosted\n');
45
64
  }
46
65
 
47
66
  if (result.contextFilePath) {
@@ -92,6 +111,28 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
92
111
  return 0;
93
112
  }
94
113
 
114
+ if (parsed.command === 'connect-hosted') {
115
+ const connectPlan = await resolveConnectHostedCliOptions(parsed.options, {
116
+ cwd: runtime.cwd,
117
+ io,
118
+ runtime,
119
+ });
120
+ const result = await connectHostedProjectMemory({
121
+ ...connectPlan,
122
+ ...(runtime.cwd ? { cwd: runtime.cwd } : {}),
123
+ });
124
+ io.stdout.write(`Connected hosted VibeCompass for ${result.mode}\n`);
125
+ io.stdout.write(`Updated ${result.projectFilePath}\n`);
126
+ io.stdout.write(`Next step: set ${result.syncEnvVar} locally before your first hosted command.\n`);
127
+ if (result.mode === 'local-primary') {
128
+ io.stdout.write('Then run: vibecompass push\n');
129
+ io.stdout.write('Hosted docs-review: vibecompass docs-review --submit-hosted\n');
130
+ } else {
131
+ io.stdout.write('Hosted docs-review: vibecompass docs-review --submit-hosted\n');
132
+ }
133
+ return 0;
134
+ }
135
+
95
136
  if (parsed.command === 'close-session' || parsed.command === 'end-session') {
96
137
  const result = await closeProjectSession({
97
138
  ...parsed.options,
@@ -168,6 +209,9 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
168
209
  }
169
210
  if (result.hosted) {
170
211
  io.stdout.write(`Hosted run: ${result.hosted.run_id}\n`);
212
+ if (result.hosted.phase) {
213
+ io.stdout.write(`Hosted phase: ${result.hosted.phase}\n`);
214
+ }
171
215
  }
172
216
  if (result.localReview) {
173
217
  io.stdout.write(`Local review output: ${result.localReview.output_path}\n`);
@@ -178,6 +222,11 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
178
222
  io.stdout.write(`- ${entry.path} (${entry.status})\n`);
179
223
  }
180
224
  }
225
+ if (result.appliedDecisionArtifact) {
226
+ io.stdout.write(`Applied decision artifact: ${result.appliedDecisionArtifact.artifact_id}\n`);
227
+ io.stdout.write(`Decision: D-${String(result.appliedDecisionArtifact.decision_id).padStart(3, '0')}\n`);
228
+ io.stdout.write(`Target: ${result.appliedDecisionArtifact.target_path}\n`);
229
+ }
181
230
  writeWarnings(io, result.warnings);
182
231
  io.stdout.write(`Recorded ${result.statePath}\n`);
183
232
  io.stdout.write(`${result.message}\n`);
@@ -188,6 +237,80 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
188
237
  return 0;
189
238
  }
190
239
 
240
+ if (parsed.command === 'push') {
241
+ const result = await pushProjectMemory(parsed.options, {
242
+ cwd: runtime.cwd,
243
+ env: runtime.env,
244
+ runtime,
245
+ });
246
+ io.stdout.write(`Push: ${result.status}\n`);
247
+ io.stdout.write(`Remote revision: ${result.remoteRevisionId}\n`);
248
+ io.stdout.write(`Run: ${result.runId}\n`);
249
+ if (result.appliedProposalIds.length > 0) {
250
+ io.stdout.write(`Applied proposals: ${result.appliedProposalIds.join(', ')}\n`);
251
+ }
252
+ if (result.staleProposalIds.length > 0) {
253
+ io.stdout.write(`Stale proposals: ${result.staleProposalIds.join(', ')}\n`);
254
+ }
255
+ io.stdout.write(`Recorded ${result.manifestPath}\n`);
256
+ io.stdout.write(`${result.message}\n`);
257
+ return 0;
258
+ }
259
+
260
+ if (parsed.command === 'pull-preview') {
261
+ const result = await pullPreviewProjectMemory(parsed.options, {
262
+ cwd: runtime.cwd,
263
+ env: runtime.env,
264
+ runtime,
265
+ });
266
+ io.stdout.write(`Pull-preview: ${result.status}\n`);
267
+ io.stdout.write(`Preview token: ${result.previewToken}\n`);
268
+ io.stdout.write(`Pending proposals: ${result.proposals.length}\n`);
269
+ for (const proposal of result.proposals) {
270
+ io.stdout.write(`- ${proposal.proposal_id}: ${proposal.summary}\n`);
271
+ }
272
+ if (result.conflicts.length > 0) {
273
+ io.stdout.write(`Conflicts: ${result.conflicts.length}\n`);
274
+ for (const conflict of result.conflicts) {
275
+ io.stdout.write(`- ${conflict.code}: ${conflict.message}\n`);
276
+ }
277
+ }
278
+ writeWarnings(io, result.warnings.map((warning) => `${warning.code}: ${warning.message}`));
279
+ io.stdout.write(`Recorded ${result.manifestPath}\n`);
280
+ io.stdout.write(`${result.message}\n`);
281
+ return 0;
282
+ }
283
+
284
+ if (parsed.command === 'pull-export') {
285
+ const result = await pullExportProjectMemory(parsed.options, {
286
+ cwd: runtime.cwd,
287
+ env: runtime.env,
288
+ runtime,
289
+ });
290
+ io.stdout.write(`Pull-export: ${result.status}\n`);
291
+ io.stdout.write(`Run: ${result.runId}\n`);
292
+ io.stdout.write(`Proposal IDs: ${result.proposalIds.join(', ')}\n`);
293
+ io.stdout.write(`Operations: ${result.operationCount}\n`);
294
+ io.stdout.write(`Wrote ${result.outputPath}\n`);
295
+ io.stdout.write(`${result.message}\n`);
296
+ return 0;
297
+ }
298
+
299
+ if (parsed.command === 'apply-export') {
300
+ const result = await applyPullExport(parsed.options, {
301
+ cwd: runtime.cwd,
302
+ env: runtime.env,
303
+ runtime,
304
+ });
305
+ io.stdout.write(`Apply-export: ${result.status}\n`);
306
+ for (const entry of result.applied) {
307
+ io.stdout.write(`- ${entry.path} (${entry.status})\n`);
308
+ }
309
+ io.stdout.write(`Recorded ${result.outputPath}\n`);
310
+ io.stdout.write(`${result.message}\n`);
311
+ return 0;
312
+ }
313
+
191
314
  throw new Error(`Unknown command "${parsed.command}".`);
192
315
  }
193
316
 
@@ -203,6 +326,10 @@ export function parseCliArgs(argv) {
203
326
  return parseStartSessionArgs(rest);
204
327
  }
205
328
 
329
+ if (command === 'connect-hosted') {
330
+ return parseConnectHostedArgs(rest);
331
+ }
332
+
206
333
  if (command === 'close-session' || command === 'end-session') {
207
334
  return parseCloseSessionArgs(rest);
208
335
  }
@@ -223,6 +350,22 @@ export function parseCliArgs(argv) {
223
350
  return parseDocsReviewArgs(rest);
224
351
  }
225
352
 
353
+ if (command === 'push') {
354
+ return parsePushArgs(rest);
355
+ }
356
+
357
+ if (command === 'pull-preview') {
358
+ return parsePullPreviewArgs(rest);
359
+ }
360
+
361
+ if (command === 'pull-export') {
362
+ return parsePullExportArgs(rest);
363
+ }
364
+
365
+ if (command === 'apply-export') {
366
+ return parseApplyExportArgs(rest);
367
+ }
368
+
226
369
  return { command };
227
370
  }
228
371
 
@@ -448,6 +591,63 @@ function parseStartSessionArgs(argv) {
448
591
  };
449
592
  }
450
593
 
594
+ function parseConnectHostedArgs(argv) {
595
+ const parsed = {};
596
+
597
+ for (let index = 0; index < argv.length; index += 1) {
598
+ const token = argv[index];
599
+
600
+ if (!token.startsWith('--')) {
601
+ throw new Error(`Unexpected argument "${token}".`);
602
+ }
603
+
604
+ const value = argv[index + 1];
605
+ if (value === undefined) {
606
+ throw new Error(`Flag "${token}" requires a value.`);
607
+ }
608
+ index += 1;
609
+
610
+ switch (token) {
611
+ case '--root':
612
+ parsed.rootDir = value;
613
+ break;
614
+ case '--sync-api-url':
615
+ parsed.syncApiUrl = value;
616
+ break;
617
+ case '--sync-project-id':
618
+ parsed.syncProjectId = value;
619
+ break;
620
+ case '--sync-credential-env-var':
621
+ parsed.syncCredentialEnvVar = value;
622
+ break;
623
+ default:
624
+ throw new Error(`Unknown flag "${token}".`);
625
+ }
626
+ }
627
+
628
+ const syncValues = [
629
+ parsed.syncApiUrl,
630
+ parsed.syncProjectId,
631
+ parsed.syncCredentialEnvVar,
632
+ ].filter(Boolean);
633
+
634
+ return {
635
+ command: 'connect-hosted',
636
+ options: {
637
+ rootDir: parsed.rootDir,
638
+ ...(syncValues.length > 0
639
+ ? {
640
+ sync: {
641
+ apiUrl: parsed.syncApiUrl,
642
+ projectId: parsed.syncProjectId,
643
+ credentialEnvVar: parsed.syncCredentialEnvVar,
644
+ },
645
+ }
646
+ : {}),
647
+ },
648
+ };
649
+ }
650
+
451
651
  function parseCloseSessionArgs(argv) {
452
652
  const parsed = {
453
653
  completed: [],
@@ -651,6 +851,11 @@ function parseDocsReviewArgs(argv) {
651
851
  continue;
652
852
  }
653
853
 
854
+ if (token === '--poll-hosted') {
855
+ parsed.pollHosted = true;
856
+ continue;
857
+ }
858
+
654
859
  if (token === '--complete') {
655
860
  parsed.complete = true;
656
861
  continue;
@@ -671,6 +876,16 @@ function parseDocsReviewArgs(argv) {
671
876
  continue;
672
877
  }
673
878
 
879
+ if (token === '--apply-decision-artifact') {
880
+ parsed.applyDecisionArtifact = true;
881
+ continue;
882
+ }
883
+
884
+ if (token === '--refresh-index') {
885
+ parsed.refreshIndex = true;
886
+ continue;
887
+ }
888
+
674
889
  if (!token.startsWith('--')) {
675
890
  throw new Error(`Unexpected argument "${token}".`);
676
891
  }
@@ -700,6 +915,9 @@ function parseDocsReviewArgs(argv) {
700
915
  case '--output':
701
916
  parsed.outputPath = value;
702
917
  break;
918
+ case '--artifact':
919
+ parsed.artifactId = value;
920
+ break;
703
921
  case '--max-tokens':
704
922
  parsed.maxTokens = value;
705
923
  break;
@@ -714,6 +932,151 @@ function parseDocsReviewArgs(argv) {
714
932
  };
715
933
  }
716
934
 
935
+ function parsePushArgs(argv) {
936
+ return {
937
+ command: 'push',
938
+ options: parseRootOnlyArgs(argv, 'push'),
939
+ };
940
+ }
941
+
942
+ function parsePullPreviewArgs(argv) {
943
+ const parsed = {};
944
+
945
+ for (let index = 0; index < argv.length; index += 1) {
946
+ const token = argv[index];
947
+ if (token === '--no-pending-proposals') {
948
+ parsed.includePendingProposals = false;
949
+ continue;
950
+ }
951
+
952
+ if (!token.startsWith('--')) {
953
+ throw new Error(`Unexpected argument "${token}".`);
954
+ }
955
+
956
+ const value = argv[index + 1];
957
+ if (value === undefined) {
958
+ throw new Error(`Flag "${token}" requires a value.`);
959
+ }
960
+ index += 1;
961
+
962
+ switch (token) {
963
+ case '--root':
964
+ parsed.rootDir = value;
965
+ break;
966
+ default:
967
+ throw new Error(`Unknown flag "${token}".`);
968
+ }
969
+ }
970
+
971
+ return {
972
+ command: 'pull-preview',
973
+ options: parsed,
974
+ };
975
+ }
976
+
977
+ function parsePullExportArgs(argv) {
978
+ const parsed = {
979
+ proposalIds: [],
980
+ };
981
+
982
+ for (let index = 0; index < argv.length; index += 1) {
983
+ const token = argv[index];
984
+
985
+ if (!token.startsWith('--')) {
986
+ throw new Error(`Unexpected argument "${token}".`);
987
+ }
988
+
989
+ const value = argv[index + 1];
990
+ if (value === undefined) {
991
+ throw new Error(`Flag "${token}" requires a value.`);
992
+ }
993
+ index += 1;
994
+
995
+ switch (token) {
996
+ case '--root':
997
+ parsed.rootDir = value;
998
+ break;
999
+ case '--preview-token':
1000
+ parsed.previewToken = value;
1001
+ break;
1002
+ case '--proposal':
1003
+ parsed.proposalIds.push(value);
1004
+ break;
1005
+ case '--output':
1006
+ parsed.outputPath = value;
1007
+ break;
1008
+ default:
1009
+ throw new Error(`Unknown flag "${token}".`);
1010
+ }
1011
+ }
1012
+
1013
+ return {
1014
+ command: 'pull-export',
1015
+ options: parsed,
1016
+ };
1017
+ }
1018
+
1019
+ function parseApplyExportArgs(argv) {
1020
+ const parsed = {};
1021
+
1022
+ for (let index = 0; index < argv.length; index += 1) {
1023
+ const token = argv[index];
1024
+
1025
+ if (!token.startsWith('--')) {
1026
+ throw new Error(`Unexpected argument "${token}".`);
1027
+ }
1028
+
1029
+ const value = argv[index + 1];
1030
+ if (value === undefined) {
1031
+ throw new Error(`Flag "${token}" requires a value.`);
1032
+ }
1033
+ index += 1;
1034
+
1035
+ switch (token) {
1036
+ case '--root':
1037
+ parsed.rootDir = value;
1038
+ break;
1039
+ case '--output':
1040
+ parsed.outputPath = value;
1041
+ break;
1042
+ default:
1043
+ throw new Error(`Unknown flag "${token}".`);
1044
+ }
1045
+ }
1046
+
1047
+ return {
1048
+ command: 'apply-export',
1049
+ options: parsed,
1050
+ };
1051
+ }
1052
+
1053
+ function parseRootOnlyArgs(argv, commandName) {
1054
+ const parsed = {};
1055
+
1056
+ for (let index = 0; index < argv.length; index += 1) {
1057
+ const token = argv[index];
1058
+ if (!token.startsWith('--')) {
1059
+ throw new Error(`Unexpected argument "${token}".`);
1060
+ }
1061
+
1062
+ const value = argv[index + 1];
1063
+ if (value === undefined) {
1064
+ throw new Error(`Flag "${token}" requires a value.`);
1065
+ }
1066
+ index += 1;
1067
+
1068
+ switch (token) {
1069
+ case '--root':
1070
+ parsed.rootDir = value;
1071
+ break;
1072
+ default:
1073
+ throw new Error(`Unknown flag "${token}" for ${commandName}.`);
1074
+ }
1075
+ }
1076
+
1077
+ return parsed;
1078
+ }
1079
+
717
1080
  function writeAgentFileSyncResult(io, result) {
718
1081
  if (!result) {
719
1082
  return;
@@ -746,12 +1109,17 @@ function usageText() {
746
1109
  return [
747
1110
  'Usage:',
748
1111
  ' vibecompass init --name <project-name> --mode <local-only|local-primary|hosted-only> --repo <id=remote> [options]',
1112
+ ' vibecompass connect-hosted [options]',
749
1113
  ' vibecompass start-session --id <lane-id> --working-on <text> [options]',
750
1114
  ' vibecompass close-session --title <text> --completed <text> --next-step <text> [options]',
751
1115
  ' vibecompass end-session --title <text> --completed <text> --next-step <text> [options] # alias',
752
1116
  ' vibecompass list-sessions [options]',
753
1117
  ' vibecompass switch-session <id> [options]',
754
1118
  ' vibecompass sync-agents [options]',
1119
+ ' vibecompass push [options]',
1120
+ ' vibecompass pull-preview [options]',
1121
+ ' vibecompass pull-export [options]',
1122
+ ' vibecompass apply-export [options]',
755
1123
  ' vibecompass docs-review --guided [options]',
756
1124
  '',
757
1125
  'Init options:',
@@ -777,6 +1145,13 @@ function usageText() {
777
1145
  ' --close-session-git-remote <name> Default Git remote name for that close-session publish step',
778
1146
  ' --force Overwrite an existing project.yaml',
779
1147
  '',
1148
+ 'Connect-hosted options:',
1149
+ ' --root <path> Project-memory root. Defaults to .compass',
1150
+ ' --sync-api-url <url> Hosted sync api_url',
1151
+ ' --sync-project-id <id> Hosted sync project_id',
1152
+ ' --sync-credential-env-var <name> Hosted sync env var reference',
1153
+ ' Replaces any existing project.yaml sync binding',
1154
+ '',
780
1155
  'Start-session options:',
781
1156
  ' --root <path> Project-memory root. Defaults to .compass',
782
1157
  ' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd',
@@ -814,16 +1189,29 @@ function usageText() {
814
1189
  ' --format <name> Optional format: claude_md, agents_md, cursor_rules, copilot_instructions',
815
1190
  ' --dry-run Show planned writes without changing files',
816
1191
  '',
1192
+ 'Sync transport options:',
1193
+ ' push --root <path> Push canonical local project memory to hosted',
1194
+ ' pull-preview --root <path> Preview hosted proposals and remote state',
1195
+ ' pull-preview --no-pending-proposals Exclude pending proposals from preview',
1196
+ ' pull-export --proposal <id> Export a selected proposal; repeatable',
1197
+ ' pull-export --preview-token <token> Export from a specific preview token',
1198
+ ' pull-export --output <path> Output path under root; defaults to state/pull-export.json',
1199
+ ' apply-export --output <path> Apply exported bundle; defaults to state/pull-export.json',
1200
+ '',
817
1201
  'Docs-review options:',
818
1202
  ' --root <path> Project-memory root. Defaults to .compass',
819
1203
  ' --guided Accepted for the explicit comprehensive-review workflow',
820
1204
  ' --submit-hosted Submit the generated review request to hosted sync',
1205
+ ' --poll-hosted Poll hosted docs-review run status and update state/docs-review.json',
821
1206
  ' --complete Mark accepted docs-review changes as completed locally',
822
1207
  ' --run-local Run the generated review request with a local provider',
823
1208
  ' --provider <name> Local provider for --run-local. Currently: anthropic',
824
1209
  ' --run-local-anthropic Compatibility alias for --run-local --provider anthropic',
825
1210
  ' --apply-output Apply accepted architecture doc blocks from review output',
826
1211
  ' --output <path> Review output path for --apply-output; defaults to state/docs-review-output.md',
1212
+ ' --apply-decision-artifact Append an accepted hosted decision artifact locally',
1213
+ ' --artifact <id> Artifact ID for --apply-decision-artifact',
1214
+ ' --refresh-index Also regenerate decisions/INDEX.md after applying a decision artifact',
827
1215
  ' --llm <name> Preferred LLM/provider to run the external architecture review',
828
1216
  ' --model <name> Model name to record for the review',
829
1217
  ' --anthropic-env-var <name> Env var to use for local Anthropic docs-review runtime',
@@ -839,6 +1227,14 @@ function createDefaultIo() {
839
1227
  };
840
1228
  }
841
1229
 
1230
+ function formatOptionalRootFlag(rootDir) {
1231
+ if (!rootDir || rootDir === '.compass') {
1232
+ return '';
1233
+ }
1234
+
1235
+ return ` --root ${rootDir}`;
1236
+ }
1237
+
842
1238
  async function main() {
843
1239
  try {
844
1240
  const exitCode = await runCli(process.argv.slice(2));