@indigoai-us/hq-cli 5.345.53 → 5.345.55

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 (37) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/assets/bot-workers/setup/context/USER-GUIDE.md +1 -1
  3. package/assets/bot-workers/setup/skills/agents-and-team.md +1 -1
  4. package/assets/bot-workers/setup/skills/pricing.md +1 -1
  5. package/dist/command-catalog.generated.d.ts +103 -2
  6. package/dist/command-catalog.generated.js +124 -2
  7. package/dist/command-registration-plan.d.ts +6 -1
  8. package/dist/command-registration-plan.js +2 -1
  9. package/dist/commands/cloud.d.ts +7 -0
  10. package/dist/commands/cloud.js +92 -16
  11. package/dist/commands/daemon.js +16 -0
  12. package/dist/commands/import-scan.d.ts +24 -0
  13. package/dist/commands/import-scan.js +75 -0
  14. package/dist/commands/lanes-session-start.d.ts +22 -0
  15. package/dist/commands/lanes-session-start.js +215 -0
  16. package/dist/commands/lanes-watch.d.ts +2 -0
  17. package/dist/commands/lanes-watch.js +2 -1
  18. package/dist/commands/lanes.js +4 -2
  19. package/dist/lib/billing/plan-lock.d.ts +2 -2
  20. package/dist/lib/billing/plan-lock.js +2 -2
  21. package/dist/lib/daemon/conflict-notices.d.ts +4 -0
  22. package/dist/lib/daemon/conflict-notices.js +80 -0
  23. package/dist/lib/import-scan/events.d.ts +135 -0
  24. package/dist/lib/import-scan/events.js +71 -0
  25. package/dist/lib/import-scan/run.d.ts +84 -0
  26. package/dist/lib/import-scan/run.js +308 -0
  27. package/dist/lib/import-scan/scan-event.schema.json +127 -0
  28. package/dist/lib/import-scan/validate.d.ts +31 -0
  29. package/dist/lib/import-scan/validate.js +111 -0
  30. package/dist/lib/lanes/flag-keys.d.ts +2 -0
  31. package/dist/lib/lanes/flag-keys.js +2 -0
  32. package/dist/lib/lanes/monitor.js +1 -1
  33. package/dist/lib/lanes/registry-flag.d.ts +2 -0
  34. package/dist/lib/lanes/registry-flag.js +1 -1
  35. package/dist/utils/manifest.d.ts +7 -1
  36. package/dist/utils/manifest.js +14 -6
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,7 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.55] — 2026-10-08
6
+ - Added `hq daemon sync conflicts acknowledge <id>` so the desktop can clear a parked-conflict notice without writing runner-owned sync state.
7
+ - Add `hq import scan`, which runs the HQ import-context scanner on this
8
+ machine (local only) and reports conversation history, repositories,
9
+ companies and projects. `--json --stream` prints the scan as v1 JSON Lines
10
+ events (`start`, `source`, `count`, `company`, `project`, `error`, `done`)
11
+ for the HQ desktop first-run setup; `--json` prints only the final `done`
12
+ object. Each scanner line is validated against a schema and rebuilt from its
13
+ v1 fields; invalid lines are dropped with a warning. Scanner diagnostics are
14
+ shown with paths and token-like strings redacted. The HQ folder comes from
15
+ `--hq-root` or the HQ installer's record only. When the scan cannot run,
16
+ fails or is cancelled, the command emits an `error` event and a `done` and
17
+ exits non-zero; cancelling stops the whole scanner process group. Event
18
+ types are exported from `dist/lib/import-scan/events.js`. Needs an hq-core
19
+ release whose import-context scanner supports `--progress-json`.
20
+ - Exit with actionable recovery steps when a vault upload is refused.
21
+
22
+ ## [5.345.54] — 2026-10-08
23
+ - Add `hq lanes session-start` for conduct-mode SessionStart bootstrapping. `HQ_LANE_ENGINE` is the preferred engine environment variable; `HQ_CONDUCT_ENGINE` remains a deprecated fallback alias. Show `hq lanes` in root help.
24
+
25
+ - Prevents `hq` commands run outside an HQ tree on Windows from hanging while searching parent directories for an HQ root.
26
+
5
27
  ## [5.345.53] — 2026-10-08
28
+ - Starter plan-limit notices quote 3 integrations when the server sends no integration limit, up from 1. The setup guide, pricing skill, and agents-and-team skill now say Starter includes 3 integrations (connected apps).
29
+
6
30
  - `hq meetings live` now also lists live meetings you were invited to but did not record, such as a standup a teammate's notetaker is recording, so you can read their live transcript.
7
31
 
8
32
  ## [5.345.52] — 2026-10-08
