@indigoai-us/hq-cli 5.345.168 → 5.345.170

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 (56) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/command-catalog.generated.d.ts +49 -4
  3. package/dist/command-catalog.generated.js +60 -4
  4. package/dist/commands/access.js +7 -2
  5. package/dist/commands/bot.js +4 -0
  6. package/dist/commands/cloud.d.ts +20 -0
  7. package/dist/commands/cloud.js +55 -4
  8. package/dist/commands/core-checkpoint-native.js +30 -10
  9. package/dist/commands/feedback-triage.d.ts +43 -1
  10. package/dist/commands/feedback-triage.js +123 -23
  11. package/dist/commands/feedback.d.ts +40 -1
  12. package/dist/commands/feedback.js +195 -7
  13. package/dist/commands/files.d.ts +42 -0
  14. package/dist/commands/files.js +223 -30
  15. package/dist/commands/mesh.js +6 -0
  16. package/dist/commands/sync-manifest.d.ts +16 -0
  17. package/dist/commands/sync-manifest.js +17 -1
  18. package/dist/lib/bot/decisions.d.ts +62 -0
  19. package/dist/lib/bot/decisions.js +228 -0
  20. package/dist/lib/bot/index.d.ts +1 -0
  21. package/dist/lib/bot/index.js +1 -0
  22. package/dist/lib/bot/run.d.ts +7 -0
  23. package/dist/lib/bot/run.js +104 -1
  24. package/dist/lib/daemon/catalog.d.ts +4 -0
  25. package/dist/lib/daemon/catalog.js +60 -29
  26. package/dist/lib/daemon/config.d.ts +7 -0
  27. package/dist/lib/daemon/config.js +32 -0
  28. package/dist/lib/daemon/run.d.ts +3 -2
  29. package/dist/lib/daemon/run.js +23 -3
  30. package/dist/lib/daemon/supervisor.d.ts +1 -0
  31. package/dist/lib/daemon/supervisor.js +4 -0
  32. package/dist/lib/daemon/sync-core-canary.d.ts +9 -0
  33. package/dist/lib/daemon/sync-core-canary.js +56 -0
  34. package/dist/lib/doctor/checks/sync-health.d.ts +39 -1
  35. package/dist/lib/doctor/checks/sync-health.js +121 -3
  36. package/dist/lib/doctor/fix/apply.d.ts +66 -8
  37. package/dist/lib/doctor/fix/apply.js +278 -80
  38. package/dist/lib/doctor/fix/process-identity.d.ts +13 -0
  39. package/dist/lib/doctor/fix/process-identity.js +34 -0
  40. package/dist/lib/doctor/fix/refresh-sync-handoff.d.ts +201 -0
  41. package/dist/lib/doctor/fix/refresh-sync-handoff.js +752 -0
  42. package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +1 -1
  43. package/dist/lib/doctor/fix/refresh-sync-outcome.js +13 -0
  44. package/dist/lib/doctor/types.d.ts +1 -1
  45. package/dist/lib/doctor/types.js +3 -0
  46. package/dist/lib/mesh/live/daemon/work-subscribe.js +31 -0
  47. package/dist/lib/work-context/organize.d.ts +7 -0
  48. package/dist/lib/work-context/organize.js +21 -1
  49. package/dist/sentry-before-send.js +14 -0
  50. package/dist/utils/breadcrumb-buffer.js +15 -4
  51. package/dist/utils/feedback-diagnostics.js +5 -2
  52. package/dist/utils/feedback-logs.d.ts +7 -0
  53. package/dist/utils/feedback-logs.js +37 -3
  54. package/dist/utils/scrub-diagnostics.d.ts +4 -0
  55. package/dist/utils/scrub-diagnostics.js +95 -0
  56. package/package.json +7 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.170] — 2026-10-25