@@ -323,7 +323,7 @@ HQ/
323
323
  HQ has three plans: **Starter**, **HQ Workforce** ($500/month per workspace), and
324
324
  **Enterprise**.
325
325
 
326
- Starter is free. It holds up to 5 members and includes 1 integration. MCP,
326
+ Starter is free. It holds up to 5 members and includes 3 integrations. MCP,
327
327
  Atlas, and telemetry are not included. Everything else — knowledge, projects,
328
328
  policies, workers, secrets, vault storage, `hq deploy`, and `hq share` — works
329
329
  the same as on the paid plans.
@@ -29,7 +29,7 @@ Run `hq whoami` and `hq companies list`, and read the "Owner context" block.
29
29
  3. **Their plan, when the flow depends on it.** Check it as
30
30
  `skills/pricing.md` says; never guess it. Inviting people, sharing a
31
31
  connected app and sharing a key are included in every plan (within
32
- Starter's limits of 5 people, 1 connected app and 10 keys). Hosted agents,
32
+ Starter's limits of 5 people, 3 connected apps and 10 keys). Hosted agents,
33
33
  Slack agents included, are not part of Starter: they need HQ Workforce
34
34
  ($500 a month for the company), and then each agent is $100 a month on
35
35
  top. If HQ refuses something or quotes a price, pass that answer on.
@@ -36,7 +36,7 @@ often empty; when it is empty it tells you nothing about the plan.
36
36
  **Starter, free.** For a company getting started:
37
37
 
38
38
  - up to 5 people (invitations not yet accepted count toward the 5)
39
- - 1 connected app
39
+ - 3 connected apps
40
40
  - 10 keys
41
41
  - 10 GB of files and 500 site deploys
42
42
  - no hosted agents (see "Agents" below)
@@ -5144,6 +5144,62 @@ export declare const COMMAND_CATALOG: readonly [{
5144
5144
  }];
5145
5145
  readonly subcommands: readonly [];
5146
5146
  }];
5147
+ }, {
5148
+ readonly name: "import";
5149
+ readonly description: "Import existing AI context (Claude Code, Codex, repos) into HQ";
5150
+ readonly aliases: readonly [];
5151
+ readonly hidden: false;
5152
+ readonly helpOption: true;
5153
+ readonly helpCommand: true;
5154
+ readonly usage: "[options] [command]";
5155
+ readonly helpText: readonly [];
5156
+ readonly arguments: readonly [];
5157
+ readonly options: readonly [];
5158
+ readonly subcommands: readonly [{
5159
+ readonly name: "scan";
5160
+ readonly description: "Scan this machine for AI context (sessions, repos, companies, projects); local only";
5161
+ readonly aliases: readonly [];
5162
+ readonly hidden: false;
5163
+ readonly helpOption: true;
5164
+ readonly helpCommand: false;
5165
+ readonly usage: "[options]";
5166
+ readonly helpText: readonly [{
5167
+ readonly position: "after";
5168
+ readonly text: "\nThe desktop app runs: hq import scan --json --stream\nEvents (v1): start, source, count, company, project, error, done. See docs/import-scan-stream.md.";
5169
+ }];
5170
+ readonly arguments: readonly [];
5171
+ readonly options: readonly [{
5172
+ readonly flags: "--json";
5173
+ readonly description: "Print only the final done event as JSON";
5174
+ readonly hidden: false;
5175
+ }, {
5176
+ readonly flags: "--stream";
5177
+ readonly description: "With --json, print every scan event as JSON Lines while scanning";
5178
+ readonly hidden: false;
5179
+ }, {
5180
+ readonly flags: "--hq-root <path>";
5181
+ readonly description: "HQ folder (default: the folder the HQ installer recorded)";
5182
+ readonly hidden: false;
5183
+ }, {
5184
+ readonly flags: "--output <path>";
5185
+ readonly description: "Where to write report.json (default: <hq>/workspace/imports/<scan id>/report.json)";
5186
+ readonly hidden: false;
5187
+ }, {
5188
+ readonly flags: "--scope <dir>";
5189
+ readonly description: "Also scan this folder (repeatable)";
5190
+ readonly hidden: false;
5191
+ readonly defaultValue: readonly [];
5192
+ }, {
5193
+ readonly flags: "--claude-export <path>";
5194
+ readonly description: "Include a claude.ai data export (unzipped folder or conversations.json)";
5195
+ readonly hidden: false;
5196
+ }, {
5197
+ readonly flags: "--no-default-scopes";
5198
+ readonly description: "Scan only --scope folders, not the default code folders";
5199
+ readonly hidden: false;
5200
+ }];
5201
+ readonly subcommands: readonly [];
5202
+ }];
5147
5203
  }, {
5148
5204
  readonly name: "reindex";
5149
5205
  readonly description: "Surface namespaced skills, materialize legacy knowledge repos, regenerate the workers registry, and trust HQ hooks for Codex, Grok, and Claude Code";
@@ -10929,7 +10985,7 @@ export declare const COMMAND_CATALOG: readonly [{
10929
10985
  readonly name: "lanes";
10930
10986
  readonly description: "Create, supervise, and steer worker lanes; routed decision queue";
10931
10987
  readonly aliases: readonly [];
10932
- readonly hidden: true;
10988
+ readonly hidden: false;
10933
10989
  readonly helpOption: true;
10934
10990
  readonly helpCommand: true;
10935
10991
  readonly usage: "[options] [command]";
@@ -10937,6 +10993,22 @@ export declare const COMMAND_CATALOG: readonly [{
10937
10993
  readonly arguments: readonly [];
10938
10994
  readonly options: readonly [];
10939
10995
  readonly subcommands: readonly [{
10996
+ readonly name: "session-start";
10997
+ readonly description: "Internal SessionStart bootstrap";
10998
+ readonly aliases: readonly [];
10999
+ readonly hidden: true;
11000
+ readonly helpOption: true;
11001
+ readonly helpCommand: false;
11002
+ readonly usage: "[options]";
11003
+ readonly helpText: readonly [];
11004
+ readonly arguments: readonly [];
11005
+ readonly options: readonly [{
11006
+ readonly flags: "--json";
11007
+ readonly description: "JSON on stdout (always)";
11008
+ readonly hidden: false;
11009
+ }];
11010
+ readonly subcommands: readonly [];
11011
+ }, {
10940
11012
  readonly name: "checks";
10941
11013
  readonly description: "Evaluate tracker and lane health checks";
10942
11014
  readonly aliases: readonly [];
@@ -12184,7 +12256,7 @@ export declare const COMMAND_CATALOG: readonly [{
12184
12256
  readonly hidden: false;
12185
12257
  }, {
12186
12258
  readonly flags: "--engine <engine>";
12187
- readonly description: "claude | codex | grok; defaults to HQ_CONDUCT_ENGINE or claude";
12259
+ readonly description: "claude | codex | grok; defaults to HQ_LANE_ENGINE (HQ_CONDUCT_ENGINE is deprecated) or claude";
12188
12260
  readonly hidden: false;
12189
12261
  }, {
12190
12262
  readonly flags: "--reminder";
@@ -13756,6 +13828,35 @@ export declare const COMMAND_CATALOG: readonly [{
13756
13828
  readonly arguments: readonly [];
13757
13829
  readonly options: readonly [];
13758
13830
  readonly subcommands: readonly [{
13831
+ readonly name: "conflicts";
13832
+ readonly description: "Manage parked conflict copy notices";
13833
+ readonly aliases: readonly [];
13834
+ readonly hidden: false;
13835
+ readonly helpOption: true;
13836
+ readonly helpCommand: true;
13837
+ readonly usage: "[options] [command]";
13838
+ readonly helpText: readonly [];
13839
+ readonly arguments: readonly [];
13840
+ readonly options: readonly [];
13841
+ readonly subcommands: readonly [{
13842
+ readonly name: "acknowledge";
13843
+ readonly description: "Acknowledge a parked conflict copy notice";
13844
+ readonly aliases: readonly [];
13845
+ readonly hidden: false;
13846
+ readonly helpOption: true;
13847
+ readonly helpCommand: false;
13848
+ readonly usage: "[options] <noticeId>";
13849
+ readonly helpText: readonly [];
13850
+ readonly arguments: readonly [{
13851
+ readonly name: "noticeId";
13852
+ readonly required: true;
13853
+ readonly variadic: false;
13854
+ readonly description: "";
13855
+ }];
13856
+ readonly options: readonly [];
13857
+ readonly subcommands: readonly [];
13858
+ }];
13859
+ }, {
13759
13860
  readonly name: "status";
13760
13861
  readonly description: "Show sync ownership, heartbeat, last pass and log path";
13761
13862
  readonly aliases: readonly [];
@@ -6253,6 +6253,75 @@ export const COMMAND_CATALOG = [
6253
6253
  }
6254
6254
  ]
6255
6255
  },
6256
+ {
6257
+ "name": "import",
6258
+ "description": "Import existing AI context (Claude Code, Codex, repos) into HQ",
6259
+ "aliases": [],
6260
+ "hidden": false,
6261
+ "helpOption": true,
6262
+ "helpCommand": true,
6263
+ "usage": "[options] [command]",
6264
+ "helpText": [],
6265
+ "arguments": [],
6266
+ "options": [],
6267
+ "subcommands": [
6268
+ {
6269
+ "name": "scan",
6270
+ "description": "Scan this machine for AI context (sessions, repos, companies, projects); local only",
6271
+ "aliases": [],
6272
+ "hidden": false,
6273
+ "helpOption": true,
6274
+ "helpCommand": false,
6275
+ "usage": "[options]",
6276
+ "helpText": [
6277
+ {
6278
+ "position": "after",
6279
+ "text": "\nThe desktop app runs: hq import scan --json --stream\nEvents (v1): start, source, count, company, project, error, done. See docs/import-scan-stream.md."
6280
+ }
6281
+ ],
6282
+ "arguments": [],
6283
+ "options": [
6284
+ {
6285
+ "flags": "--json",
6286
+ "description": "Print only the final done event as JSON",
6287
+ "hidden": false
6288
+ },
6289
+ {
6290
+ "flags": "--stream",
6291
+ "description": "With --json, print every scan event as JSON Lines while scanning",
6292
+ "hidden": false
6293
+ },
6294
+ {
6295
+ "flags": "--hq-root <path>",
6296
+ "description": "HQ folder (default: the folder the HQ installer recorded)",
6297
+ "hidden": false
6298
+ },
6299
+ {
6300
+ "flags": "--output <path>",
6301
+ "description": "Where to write report.json (default: <hq>/workspace/imports/<scan id>/report.json)",
6302
+ "hidden": false
6303
+ },
6304
+ {
6305
+ "flags": "--scope <dir>",
6306
+ "description": "Also scan this folder (repeatable)",
6307
+ "hidden": false,
6308
+ "defaultValue": []
6309
+ },
6310
+ {
6311
+ "flags": "--claude-export <path>",
6312
+ "description": "Include a claude.ai data export (unzipped folder or conversations.json)",
6313
+ "hidden": false
6314
+ },
6315
+ {
6316
+ "flags": "--no-default-scopes",
6317
+ "description": "Scan only --scope folders, not the default code folders",
6318
+ "hidden": false
6319
+ }
6320
+ ],
6321
+ "subcommands": []
6322
+ }
6323
+ ]
6324
+ },
6256
6325
  {
6257
6326
  "name": "reindex",
6258
6327
  "description": "Surface namespaced skills, materialize legacy knowledge repos, regenerate the workers registry, and trust HQ hooks for Codex, Grok, and Claude Code",
@@ -13278,7 +13347,7 @@ export const COMMAND_CATALOG = [
13278
13347
  "name": "lanes",
13279
13348
  "description": "Create, supervise, and steer worker lanes; routed decision queue",
13280
13349
  "aliases": [],
13281
- "hidden": true,
13350
+ "hidden": false,
13282
13351
  "helpOption": true,
13283
13352
  "helpCommand": true,
13284
13353
  "usage": "[options] [command]",
@@ -13286,6 +13355,25 @@ export const COMMAND_CATALOG = [
13286
13355
  "arguments": [],
13287
13356
  "options": [],
13288
13357
  "subcommands": [
13358
+ {
13359
+ "name": "session-start",
13360
+ "description": "Internal SessionStart bootstrap",
13361
+ "aliases": [],
13362
+ "hidden": true,
13363
+ "helpOption": true,
13364
+ "helpCommand": false,
13365
+ "usage": "[options]",
13366
+ "helpText": [],
13367
+ "arguments": [],
13368
+ "options": [
13369
+ {
13370
+ "flags": "--json",
13371
+ "description": "JSON on stdout (always)",
13372
+ "hidden": false
13373
+ }
13374
+ ],
13375
+ "subcommands": []
13376
+ },
13289
13377
  {
13290
13378
  "name": "checks",
13291
13379
  "description": "Evaluate tracker and lane health checks",
@@ -14811,7 +14899,7 @@ export const COMMAND_CATALOG = [
14811
14899
  },
14812
14900
  {
14813
14901
  "flags": "--engine <engine>",
14814
- "description": "claude | codex | grok; defaults to HQ_CONDUCT_ENGINE or claude",
14902
+ "description": "claude | codex | grok; defaults to HQ_LANE_ENGINE (HQ_CONDUCT_ENGINE is deprecated) or claude",
14815
14903
  "hidden": false
14816
14904
  },
14817
14905
  {
@@ -16736,6 +16824,40 @@ export const COMMAND_CATALOG = [
16736
16824
  "arguments": [],
16737
16825
  "options": [],
16738
16826
  "subcommands": [
16827
+ {
16828
+ "name": "conflicts",
16829
+ "description": "Manage parked conflict copy notices",
16830
+ "aliases": [],
16831
+ "hidden": false,
16832
+ "helpOption": true,
16833
+ "helpCommand": true,
16834
+ "usage": "[options] [command]",
16835
+ "helpText": [],
16836
+ "arguments": [],
16837
+ "options": [],
16838
+ "subcommands": [
16839
+ {
16840
+ "name": "acknowledge",
16841
+ "description": "Acknowledge a parked conflict copy notice",
16842
+ "aliases": [],
16843
+ "hidden": false,
16844
+ "helpOption": true,
16845
+ "helpCommand": false,
16846
+ "usage": "[options] <noticeId>",
16847
+ "helpText": [],
16848
+ "arguments": [
16849
+ {
16850
+ "name": "noticeId",
16851
+ "required": true,
16852
+ "variadic": false,
16853
+ "description": ""
16854
+ }
16855
+ ],
16856
+ "options": [],
16857
+ "subcommands": []
16858
+ }
16859
+ ]
16860
+ },
16739
16861
  {
16740
16862
  "name": "status",
16741
16863
  "description": "Show sync ownership, heartbeat, last pass and log path",
@@ -346,6 +346,12 @@ export declare const REGISTRATION_PLAN: readonly [{
346
346
  readonly parent: "program";
347
347
  readonly module: "./commands/integrations.js";
348
348
  readonly exportName: "registerIntegrationsCommand";
349
+ }, {
350
+ readonly type: "registrar";
351
+ readonly root: "import";
352
+ readonly parent: "program";
353
+ readonly module: "./commands/import-scan.js";
354
+ readonly exportName: "registerImportCommand";
349
355
  }, {
350
356
  readonly type: "registrar";
351
357
  readonly root: "reindex";
@@ -442,7 +448,6 @@ export declare const REGISTRATION_PLAN: readonly [{
442
448
  readonly parent: "program";
443
449
  readonly module: "./commands/lanes.js";
444
450
  readonly exportName: "registerLanesCommand";
445
- readonly hidden: true;
446
451
  }, {
447
452
  readonly type: "registrar";
448
453
  readonly root: "monitor";
@@ -60,6 +60,7 @@ export const REGISTRATION_PLAN = [
60
60
  { type: "registrar", root: "sources", parent: "program", module: "./commands/sources.js", exportName: "registerSourcesCommand" },
61
61
  { type: "registrar", root: "signals", parent: "program", module: "./commands/signals.js", exportName: "registerSignalsCommand" },
62
62
  { type: "registrar", root: "integrations", parent: "program", module: "./commands/integrations.js", exportName: "registerIntegrationsCommand" },
63
+ { type: "registrar", root: "import", parent: "program", module: "./commands/import-scan.js", exportName: "registerImportCommand" },
63
64
  { type: "registrar", root: "reindex", parent: "program", module: "./commands/reindex.js", exportName: "registerReindexCommand" },
64
65
  { type: "registrar", root: "rescue", parent: "program", module: "./commands/rescue.js", exportName: "registerRescueCommand" },
65
66
  { type: "registrar", root: "mcp", parent: "program", module: "./commands/mcp-status.js", exportName: "registerMcpCommand" },
@@ -75,7 +76,7 @@ export const REGISTRATION_PLAN = [
75
76
  { type: "registrar", root: "index", parent: "program", module: "./commands/index-cmd.js", exportName: "registerIndexCommand" },
76
77
  { type: "registrar", root: "doctor", parent: "program", module: "./commands/doctor.js", exportName: "registerDoctorCommand" },
77
78
  { type: "registrar", root: "mesh", parent: "program", module: "./commands/mesh.js", exportName: "registerMeshCommand" },
78
- { type: "registrar", root: "lanes", parent: "program", module: "./commands/lanes.js", exportName: "registerLanesCommand", hidden: true },
79
+ { type: "registrar", root: "lanes", parent: "program", module: "./commands/lanes.js", exportName: "registerLanesCommand" },
79
80
  { type: "registrar", root: "monitor", parent: "program", module: "./commands/monitor.js", exportName: "registerMonitorCommand" },
80
81
  { type: "registrar", root: "daemon", parent: "program", module: "./commands/daemon.js", exportName: "registerDaemonCommand", hidden: true },
81
82
  { type: "registrar", root: "bot", parent: "program", module: "./commands/bot.js", exportName: "registerBotCommand" },
@@ -65,7 +65,10 @@ export declare function withSyncHealthReport<T>(run: () => Promise<T>): Promise<
65
65
  * nothing was scope-excluded. Keeping this pure makes the "never silently
66
66
  * succeed when files were dropped" guarantee unit-testable.
67
67
  */
68
+ export declare function scopeExcludedExitCode(count: number): 0 | 1;
68
69
  export declare function scopeExcludedWarning(count: number): string | null;
70
+ /** Actionable recovery for files refused because the vault baseline is stale. */
71
+ export declare function staleUploadRefusalLines(count: number, paths?: readonly string[]): string[];
69
72
  /**
70
73
  * Optional detail line listing the effective write prefixes when a push
71
74
  * dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
@@ -231,6 +234,8 @@ export interface ShareCallResult {
231
234
  filesSkipped: number;
232
235
  filesDeleted: number;
233
236
  filesExcludedByScope: number;
237
+ filesRefusedStale?: number;
238
+ filesRefusedStalePaths?: string[];
234
239
  conflictPaths: string[];
235
240
  aborted: boolean;
236
241
  /** Files this leg's plan limit refused; absent or null when none were. */
@@ -272,6 +277,8 @@ export interface PushAllResult {
272
277
  bytesUploaded: number;
273
278
  filesDeleted: number;
274
279
  filesExcludedByScope: number;
280
+ filesRefusedStale?: number;
281
+ filesRefusedStalePaths?: string[];
275
282
  errors: Array<{
276
283
  company: string;
277
284
  message: string;
@@ -139,6 +139,9 @@ export async function withSyncHealthReport(run) {
139
139
  * nothing was scope-excluded. Keeping this pure makes the "never silently
140
140
  * succeed when files were dropped" guarantee unit-testable.
141
141
  */
142
+ export function scopeExcludedExitCode(count) {
143
+ return count > 0 ? 1 : 0;
144
+ }
142
145
  export function scopeExcludedWarning(count) {
143
146
  if (count <= 0)
144
147
  return null;
@@ -147,6 +150,19 @@ export function scopeExcludedWarning(count) {
147
150
  `They were skipped, not synced. Re-run with --json to list them, or ` +
148
151
  `ask an admin to grant you write on those paths.`);
149
152
  }
153
+ /** Actionable recovery for files refused because the vault baseline is stale. */
154
+ export function staleUploadRefusalLines(count, paths = []) {
155
+ if (count <= 0)
156
+ return [];
157
+ const lines = [
158
+ `⚠ ${count} file(s) were NOT uploaded because the vault copy changed after your edit. ` +
159
+ `Pull the current vault copy, merge your edit, and push again.`,
160
+ ];
161
+ if (paths.length > 0) {
162
+ lines.push(" Not uploaded:", ...paths.map((filePath) => ` - ${filePath}`));
163
+ }
164
+ return lines;
165
+ }
150
166
  /**
151
167
  * Optional detail line listing the effective write prefixes when a push
152
168
  * dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
@@ -176,7 +192,7 @@ export function scopeExcludedLines(count, prefixSet) {
176
192
  `⚠ ${count} file(s) were NOT uploaded — the server refused them. ` +
177
193
  `A common cause is a file someone else created in a private folder ` +
178
194
  `(foo/): only its creator can change it. They were skipped, not ` +
179
- `synced. Re-run with --json to list them.`,
195
+ `synced. Re-run with --json to list them, or ask its creator to grant access.`,
180
196
  ];
181
197
  }
182
198
  const lines = [scopeExcludedWarning(count)];
@@ -527,6 +543,12 @@ export async function pushAll(options, deps) {
527
543
  result.bytesUploaded += r.bytesUploaded;
528
544
  result.filesDeleted += r.filesDeleted;
529
545
  result.filesExcludedByScope += r.filesExcludedByScope;
546
+ if ((r.filesRefusedStale ?? 0) > 0) {
547
+ result.filesRefusedStale =
548
+ (result.filesRefusedStale ?? 0) + (r.filesRefusedStale ?? 0);
549
+ result.filesRefusedStalePaths ??= [];
550
+ result.filesRefusedStalePaths.push(...(r.filesRefusedStalePaths ?? []));
551
+ }
530
552
  result.perCompany.push({
531
553
  slug: entry.slug,
532
554
  result: r,
@@ -977,6 +999,17 @@ export function registerCloudCommands(program) {
977
999
  ...(pushPrefixSet !== undefined ? { prefixSet: pushPrefixSet } : {}),
978
1000
  });
979
1001
  const planLimitRefused = planLimitTracker.summary();
1002
+ const staleRefusalCount = result.filesRefusedStale ?? 0;
1003
+ const refusalRecovery = [
1004
+ result.filesExcludedByScope > 0
1005
+ ? "Check write access for the refused path; ask its creator or a company admin to grant access."
1006
+ : null,
1007
+ staleRefusalCount > 0
1008
+ ? "Pull the current vault copy, merge your edit, and push again."
1009
+ : null,
1010
+ ]
1011
+ .filter((message) => message !== null)
1012
+ .join(" ");
980
1013
  if (jsonMode) {
981
1014
  // Synthetic terminal event so subprocess consumers can read final
982
1015
  // counts without summing per-file events. Distinguished from
@@ -991,6 +1024,10 @@ export function registerCloudCommands(program) {
991
1024
  filesSkipped: result.filesSkipped,
992
1025
  conflictPaths: result.conflictPaths,
993
1026
  aborted: result.aborted,
1027
+ filesExcludedByScope: result.filesExcludedByScope,
1028
+ filesRefusedStale: staleRefusalCount,
1029
+ filesRefusedStalePaths: result.filesRefusedStalePaths ?? [],
1030
+ ...(refusalRecovery ? { refusalRecovery } : {}),
994
1031
  ...(planLimitRefused ? { planLimitRefused } : {}),
995
1032
  });
996
1033
  }
@@ -1019,22 +1056,32 @@ export function registerCloudCommands(program) {
1019
1056
  for (const line of scopeExcludedLines(result.filesExcludedByScope, pushPrefixSet)) {
1020
1057
  log(chalk.yellow(line));
1021
1058
  }
1059
+ for (const line of staleUploadRefusalLines(result.filesRefusedStale ?? 0, result.filesRefusedStalePaths)) {
1060
+ log(chalk.yellow(line));
1061
+ }
1022
1062
  await syncHealth.succeeded();
1023
- if (!jsonMode)
1024
- process.exit(1);
1063
+ process.exit(1);
1025
1064
  return;
1026
1065
  }
1027
- if (result.filesExcludedByScope > 0) {
1066
+ if (result.filesExcludedByScope > 0 || staleRefusalCount > 0) {
1028
1067
  log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
1029
- `${result.filesSkipped} skipped, ${result.filesExcludedByScope} scope-excluded)`));
1068
+ `${result.filesSkipped} skipped, ${result.filesExcludedByScope + staleRefusalCount} files not uploaded)`));
1030
1069
  for (const line of scopeExcludedLines(result.filesExcludedByScope, pushPrefixSet)) {
1031
1070
  log(chalk.yellow(line));
1032
1071
  }
1072
+ for (const line of staleUploadRefusalLines(staleRefusalCount, result.filesRefusedStalePaths)) {
1073
+ log(chalk.yellow(line));
1074
+ }
1033
1075
  }
1034
1076
  else {
1035
1077
  log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
1036
1078
  }
1037
1079
  await syncHealth.succeeded();
1080
+ const exitCode = scopeExcludedExitCode(result.filesExcludedByScope + staleRefusalCount);
1081
+ if (exitCode !== 0) {
1082
+ process.exit(exitCode);
1083
+ return;
1084
+ }
1038
1085
  }
1039
1086
  catch (err) {
1040
1087
  if (syncHealth)
@@ -1630,13 +1677,13 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
1630
1677
  else if (row.result) {
1631
1678
  const r = row.result;
1632
1679
  const legRefused = r.planLimit ?? null;
1633
- const status = r.aborted || r.filesExcludedByScope > 0 || legRefused
1680
+ const status = r.aborted || r.filesExcludedByScope > 0 || (r.filesRefusedStale ?? 0) > 0 || legRefused
1634
1681
  ? chalk.yellow("⚠")
1635
1682
  : chalk.green("✓");
1636
1683
  console.log(` ${status} ${row.slug}: ${r.filesUploaded} file(s), ` +
1637
1684
  `${formatBytes(r.bytesUploaded)}, ${r.filesSkipped} skipped, ` +
1638
1685
  `${r.filesDeleted} deleted, ${r.filesExcludedByScope} scope-excluded, ` +
1639
- `${r.conflictPaths.length} conflict(s)` +
1686
+ `${r.filesRefusedStale ?? 0} stale-refused, ${r.conflictPaths.length} conflict(s)` +
1640
1687
  (r.aborted ? " — aborted" : "") +
1641
1688
  (legRefused
1642
1689
  ? ` — ${describeRefusedCount(legRefused)} refused by plan limit`
@@ -1652,7 +1699,8 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
1652
1699
  const summary = `\nPushed ${result.filesUploaded} file(s) ` +
1653
1700
  `(${formatBytes(result.bytesUploaded)}) across ${result.attempted} ` +
1654
1701
  `target(s); ${result.filesDeleted} deleted; ${errored} error(s); ` +
1655
- `${result.filesExcludedByScope} scope-excluded` +
1702
+ `${result.filesExcludedByScope} scope-excluded; ` +
1703
+ `${result.filesRefusedStale ?? 0} stale-refused` +
1656
1704
  (refusedTargets > 0
1657
1705
  ? `; new files refused by a plan limit on ${refusedTargets} target(s)`
1658
1706
  : "");
@@ -1673,10 +1721,24 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
1673
1721
  for (const line of scopeExcludedLines(excluded, row.prefixSet)) {
1674
1722
  console.log(chalk.yellow(`${row.slug}: ${line}`));
1675
1723
  }
1724
+ const staleCount = row.result?.filesRefusedStale ?? 0;
1725
+ for (const line of staleUploadRefusalLines(staleCount, row.result?.filesRefusedStalePaths)) {
1726
+ console.log(chalk.yellow(`${row.slug}: ${line}`));
1727
+ }
1676
1728
  }
1677
1729
  if (errored > 0)
1678
1730
  throw new SyncExitError(1);
1679
- return refused;
1731
+ const refusalSummary = {
1732
+ planLimitRefused: refused,
1733
+ filesExcludedByScope: result.filesExcludedByScope,
1734
+ filesRefusedStale: result.filesRefusedStale ?? 0,
1735
+ filesRefusedStalePaths: [...new Set(result.filesRefusedStalePaths ?? [])],
1736
+ };
1737
+ return (refusalSummary.planLimitRefused ||
1738
+ refusalSummary.filesExcludedByScope > 0 ||
1739
+ refusalSummary.filesRefusedStale > 0
1740
+ ? refusalSummary
1741
+ : null);
1680
1742
  }
1681
1743
  async function runNowSingle(hqRoot, company, personal, message, onConflict, modeAllOverride, forceScopeShrink) {
1682
1744
  console.log(chalk.bold("\nHQ Sync — Now"));
@@ -1763,7 +1825,10 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1763
1825
  ...(nowPushPrefixSet !== undefined ? { prefixSet: nowPushPrefixSet } : {}),
1764
1826
  });
1765
1827
  const pushRefused = planLimitTracker.summary();
1766
- const pushStatus = pushResult.aborted || pushResult.filesExcludedByScope > 0 || pushRefused
1828
+ const pushStatus = pushResult.aborted ||
1829
+ pushResult.filesExcludedByScope > 0 ||
1830
+ (pushResult.filesRefusedStale ?? 0) > 0 ||
1831
+ pushRefused
1767
1832
  ? chalk.yellow("⚠")
1768
1833
  : chalk.green("✓");
1769
1834
  console.log(` ${pushStatus} ` +
@@ -1780,6 +1845,9 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1780
1845
  console.log(chalk.yellow(line));
1781
1846
  }
1782
1847
  }
1848
+ for (const line of staleUploadRefusalLines(pushResult.filesRefusedStale ?? 0, pushResult.filesRefusedStalePaths)) {
1849
+ console.log(chalk.yellow(line));
1850
+ }
1783
1851
  if (pushResult.aborted) {
1784
1852
  console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
1785
1853
  throw new SyncExitError(1);
@@ -1900,6 +1968,11 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1900
1968
  "refused by your plan limit (listed above)."));
1901
1969
  throw new SyncExitError(1, { health: "succeeded" });
1902
1970
  }
1971
+ if (pushResult.filesExcludedByScope > 0 ||
1972
+ (pushResult.filesRefusedStale ?? 0) > 0) {
1973
+ console.log(chalk.yellow("\n⚠ Sync now finished, but one or more requested uploads were refused."));
1974
+ throw new SyncExitError(1, { health: "succeeded" });
1975
+ }
1903
1976
  console.log(chalk.green("\n✓ Sync now complete"));
1904
1977
  if (!personalMode &&
1905
1978
  nudgeResolvedCompanyUid &&
@@ -1943,13 +2016,16 @@ async function runNowAll(hqRoot, message, onConflict, modeAllOverride, skipPerso
1943
2016
  // pull-side).
1944
2017
  await runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal, forceScopeShrink);
1945
2018
  if (refused) {
1946
- // A plan-limit refusal does not stop the pull, but the run must not end
1947
- // looking like a clean sync: say so last and exit non-zero.
1948
- console.log(chalk.yellow(`\n⚠ Sync finished, but ${describeRefusedFiles(refused)} refused ` +
1949
- "by your plan limit (listed under push --all above)."));
1950
- if (refused.upgradeUrl) {
1951
- console.log(chalk.yellow(` Upgrade: ${refused.upgradeUrl}`));
2019
+ // A refused push does not stop the pull, but the run must not end looking
2020
+ // like a clean sync. Its per-target details were printed above.
2021
+ if (refused.planLimitRefused) {
2022
+ console.log(chalk.yellow(`\n⚠ Sync finished, but ${describeRefusedFiles(refused.planLimitRefused)} refused ` +
2023
+ "by your plan limit (listed under push --all above)."));
2024
+ if (refused.planLimitRefused.upgradeUrl) {
2025
+ console.log(chalk.yellow(` Upgrade: ${refused.planLimitRefused.upgradeUrl}`));
2026
+ }
1952
2027
  }
2028
+ console.log(chalk.yellow("\n⚠ Sync finished, but one or more requested uploads were refused."));
1953
2029
  throw new SyncExitError(1, { health: "succeeded" });
1954
2030
  }
1955
2031
  }