6
+ - `hq access --fix` now labels its suggested recipient as an access contact and explicitly distinguishes an ACL creator from a fallback company owner or admin. The label no longer implies that the contact wrote the file; access checks and sharing permissions are unchanged.
7
+
8
+ - `hq doctor --fix` no longer counts a manifest upload as done when `hq sync manifest` exits 0 without uploading. It now runs `hq sync manifest --json` and reports `soft_skipped`, `throttled`, `disabled`, `locked` and `resend_full_scheduled` as not uploaded, with the reason in the unresolved-check next step. When the server asks for a full resend, `--fix` retries once with `--full`, so that scope can clear in the same run; if the server asks again, the reason says a full resend is queued. An `hq` on PATH that predates `--json` falls back to exit-code handling. `hq sync manifest --json` prints the pass outcome as one JSON line (status, mode, error kind and HTTP status; no paths or error text). On hq-cloud releases that store the last failed pass, a stale `sync.manifest.*` row names that failure instead of saying the daily pass is not running, and a failure `--fix` cannot clear (404 route, 403, rejected identity; a 401 keeps its `--fix` target) is reported as `manifest-upload-rejected` with no `--fix` target (feedback_84d7a862).
9
+ - `hq feedback` adds a `stalled` status for work that started and then stopped. A feedback row is in progress only while it has a linked open PR or a live lane; after 48 hours with no activity it moves to `stalled` (the hq-pro sweep, guard and `prUrl`/`laneId` fields ship separately), which is listed apart from `new` (not started). `hq feedback list --status stalled` filters on it, the list table now has a STATUS column that marks stalled rows `stalled!`, and `--status` accepts `in-progress` for `in_progress`; the server validates the value. `hq feedback update --state` accepts `stalled`.
10
+ - `hq feedback update --state in-progress` (or `in_progress`) now requires `--pr <url>` or `--lane <id>`. The CLI first stores the link as `prUrl`/`laneId` and reads the ticket back to check that the server saved it, then posts the state. If the server rejects or ignores the link fields (a server without the new fields), the command exits 1 without posting the state, and the message gives the ticket's current state as read back from the server. The new `hq feedback claim <id>` assigns a ticket to you, and it and `hq feedback assign` send only the assignee and never set a status.
11
+ - `hq files share <name> --with <member>` now looks an extension-less name such as `knowledge` up in the vault before granting it. If `knowledge/` has files and no object `knowledge` exists, the grant covers the folder glob `knowledge/*` and a note says so. Before, it granted an exact object named `knowledge`, which covered nothing under the folder. If only the object exists, the exact key is granted as before. If both exist, neither exists, or the lookup fails, the command refuses with exit 1 and points to `--exact` or `'knowledge/*'`. The share-session path (no `--with`) resolves the name the same way. `hq files unshare <name> --with <member>` first removes the exact `<name>` grant (including rows left by older versions), and when none matches it tries `<name>/*` unless the vault shows `<name>` is only a file; it says which row it removed, lists both rows when nothing matched, and is never blocked by the vault lookup. `unshare` without `--with` still uses the prefix exactly as typed. `--exact` keeps the literal key, and now errors without `--with`. Prefixes with a `*` inside a segment (`**`, `know*`) or an empty segment (`/knowledge`) are refused. The access-request prompt sent to an owner now passes `--exact`, so it grants only the requested object.
12
+ - Fix `hq feedback list` hiding most items: `--status <s>` alone now lists every item in that status instead of only untriaged ones, the new `--open` lists every item that is not resolved, wont-fix or accepted, and a list cut short by `--limit`, the page cap or the server prints a notice on stderr and adds `truncated: true` to `--json` output.
13
+ - `hq mesh context organize` now lists project candidates for a session that attached a project existing only on this machine. Before this fix, such a session could be saved with its company slug but no company id (when the membership check was offline or failed during reconcile), and organize answered `candidates_unavailable` even when signed in. Organize now looks the slug up in your active memberships (5-second limit), saves the company id and fetches candidates. Offline sessions and slugs you are not an active member of keep the old answer. Follow-up to hq-pro #5114.
14
+ - `hq sync push` no longer prints a success line when uploads are held after an account sign-in change on this computer. Before, the push skipped every named file without counting it and reported "Pushed 0 file(s) (0 B, 0 skipped)" with exit 0, while the vault kept the old copy. It now prints how many files were not uploaded, names `hq sync held` and `hq sync release-held`, adds `filesHeldByIdentityFence` to the `--json` complete event, and exits 1. Feedback a69e12ba-c8c6-40a9-ba00-01e60d302e18.
15
+ - HQ's existing daemon can now supervise one explicitly configured personal-vault sync-core canary on macOS or Linux. Activation fails closed unless the binary, vault ownership and full-mode configuration match, hands ownership over without overlapping the existing sync runner, and restarts a core process that stops reporting health. The existing realtime connection accepts only the versioned, content-free sync wake envelope. Company vaults and machines without the canary setting keep the existing sync path unchanged.
16
+ - The personal local bot records a decision pick from the owner's HQ DM. When a decision is in conversation in that DM (`hq decide talk`) and the owner's whole reply names one option (`Release`, `ship`, `go with Release`), the bot records it through the decisions API with source `thread` and replies `Recorded: <option>.` Any other reply goes to the model with the decision loaded, and nothing is recorded. Only the owner's messages count, only decisions addressed to the owner in the owner's own companies are used, and the call uses the bot's own sign-in. After 24 hours with no pick the bot restates the full decision once. If the decisions API is not deployed (404), a pick gets a plain reply saying it was not recorded.
17
+
18
+
19
+ ## [5.345.169] — 2026-10-24
20
+ - Sentry error reports now redact CloudFront signed-URL params (`Policy`, `Key-Pair-Id`) and the `AWSAccessKeyId` and `GoogleAccessId` ids when they appear as structured keys in event data, for example a parsed query under `extra`. The feedback diagnostics scrubber already redacted them (#2177); the Sentry scrubber caught them only inside strings. The shared structured-key cases now run against the Sentry scrubber too.
21
+ - A debounced automatic checkpoint is now queued for the next checkpoint sibling. Before this, `hq core checkpoint` wrote the `-auto-` thread when the per-session debounce skipped the sibling, but it never queued the payload, so no sibling ever filled the thread in. The payload is now added to the pending queue once, and the next sibling run picks it up. The debounce still limits how many siblings start. When the pending queue goes over its cap of 50, a message on stderr now gives the number of oldest payloads dropped. Feedback a2a5c2c3-5ea0-4e1f-8155-1f39ce6ac750.
22
+
23
+ - `hq feedback` now redacts presigned URL signatures, session tokens, bearer tokens and other credentials from the diagnostics it attaches. Before this fix, recent Sentry breadcrumbs were copied into diagnostics without scrubbing, so an S3 presigned URL's X-Amz-Security-Token could reach the stored feedback row. Diagnostics passed in by callers are scrubbed too, and the inline log tails and the S3 log bundle now also catch bare, URL-encoded, CloudFront, GCS and Azure signed URL parameters.
24
+ - `hq doctor --fix` no longer starts a second background sync pull while a pull it stopped at its time limit is still shutting down. It takes the pull-start lock from another process only when that process has exited or its process id now belongs to a different process.
25
+ - `hq doctor --fix` can now refresh sync journals on drives without hard links (exFAT, FAT, some network drives). `hq doctor` warns, naming the process, when a journal refresh lock has been held for more than 10 minutes.
26
+ - `hq doctor --fix` no longer kills a slow sync journal pull after 90 seconds. A pull that is still running keeps going in the background for up to 10 minutes, one at a time, and `hq doctor` shows that journal as "refresh in progress" until it finishes. It never blocks your own `hq sync`, and it steps aside at once for `hq rescue` and other repairs, keeping what it already downloaded. A large company is no longer reported as stale every day.
27
+
5
28
  ## [5.345.168] — 2026-10-24
6
29
  - `hq dm` now picks the default audience from the recipient when no audience flag is passed. A 1:1 DM to a person (email, `prs_` uid, or a bare name that resolves to a person) sends `audience: "human"`. A 1:1 DM to an agent (`agt_` uid), a group DM and a channel post send `audience: "agent"`, so script and lane messages stay out of people's conversations. Scheduled sends (`--at`, `--in`) follow the same rule. Pass `--for-human` (alias for `--audience human`) to tag any message for people; an explicit `--audience human|agent|both` wins over `--for-human`, with a warning on stderr, and is validated on every path. `hq core delegate send` tags the pickup DM `audience: "both"`, `hq dm-bind post` keeps the channel default (`audience: "agent"`) for its automated status posts, and the agent MCP `hq_dm_send` tool, `hq agents message` and the `hq access` grant request apply the recipient rule. Reading commands (inbox, thread, channel) are unchanged, and `hq dm forward` does not set an audience.
7
30
  - `hq scan secrets` no longer exits 2 when a repo's `.gitleaks.toml` allowlist is rejected. In staged and file scans it now prints a fixed warning that quotes no part of the file and scans with no repo allowlist, so a real key is still reported (exit 1) and a commit that replaces a bad allowlist is not blocked. An unknown `--allowlist-rev` still exits 2.
@@ -1164,6 +1164,10 @@ export declare const COMMAND_CATALOG: readonly [{
1164
1164
  readonly flags: "--print";
1165
1165
  readonly description: "Build and print the chunks as JSON; upload nothing";
1166
1166
  readonly hidden: false;
1167
+ }, {
1168
+ readonly flags: "--json";
1169
+ readonly description: "Print the pass outcome as one JSON line instead of a summary";
1170
+ readonly hidden: false;
1167
1171
  }, {
1168
1172
  readonly flags: "--respect-throttle";
1169
1173
  readonly description: "Honour the 24h per-scope upload throttle instead of forcing a pass (for scheduled callers)";
@@ -3067,6 +3071,10 @@ export declare const COMMAND_CATALOG: readonly [{
3067
3071
  readonly flags: "--permission <level>";
3068
3072
  readonly description: "Permission level (only with --with): read | write";
3069
3073
  readonly hidden: false;
3074
+ }, {
3075
+ readonly flags: "--exact";
3076
+ readonly description: "With --with: grant exactly the object named by <prefix>, even when it looks like a folder (by default a bare folder name such as 'knowledge' is granted as the folder glob 'knowledge/*').";
3077
+ readonly hidden: false;
3070
3078
  }, {
3071
3079
  readonly flags: "--full";
3072
3080
  readonly description: "Grant access to the ENTIRE vault (the '*' wildcard prefix) — no need to quote a glob. Requires --with; defaults to write permission.";
@@ -3092,7 +3100,7 @@ export declare const COMMAND_CATALOG: readonly [{
3092
3100
  readonly subcommands: readonly [];
3093
3101
  }, {
3094
3102
  readonly name: "unshare";
3095
- readonly description: "Remove a named file access grant, or remove an empty ACL row or grantee-less entries without --with.";
3103
+ readonly description: "Remove a named file access grant, or remove an empty ACL row or grantee-less entries without --with. With --with, a bare name such as 'knowledge' first removes the exact 'knowledge' grant (rows from older `hq files share` versions) and, when none matches, the folder-glob 'knowledge/*' grant. Without --with, the prefix is used exactly as typed: pass 'knowledge/*' (quoted) to clean up the folder-glob row.";
3096
3104
  readonly aliases: readonly [];
3097
3105
  readonly hidden: false;
3098
3106
  readonly helpOption: true;
@@ -3109,6 +3117,10 @@ export declare const COMMAND_CATALOG: readonly [{
3109
3117
  readonly flags: "--with <principal>";
3110
3118
  readonly description: "Person uid, agent uid, company Outpost id (out_...), email address, group id, or '@all' to remove the grant";
3111
3119
  readonly hidden: false;
3120
+ }, {
3121
+ readonly flags: "--exact";
3122
+ readonly description: "With --with: remove the grant on exactly the object named by <prefix>. By default a bare folder name such as 'knowledge' resolves to the folder glob 'knowledge/*', the same way `hq files share` resolves it.";
3123
+ readonly hidden: false;
3112
3124
  }];
3113
3125
  readonly subcommands: readonly [];
3114
3126
  }, {
@@ -4565,12 +4577,20 @@ export declare const COMMAND_CATALOG: readonly [{
4565
4577
  }];
4566
4578
  readonly options: readonly [{
4567
4579
  readonly flags: "--state <state>";
4568
- readonly description: "One of: triaged, in-progress, shipped, verified, resolved";
4580
+ readonly description: "One of: triaged, in-progress, stalled, shipped, verified, resolved";
4569
4581
  readonly hidden: false;
4570
4582
  }, {
4571
4583
  readonly flags: "--note <text>";
4572
4584
  readonly description: "What the customer should read (plain text, up to 2000 characters)";
4573
4585
  readonly hidden: false;
4586
+ }, {
4587
+ readonly flags: "--pr <url>";
4588
+ readonly description: "Open pull request for this ticket (required with --state in-progress unless --lane is given)";
4589
+ readonly hidden: false;
4590
+ }, {
4591
+ readonly flags: "--lane <id>";
4592
+ readonly description: "Live lane working this ticket (required with --state in-progress unless --pr is given)";
4593
+ readonly hidden: false;
4574
4594
  }];
4575
4595
  readonly subcommands: readonly [];
4576
4596
  }, {
@@ -4632,6 +4652,27 @@ export declare const COMMAND_CATALOG: readonly [{
4632
4652
  readonly hidden: false;
4633
4653
  }];
4634
4654
  readonly subcommands: readonly [];
4655
+ }, {
4656
+ readonly name: "claim";
4657
+ readonly description: "Assign a ticket to yourself (sets the assignee only, never the status)";
4658
+ readonly aliases: readonly [];
4659
+ readonly hidden: false;
4660
+ readonly helpOption: true;
4661
+ readonly helpCommand: false;
4662
+ readonly usage: "[options] <id>";
4663
+ readonly helpText: readonly [];
4664
+ readonly arguments: readonly [{
4665
+ readonly name: "id";
4666
+ readonly required: true;
4667
+ readonly variadic: false;
4668
+ readonly description: "Feedback id (feedback_<uuid>)";
4669
+ }];
4670
+ readonly options: readonly [{
4671
+ readonly flags: "--json";
4672
+ readonly description: "Print { id, assignee } as JSON";
4673
+ readonly hidden: false;
4674
+ }];
4675
+ readonly subcommands: readonly [];
4635
4676
  }, {
4636
4677
  readonly name: "list";
4637
4678
  readonly description: "List feedback: untriaged (default), yours, or one person's";
@@ -4646,6 +4687,10 @@ export declare const COMMAND_CATALOG: readonly [{
4646
4687
  readonly flags: "--untriaged";
4647
4688
  readonly description: "Items nobody has triaged yet (the default)";
4648
4689
  readonly hidden: false;
4690
+ }, {
4691
+ readonly flags: "--open";
4692
+ readonly description: "Every item not resolved, won't-fix or accepted, triaged or not";
4693
+ readonly hidden: false;
4649
4694
  }, {
4650
4695
  readonly flags: "--mine";
4651
4696
  readonly description: "Items assigned to you";
@@ -4656,7 +4701,7 @@ export declare const COMMAND_CATALOG: readonly [{
4656
4701
  readonly hidden: false;
4657
4702
  }, {
4658
4703
  readonly flags: "--status <status>";
4659
- readonly description: "Only items with this status";
4704
+ readonly description: "Only items with this status (new, triaging, accepted, wont_fix, in_progress, resolved, waiting_on_customer, reopened, stalled, needs_info). Without --untriaged, --mine or --assignee it lists every item in that status";
4660
4705
  readonly hidden: false;
4661
4706
  }, {
4662
4707
  readonly flags: "--limit <n>";
@@ -4668,7 +4713,7 @@ export declare const COMMAND_CATALOG: readonly [{
4668
4713
  readonly hidden: false;
4669
4714
  }, {
4670
4715
  readonly flags: "--json";
4671
- readonly description: "Print { items, count } as JSON";
4716
+ readonly description: "Print { items, count } as JSON, plus truncated: true when more items match";
4672
4717
  readonly hidden: false;
4673
4718
  }];
4674
4719
  readonly subcommands: readonly [];
@@ -1399,6 +1399,11 @@ export const COMMAND_CATALOG = [
1399
1399
  "description": "Build and print the chunks as JSON; upload nothing",
1400
1400
  "hidden": false
1401
1401
  },
1402
+ {
1403
+ "flags": "--json",
1404
+ "description": "Print the pass outcome as one JSON line instead of a summary",
1405
+ "hidden": false
1406
+ },
1402
1407
  {
1403
1408
  "flags": "--respect-throttle",
1404
1409
  "description": "Honour the 24h per-scope upload throttle instead of forcing a pass (for scheduled callers)",
@@ -3693,6 +3698,11 @@ export const COMMAND_CATALOG = [
3693
3698
  "description": "Permission level (only with --with): read | write",
3694
3699
  "hidden": false
3695
3700
  },
3701
+ {
3702
+ "flags": "--exact",
3703
+ "description": "With --with: grant exactly the object named by <prefix>, even when it looks like a folder (by default a bare folder name such as 'knowledge' is granted as the folder glob 'knowledge/*').",
3704
+ "hidden": false
3705
+ },
3696
3706
  {
3697
3707
  "flags": "--full",
3698
3708
  "description": "Grant access to the ENTIRE vault (the '*' wildcard prefix) — no need to quote a glob. Requires --with; defaults to write permission.",
@@ -3724,7 +3734,7 @@ export const COMMAND_CATALOG = [
3724
3734
  },
3725
3735
  {
3726
3736
  "name": "unshare",
3727
- "description": "Remove a named file access grant, or remove an empty ACL row or grantee-less entries without --with.",
3737
+ "description": "Remove a named file access grant, or remove an empty ACL row or grantee-less entries without --with. With --with, a bare name such as 'knowledge' first removes the exact 'knowledge' grant (rows from older `hq files share` versions) and, when none matches, the folder-glob 'knowledge/*' grant. Without --with, the prefix is used exactly as typed: pass 'knowledge/*' (quoted) to clean up the folder-glob row.",
3728
3738
  "aliases": [],
3729
3739
  "hidden": false,
3730
3740
  "helpOption": true,
@@ -3744,6 +3754,11 @@ export const COMMAND_CATALOG = [
3744
3754
  "flags": "--with <principal>",
3745
3755
  "description": "Person uid, agent uid, company Outpost id (out_...), email address, group id, or '@all' to remove the grant",
3746
3756
  "hidden": false
3757
+ },
3758
+ {
3759
+ "flags": "--exact",
3760
+ "description": "With --with: remove the grant on exactly the object named by <prefix>. By default a bare folder name such as 'knowledge' resolves to the folder glob 'knowledge/*', the same way `hq files share` resolves it.",
3761
+ "hidden": false
3747
3762
  }
3748
3763
  ],
3749
3764
  "subcommands": []
@@ -5529,13 +5544,23 @@ export const COMMAND_CATALOG = [
5529
5544
  "options": [
5530
5545
  {
5531
5546
  "flags": "--state <state>",
5532
- "description": "One of: triaged, in-progress, shipped, verified, resolved",
5547
+ "description": "One of: triaged, in-progress, stalled, shipped, verified, resolved",
5533
5548
  "hidden": false
5534
5549
  },
5535
5550
  {
5536
5551
  "flags": "--note <text>",
5537
5552
  "description": "What the customer should read (plain text, up to 2000 characters)",
5538
5553
  "hidden": false
5554
+ },
5555
+ {
5556
+ "flags": "--pr <url>",
5557
+ "description": "Open pull request for this ticket (required with --state in-progress unless --lane is given)",
5558
+ "hidden": false
5559
+ },
5560
+ {
5561
+ "flags": "--lane <id>",
5562
+ "description": "Live lane working this ticket (required with --state in-progress unless --pr is given)",
5563
+ "hidden": false
5539
5564
  }
5540
5565
  ],
5541
5566
  "subcommands": []
@@ -5613,6 +5638,32 @@ export const COMMAND_CATALOG = [
5613
5638
  ],
5614
5639
  "subcommands": []
5615
5640
  },
5641
+ {
5642
+ "name": "claim",
5643
+ "description": "Assign a ticket to yourself (sets the assignee only, never the status)",
5644
+ "aliases": [],
5645
+ "hidden": false,
5646
+ "helpOption": true,
5647
+ "helpCommand": false,
5648
+ "usage": "[options] <id>",
5649
+ "helpText": [],
5650
+ "arguments": [
5651
+ {
5652
+ "name": "id",
5653
+ "required": true,
5654
+ "variadic": false,
5655
+ "description": "Feedback id (feedback_<uuid>)"
5656
+ }
5657
+ ],
5658
+ "options": [
5659
+ {
5660
+ "flags": "--json",
5661
+ "description": "Print { id, assignee } as JSON",
5662
+ "hidden": false
5663
+ }
5664
+ ],
5665
+ "subcommands": []
5666
+ },
5616
5667
  {
5617
5668
  "name": "list",
5618
5669
  "description": "List feedback: untriaged (default), yours, or one person's",
@@ -5629,6 +5680,11 @@ export const COMMAND_CATALOG = [
5629
5680
  "description": "Items nobody has triaged yet (the default)",
5630
5681
  "hidden": false
5631
5682
  },
5683
+ {
5684
+ "flags": "--open",
5685
+ "description": "Every item not resolved, won't-fix or accepted, triaged or not",
5686
+ "hidden": false
5687
+ },
5632
5688
  {
5633
5689
  "flags": "--mine",
5634
5690
  "description": "Items assigned to you",
@@ -5641,7 +5697,7 @@ export const COMMAND_CATALOG = [
5641
5697
  },
5642
5698
  {
5643
5699
  "flags": "--status <status>",
5644
- "description": "Only items with this status",
5700
+ "description": "Only items with this status (new, triaging, accepted, wont_fix, in_progress, resolved, waiting_on_customer, reopened, stalled, needs_info). Without --untriaged, --mine or --assignee it lists every item in that status",
5645
5701
  "hidden": false
5646
5702
  },
5647
5703
  {
@@ -5656,7 +5712,7 @@ export const COMMAND_CATALOG = [
5656
5712
  },
5657
5713
  {
5658
5714
  "flags": "--json",
5659
- "description": "Print { items, count } as JSON",
5715
+ "description": "Print { items, count } as JSON, plus truncated: true when more items match",
5660
5716
  "hidden": false
5661
5717
  }
5662
5718
  ],
@@ -611,7 +611,12 @@ async function completeTarget(input, deps, located) {
611
611
  }
612
612
  else {
613
613
  grantor = resolved;
614
- const grantorLine = `Grantor: ${grantorLabel(resolved)} (${resolved.source})`;
614
+ const contactRole = resolved.source === "acl-creator"
615
+ ? "ACL creator"
616
+ : resolved.source === "owner"
617
+ ? "company owner fallback"
618
+ : "company admin fallback";
619
+ const grantorLine = `Access contact: ${grantorLabel(resolved)} (${contactRole}; file authorship not verified)`;
615
620
  // The recipient is always shown BEFORE any send. Under --json stdout is
616
621
  // reserved for the single JSON object, so the line goes to stderr.
617
622
  if (json)
@@ -659,7 +664,7 @@ async function completeTarget(input, deps, located) {
659
664
  }
660
665
  if (approved && outcome === "no-access") {
661
666
  const message = `${requester.email} is asking for read access to ${resolvedKey} in ${slug}.`;
662
- const prompt = `hq files share ${resolvedKey} --with ${requester.email} --permission read --company ${slug}`;
667
+ const prompt = `hq files share ${resolvedKey} --exact --with ${requester.email} --permission read --company ${slug}`;
663
668
  const sent = await deps.dm.send({
664
669
  recipient: resolved.email,
665
670
  message,
@@ -26,6 +26,7 @@ import { hasPromotionHold, promotionHoldSummary } from "../lib/bot/promotion-hol
26
26
  * list (online flag), and rm. Secrets are never printed.
27
27
  */
28
28
  import { promoteLocalBot } from "../lib/bot/promote.js";
29
+ import { DecisionsClient } from "../lib/decisions/client.js";
29
30
  import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
30
31
  import { preloadResidentPackageModules, reportResidentPackagePreloadFailure, runResidentCommandWithPackageLease, } from "../utils/resident-package-preload.js";
31
32
  import { followLogFile } from "../lib/log-follow.js";
@@ -1063,6 +1064,9 @@ async function runBotResident(nameArg) {
1063
1064
  api,
1064
1065
  runtime: runtimeFor(config.runtime),
1065
1066
  ownerContext: () => owner.get(),
1067
+ // US-005: decision picks in the owner DM, with the bot's own machine token
1068
+ // (never the owner's login).
1069
+ decisions: new DecisionsClient(token),
1066
1070
  // Chat attachments are fetched with the bot's own machine identity against
1067
1071
  // the same API its inbox and posts use (BotApi has no baseUrl override either).
1068
1072
  downloadAttachment: createVaultObjectDownloader({ getToken: token }),
@@ -67,6 +67,26 @@ export declare function withSyncHealthReport<T>(run: () => Promise<T>): Promise<
67
67
  */
68
68
  export declare function scopeExcludedExitCode(count: number): 0 | 1;
69
69
  export declare function scopeExcludedWarning(count: number): string | null;
70
+ /**
71
+ * Counts the files share() held back because this sync scope is fenced after an account
72
+ * switch on this computer (feedback_a69e12ba). share() reports the hold only as
73
+ * an `identity-switch-push-held` event and leaves the files out of every
74
+ * result counter, so without this the push printed "Pushed 0 file(s) (0 B,
75
+ * 0 skipped)" as a success while the vault kept the old copy.
76
+ *
77
+ * Each event carries the size of one fenced hold, so the tracker keeps the
78
+ * largest count it sees instead of a sum: a repeated emit for the same hold
79
+ * cannot inflate the total. A missing, non-numeric or negative count reads as 0.
80
+ */
81
+ export declare function createIdentityHeldTracker(): {
82
+ observe: (event: {
83
+ type: string;
84
+ count?: unknown;
85
+ }) => void;
86
+ count: () => number;
87
+ };
88
+ /** Recovery lines for files held by the account-switch push fence. */
89
+ export declare function identityHeldLines(count: number): string[];
70
90
  /** Actionable recovery for files refused because the vault baseline is stale. */
71
91
  export declare function staleUploadRefusalLines(count: number, paths?: readonly string[]): string[];
72
92
  /**
@@ -153,6 +153,41 @@ export function scopeExcludedWarning(count) {
153
153
  `They were skipped, not synced. Re-run with --json to list them, or ` +
154
154
  `ask an admin to grant you write on those paths.`);
155
155
  }
156
+ /**
157
+ * Counts the files share() held back because this sync scope is fenced after an account
158
+ * switch on this computer (feedback_a69e12ba). share() reports the hold only as
159
+ * an `identity-switch-push-held` event and leaves the files out of every
160
+ * result counter, so without this the push printed "Pushed 0 file(s) (0 B,
161
+ * 0 skipped)" as a success while the vault kept the old copy.
162
+ *
163
+ * Each event carries the size of one fenced hold, so the tracker keeps the
164
+ * largest count it sees instead of a sum: a repeated emit for the same hold
165
+ * cannot inflate the total. A missing, non-numeric or negative count reads as 0.
166
+ */
167
+ export function createIdentityHeldTracker() {
168
+ let held = 0;
169
+ return {
170
+ observe(event) {
171
+ if (event.type !== "identity-switch-push-held")
172
+ return;
173
+ const n = typeof event.count === "number" && Number.isFinite(event.count)
174
+ ? Math.max(0, Math.trunc(event.count))
175
+ : 0;
176
+ held = Math.max(held, n);
177
+ },
178
+ count: () => held,
179
+ };
180
+ }
181
+ /** Recovery lines for files held by the account-switch push fence. */
182
+ export function identityHeldLines(count) {
183
+ if (count <= 0)
184
+ return [];
185
+ return [
186
+ `⚠ ${count} file(s) were NOT uploaded: uploads for this company are held on this computer ` +
187
+ `after an account sign-in change. Run \`hq sync held\` to see what is held, then ` +
188
+ `\`hq sync release-held\` to upload it.`,
189
+ ];
190
+ }
156
191
  /** Actionable recovery for files refused because the vault baseline is stale. */
157
192
  export function staleUploadRefusalLines(count, paths = []) {
158
193
  if (count <= 0)
@@ -1028,6 +1063,7 @@ export function registerCloudCommands(program) {
1028
1063
  companyRef: targetCompany ?? readActiveCompany(options.hqRoot),
1029
1064
  });
1030
1065
  const planLimitTracker = createPlanLimitRefusalTracker(companySlugForBilling(progressCompany));
1066
+ const identityHeld = createIdentityHeldTracker();
1031
1067
  syncHealth = beginSyncHealthReport();
1032
1068
  if (!personalMode) {
1033
1069
  const companyUid = entityContext?.uid ??
@@ -1055,7 +1091,10 @@ export function registerCloudCommands(program) {
1055
1091
  }),
1056
1092
  hqRoot: options.hqRoot,
1057
1093
  onEvent,
1058
- progressRecorder: trackingProgressRecorder(planLimitTracker, progressCompany),
1094
+ progressRecorder: ((record) => (event) => {
1095
+ identityHeld.observe(event);
1096
+ record(event);
1097
+ })(trackingProgressRecorder(planLimitTracker, progressCompany)),
1059
1098
  ...(personalMode ? { personalMode: true } : {}),
1060
1099
  ...(journalSlug !== undefined ? { journalSlug } : {}),
1061
1100
  ...(author ? { author } : {}),
@@ -1063,6 +1102,7 @@ export function registerCloudCommands(program) {
1063
1102
  ...namedPathShareOptions(userNamedPaths),
1064
1103
  });
1065
1104
  const planLimitRefused = planLimitTracker.summary();
1105
+ const identityHeldCount = identityHeld.count();
1066
1106
  const staleRefusalCount = result.filesRefusedStale ?? 0;
1067
1107
  const uploadFailed = readUploadFailures(result);
1068
1108
  const refusalRecovery = [
@@ -1075,6 +1115,9 @@ export function registerCloudCommands(program) {
1075
1115
  uploadFailed.count > 0
1076
1116
  ? "Push again; the files that failed are still on this machine."
1077
1117
  : null,
1118
+ identityHeldCount > 0
1119
+ ? "Run `hq sync held`, then `hq sync release-held` to upload the held files."
1120
+ : null,
1078
1121
  ]
1079
1122
  .filter((message) => message !== null)
1080
1123
  .join(" ");
@@ -1095,6 +1138,7 @@ export function registerCloudCommands(program) {
1095
1138
  filesExcludedByScope: result.filesExcludedByScope,
1096
1139
  filesRefusedStale: staleRefusalCount,
1097
1140
  filesRefusedStalePaths: result.filesRefusedStalePaths ?? [],
1141
+ filesHeldByIdentityFence: identityHeldCount,
1098
1142
  filesUploadFailed: uploadFailed.count,
1099
1143
  uploadFailedPaths: uploadFailed.paths,
1100
1144
  ...(refusalRecovery ? { refusalRecovery } : {}),
@@ -1115,8 +1159,8 @@ export function registerCloudCommands(program) {
1115
1159
  // Never a success line over a refusal: say what was pushed, what
1116
1160
  // was refused and how to fix it, then exit non-zero. The sync
1117
1161
  // itself worked, so client health records a success. `--json`
1118
- // callers read `planLimitRefused` on the complete event instead
1119
- // and keep their exit status.
1162
+ // callers also read `planLimitRefused` on the complete event; the
1163
+ // exit status is non-zero in both modes.
1120
1164
  log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
1121
1165
  `${result.filesSkipped} skipped), but ${describeRefusedFiles(planLimitRefused)} ` +
1122
1166
  "refused: new files are paused for this account."));
@@ -1132,11 +1176,15 @@ export function registerCloudCommands(program) {
1132
1176
  for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
1133
1177
  log(chalk.yellow(line));
1134
1178
  }
1179
+ for (const line of identityHeldLines(identityHeldCount)) {
1180
+ log(chalk.yellow(line));
1181
+ }
1135
1182
  await syncHealth.succeeded();
1136
1183
  process.exit(1);
1137
1184
  return;
1138
1185
  }
1139
- const notUploadedCount = result.filesExcludedByScope + staleRefusalCount + uploadFailed.count;
1186
+ const notUploadedCount = result.filesExcludedByScope + staleRefusalCount + uploadFailed.count +
1187
+ identityHeldCount;
1140
1188
  if (notUploadedCount > 0) {
1141
1189
  log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
1142
1190
  `${result.filesSkipped} skipped, ${notUploadedCount} files not uploaded)`));
@@ -1149,6 +1197,9 @@ export function registerCloudCommands(program) {
1149
1197
  for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
1150
1198
  log(chalk.yellow(line));
1151
1199
  }
1200
+ for (const line of identityHeldLines(identityHeldCount)) {
1201
+ log(chalk.yellow(line));
1202
+ }
1152
1203
  }
1153
1204
  else {
1154
1205
  const skipSummary = formatPushSkipSummary(result);
@@ -1206,8 +1206,30 @@ function appendPending(pendingPath, payload) {
1206
1206
  // runs once the queue is already over cap, where newest-wins is the point.
1207
1207
  const kept = lines.slice(-MAX_PENDING_PAYLOADS);
1208
1208
  fs.writeFileSync(pendingPath, `${kept.join("\n")}\n`);
1209
+ // Say so instead of dropping silently: a wedged sibling shows up here.
1210
+ const dropped = lines.length - kept.length;
1211
+ printError(`checkpoint: pending queue over cap (${MAX_PENDING_PAYLOADS}); dropped ${dropped} oldest payload(s)`);
1209
1212
  return kept.length;
1210
1213
  }
1214
+ /**
1215
+ * Queue a payload for the next sibling run and record its ontology touches.
1216
+ * Shared by the busy-sibling path and the debounced automatic path, so both
1217
+ * hand their thread to the next run instead of leaving it unfilled.
1218
+ */
1219
+ async function queuePayload(liveRoot, payload) {
1220
+ const siblingRoot = path.join(liveRoot, "workspace", "checkpoints", "sibling");
1221
+ fs.mkdirSync(siblingRoot, { recursive: true });
1222
+ const depth = appendPending(path.join(siblingRoot, "pending.jsonl"), payload);
1223
+ try {
1224
+ const { recordQueuedTouches } = await import("../lib/checkpoint-ontology/queued-touches.js");
1225
+ recordQueuedTouches(liveRoot, payload.company, payload.files, new Date());
1226
+ }
1227
+ catch {
1228
+ // Queued touches only help a later ontology plan; they must never fail
1229
+ // the checkpoint itself.
1230
+ }
1231
+ return depth;
1232
+ }
1211
1233
  /**
1212
1234
  * Take ownership of the queued payloads by renaming the queue aside.
1213
1235
  *
@@ -1369,15 +1391,7 @@ async function startSibling(liveRoot, input, threadPath, backend) {
1369
1391
  const activePid = existingSiblingPid(lockPath, Date.now());
1370
1392
  fs.mkdirSync(siblingRoot, { recursive: true });
1371
1393
  if (activePid !== null) {
1372
- const depth = appendPending(pendingPath, payload);
1373
- try {
1374
- const { recordQueuedTouches } = await import("../lib/checkpoint-ontology/queued-touches.js");
1375
- recordQueuedTouches(liveRoot, payload.company, payload.files, new Date());
1376
- }
1377
- catch {
1378
- // Queued touches only help a later ontology plan; they must never fail
1379
- // the checkpoint itself.
1380
- }
1394
+ const depth = await queuePayload(liveRoot, payload);
1381
1395
  printResult(`checkpoint: sibling busy — payload queued (${depth} pending)`);
1382
1396
  return;
1383
1397
  }
@@ -1579,7 +1593,13 @@ async function runCheckpoint(options, command, group, preResolvedHqRoot) {
1579
1593
  printResult(`checkpoint: sibling disabled (${SIBLING_SETTING})`);
1580
1594
  }
1581
1595
  else if (autoDebounced) {
1582
- printResult("checkpoint: sibling skipped (automatic checkpoint; one ran for this session recently)");
1596
+ // feedback_a2a5c2c3: the debounce limits how many siblings start, but
1597
+ // the thread still needs filling. Queue it once; the next sibling run
1598
+ // claims it. The debounce stamp is not refreshed, so the window still
1599
+ // closes on schedule and the next automatic checkpoint starts a run.
1600
+ const depth = await queuePayload(liveRoot, siblingPayload(input, threadPath, "none"));
1601
+ printResult("checkpoint: sibling skipped (automatic checkpoint; one ran for this session recently)" +
1602
+ ` — payload queued (${depth} pending)`);
1583
1603
  }
1584
1604
  else if (backend === "none") {
1585
1605
  printResult("checkpoint: sibling disabled (backend none)");
@@ -9,6 +9,18 @@ import { Command } from "commander";
9
9
  */
10
10
  export declare const TRIAGE_KINDS: readonly ["bug", "feature", "question"];
11
11
  export type TriageKind = (typeof TRIAGE_KINDS)[number];
12
+ /**
13
+ * Internal feedback row statuses `hq feedback list --status` accepts.
14
+ * `stalled` is open work that started and then stopped: a row is in_progress
15
+ * only while it has a linked open PR or a live lane, and moves to stalled after
16
+ * 48h with no activity. It is listed apart from `new` (not started).
17
+ */
18
+ /**
19
+ * Statuses shown in `list --status` help. Help text only: the CLI does not
20
+ * validate against this list, because hq-pro owns the set and returns a 400
21
+ * for an unknown value.
22
+ */
23
+ export declare const FEEDBACK_LIST_STATUSES: readonly ["new", "triaging", "accepted", "wont_fix", "in_progress", "resolved", "waiting_on_customer", "reopened", "stalled", "needs_info"];
12
24
  export declare const MAX_TRIAGE_SUMMARY = 2000;
13
25
  export declare const DEFAULT_LIST_LIMIT = 50;
14
26
  export interface FeedbackListRow {
@@ -24,6 +36,14 @@ export type FeedbackListSelector = {
24
36
  } | {
25
37
  kind: "assignee";
26
38
  assignee: string;
39
+ }
40
+ /** Every visible row, untriaged or not. Used for `--status` with no other selector. */
41
+ | {
42
+ kind: "any";
43
+ }
44
+ /** Every row not in a terminal status, untriaged or not (`--open`). */
45
+ | {
46
+ kind: "open";
27
47
  };
28
48
  /** Email claim of the caller's staff token, used by `list --mine`. */
29
49
  export declare function emailFromToken(token: string): string | null;
@@ -36,14 +56,36 @@ export declare function formatFeedbackAge(iso: string | undefined, now?: number)
36
56
  * back as `?cursor=`.
37
57
  */
38
58
  export declare function cursorFrom(data: unknown): string | null;
59
+ /**
60
+ * Lifecycle states hq-pro treats as closed (`TERMINAL_STATUSES` in
61
+ * feedback-status.ts). `--open` lists every row outside this set; a row with
62
+ * no status counts as open.
63
+ */
64
+ export declare const TERMINAL_FEEDBACK_STATUSES: readonly string[];
65
+ /** Why a list stopped before the server ran out of rows, if it did. */
66
+ export type FeedbackListTruncation = "limit" | "page-cap" | "server";
67
+ export interface FeedbackListResult {
68
+ items: FeedbackListRow[];
69
+ /** Set when matching rows may exist beyond `items`. */
70
+ truncated?: FeedbackListTruncation;
71
+ }
39
72
  /** Page through `GET /v1/feedback` until `limit` rows (or every row with `limit: null`). */
40
73
  export declare function listFeedback(opts: {
41
74
  token: string;
42
75
  selector: FeedbackListSelector;
43
76
  status?: string;
44
77
  limit: number | null;
45
- }): Promise<FeedbackListRow[]>;
78
+ }): Promise<FeedbackListResult>;
79
+ /** One-line stderr notice for a list that stopped before the end. */
80
+ export declare function feedbackTruncationNotice(reason: FeedbackListTruncation, shown: number): string;
46
81
  export declare function formatFeedbackTable(rows: FeedbackListRow[], now?: number): string;
82
+ /** Status cell for the list table. A stalled row is flagged so it does not read as not started. */
83
+ export declare function formatFeedbackStatus(status: unknown): string;
84
+ /**
85
+ * Normalize `list --status`; accepts `in-progress` as an alias of `in_progress`.
86
+ * Any non-empty value goes to the server, which validates it.
87
+ */
88
+ export declare function parseListStatus(raw: string): string;
47
89
  export declare function parseListLimit(raw: string): number;
48
90
  export declare function buildTriagePatch(opts: {
49
91
  id: string;