@indigoai-us/hq-cli 5.345.156 → 5.345.160

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 (188) hide show
  1. package/CHANGELOG.md +255 -0
  2. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
  3. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
  4. package/assets/scaffold/core/scripts/worktree.sh +4 -0
  5. package/dist/command-catalog.generated.d.ts +535 -10
  6. package/dist/command-catalog.generated.js +678 -31
  7. package/dist/command-registration-plan.d.ts +18 -0
  8. package/dist/command-registration-plan.js +4 -0
  9. package/dist/commands/_patterns.d.ts +17 -0
  10. package/dist/commands/_patterns.js +22 -0
  11. package/dist/commands/access.d.ts +6 -0
  12. package/dist/commands/access.js +18 -1
  13. package/dist/commands/acl-lock.d.ts +41 -0
  14. package/dist/commands/acl-lock.js +28 -0
  15. package/dist/commands/agent-enroll.d.ts +2 -0
  16. package/dist/commands/agent-enroll.js +9 -4
  17. package/dist/commands/agent-kit.js +8 -6
  18. package/dist/commands/agents.js +5 -11
  19. package/dist/commands/billing-sponsor.d.ts +4 -4
  20. package/dist/commands/billing-sponsor.js +11 -11
  21. package/dist/commands/billing-usage.d.ts +22 -8
  22. package/dist/commands/billing-usage.js +57 -22
  23. package/dist/commands/billing.d.ts +7 -7
  24. package/dist/commands/billing.js +13 -13
  25. package/dist/commands/bot.js +7 -4
  26. package/dist/commands/channel-suggest.d.ts +56 -0
  27. package/dist/commands/channel-suggest.js +170 -0
  28. package/dist/commands/channels.js +7 -1
  29. package/dist/commands/cloud-provision.js +2 -2
  30. package/dist/commands/cloud.d.ts +45 -0
  31. package/dist/commands/cloud.js +157 -17
  32. package/dist/commands/company.js +4 -3
  33. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  34. package/dist/commands/core-checkpoint-native.js +67 -2
  35. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  36. package/dist/commands/core-pipeline-driver-native.js +131 -5
  37. package/dist/commands/core.js +12 -0
  38. package/dist/commands/crm.js +83 -6
  39. package/dist/commands/db-remote-deps.js +12 -1
  40. package/dist/commands/deploy-access.d.ts +33 -0
  41. package/dist/commands/deploy-access.js +322 -0
  42. package/dist/commands/dm-attach.d.ts +106 -0
  43. package/dist/commands/dm-attach.js +184 -0
  44. package/dist/commands/dm.d.ts +37 -0
  45. package/dist/commands/dm.js +172 -10
  46. package/dist/commands/feedback-show.d.ts +40 -0
  47. package/dist/commands/feedback-show.js +261 -0
  48. package/dist/commands/feedback.d.ts +21 -0
  49. package/dist/commands/feedback.js +152 -2
  50. package/dist/commands/files.d.ts +24 -0
  51. package/dist/commands/files.js +71 -8
  52. package/dist/commands/integrations-connect.js +7 -3
  53. package/dist/commands/lanes-accounts.js +16 -1
  54. package/dist/commands/lanes-loop.js +14 -4
  55. package/dist/commands/lanes-message.js +6 -4
  56. package/dist/commands/lanes-resume.js +36 -13
  57. package/dist/commands/lanes-shared.d.ts +5 -0
  58. package/dist/commands/lanes-shared.js +13 -3
  59. package/dist/commands/lanes-watch.js +1 -0
  60. package/dist/commands/lanes.d.ts +20 -1
  61. package/dist/commands/lanes.js +201 -51
  62. package/dist/commands/meetings.d.ts +4 -4
  63. package/dist/commands/meetings.js +17 -8
  64. package/dist/commands/mesh.js +149 -1
  65. package/dist/commands/monitor.d.ts +1 -0
  66. package/dist/commands/monitor.js +4 -1
  67. package/dist/commands/repo.d.ts +41 -0
  68. package/dist/commands/repo.js +331 -0
  69. package/dist/commands/scan.d.ts +3 -1
  70. package/dist/commands/scan.js +7 -3
  71. package/dist/commands/secrets.d.ts +21 -0
  72. package/dist/commands/secrets.js +114 -14
  73. package/dist/commands/signals.js +17 -5
  74. package/dist/commands/skill.js +6 -6
  75. package/dist/commands/staff/listings.d.ts +41 -0
  76. package/dist/commands/staff/listings.js +160 -0
  77. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  78. package/dist/commands/staff/reset-test-account.js +4 -3
  79. package/dist/commands/whoami.js +1 -1
  80. package/dist/hook-engine-path.js +13 -2
  81. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
  82. package/dist/hq-cloud-sync-runner/package.json +1 -1
  83. package/dist/lib/agent-kit/run/inbox.js +2 -1
  84. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  85. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  86. package/dist/lib/agent-kit/wake.d.ts +4 -0
  87. package/dist/lib/agent-kit/wake.js +49 -3
  88. package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
  89. package/dist/lib/billing/plan-limit-refusals.js +26 -3
  90. package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
  91. package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
  92. package/dist/lib/billing/plan-lock.d.ts +22 -6
  93. package/dist/lib/billing/plan-lock.js +58 -30
  94. package/dist/lib/billing/usage-cost.d.ts +24 -2
  95. package/dist/lib/billing/usage-cost.js +103 -3
  96. package/dist/lib/billing/usage-only.d.ts +37 -1
  97. package/dist/lib/billing/usage-only.js +89 -4
  98. package/dist/lib/billing/usage-pause.d.ts +27 -0
  99. package/dist/lib/billing/usage-pause.js +50 -0
  100. package/dist/lib/bot/inflight.d.ts +7 -1
  101. package/dist/lib/bot/inflight.js +10 -2
  102. package/dist/lib/bot/prompt.js +3 -0
  103. package/dist/lib/bot/run.js +72 -20
  104. package/dist/lib/bot/session.d.ts +14 -0
  105. package/dist/lib/bot/session.js +70 -0
  106. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  107. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  108. package/dist/lib/capacity/lane-login.d.ts +6 -0
  109. package/dist/lib/capacity/lane-login.js +5 -0
  110. package/dist/lib/core-utils/worktree.js +2 -0
  111. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  112. package/dist/lib/daemon/exit-cause.js +88 -0
  113. package/dist/lib/daemon/handoff.d.ts +2 -0
  114. package/dist/lib/daemon/handoff.js +6 -1
  115. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  116. package/dist/lib/daemon/paths.d.ts +2 -0
  117. package/dist/lib/daemon/paths.js +1 -0
  118. package/dist/lib/daemon/run.js +13 -0
  119. package/dist/lib/daemon/supervisor.d.ts +54 -1
  120. package/dist/lib/daemon/supervisor.js +297 -21
  121. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  122. package/dist/lib/daemon/unit-holds.js +86 -0
  123. package/dist/lib/db/control-plane.d.ts +5 -0
  124. package/dist/lib/db/control-plane.js +10 -0
  125. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  126. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  127. package/dist/lib/hook-engine/resolve.d.ts +14 -0
  128. package/dist/lib/hook-engine/resolve.js +55 -1
  129. package/dist/lib/index-render/company-knowledge.js +1 -3
  130. package/dist/lib/index-render/projects.js +1 -3
  131. package/dist/lib/lanes/account-company.d.ts +28 -0
  132. package/dist/lib/lanes/account-company.js +53 -0
  133. package/dist/lib/lanes/accounts.d.ts +22 -3
  134. package/dist/lib/lanes/accounts.js +38 -2
  135. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  136. package/dist/lib/lanes/claude-balance.js +6 -1
  137. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  138. package/dist/lib/lanes/claude-credential.js +163 -0
  139. package/dist/lib/lanes/compaction.d.ts +2 -0
  140. package/dist/lib/lanes/compaction.js +9 -0
  141. package/dist/lib/lanes/config.js +6 -0
  142. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  143. package/dist/lib/lanes/hooks-install.js +2 -1
  144. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  145. package/dist/lib/lanes/host-capacity.js +1 -0
  146. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  147. package/dist/lib/lanes/keep-alive.js +3 -1
  148. package/dist/lib/lanes/links.js +10 -4
  149. package/dist/lib/lanes/loop.js +16 -1
  150. package/dist/lib/lanes/node-binary.d.ts +17 -0
  151. package/dist/lib/lanes/node-binary.js +30 -0
  152. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  153. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  154. package/dist/lib/lanes/reconcile.js +4 -0
  155. package/dist/lib/lanes/remote-control-lane.js +2 -0
  156. package/dist/lib/lanes/spawn.d.ts +3 -0
  157. package/dist/lib/lanes/spawn.js +119 -9
  158. package/dist/lib/lanes/store.d.ts +6 -0
  159. package/dist/lib/lanes/store.js +107 -18
  160. package/dist/lib/lanes/types.d.ts +2 -0
  161. package/dist/lib/lanes/watch.js +28 -5
  162. package/dist/lib/lanes/watcher-main.js +4 -3
  163. package/dist/lib/lanes/watcher.d.ts +4 -0
  164. package/dist/lib/lanes/watcher.js +110 -21
  165. package/dist/lib/mesh/api.d.ts +32 -0
  166. package/dist/lib/mesh/api.js +75 -0
  167. package/dist/lib/plan-limit-nag.d.ts +0 -16
  168. package/dist/lib/plan-limit-nag.js +17 -18
  169. package/dist/lib/secret-scan/detect.js +9 -0
  170. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  171. package/dist/lib/secret-scan/repo-config.js +237 -0
  172. package/dist/lib/secret-scan/rules.d.ts +7 -1
  173. package/dist/lib/secret-scan/rules.js +3 -1
  174. package/dist/lib/secret-scan/staged.d.ts +5 -0
  175. package/dist/lib/secret-scan/staged.js +19 -2
  176. package/dist/lib/secret-scan/types.d.ts +2 -0
  177. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  178. package/dist/lib/sync/server-owned-local-only.js +75 -0
  179. package/dist/main.d.ts +0 -2
  180. package/dist/main.js +19 -57
  181. package/dist/utils/package-use-lease.js +27 -9
  182. package/dist/utils/plan-gate-error.d.ts +30 -14
  183. package/dist/utils/plan-gate-error.js +60 -46
  184. package/dist/utils/team-upgrade.d.ts +19 -6
  185. package/dist/utils/team-upgrade.js +37 -39
  186. package/dist/utils/version-gate.d.ts +7 -0
  187. package/dist/utils/version-gate.js +66 -0
  188. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -2,7 +2,262 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.160] — 2026-10-22
6
+ - The bundled sync engine now uses hq-cloud 6.18.98. When HQ pauses an account because of usage, `hq sync` now shows the server's usage-pause message and a Billing link instead of a generic failure (hq-cloud #1036).
7
+ - `hq billing usage` now shows the amounts on HQ's usage statement, which is what the invoice bills. Before this it re-priced the meters itself, so it could show a different figure than the statement, for example charging the vault fee during the trial when the statement waived it.
8
+ - When HQ pauses an account because of usage (spend limit reached, card needed, or trial cap hit), `hq` now shows the server's own message and a Billing link, with no plan-limit or Upgrade wording and no old plan names (Starter, Workforce, Individual). This covers sync, skills, agent enroll, meetings, members, secrets, integrations and remote databases. `hq billing upgrade` now opens the usage-pricing switch page instead of a Workforce checkout. `hq billing usage` now shows the company's usage view, and tells a member to use `--personal` for their own usage. Plan and trial reads use the server's shared plan status when it is available, and older servers keep working as before. (US-024)
9
+
10
+ - `hq files acl` and `hq access` show a path whose most-specific ACL row has no grants and is not open as locked by that row (`lockedBy` in `--json`), instead of as inheriting a broader `*` grant. An open empty row is not a lock: broader grants still apply. An empty ancestor row, whose open flag the server does not report, reads as locked unless that row is open.
11
+ - `hq billing upgrade` no longer dead-ends on the closed HQ Workforce checkout. When hq-pro answers that Workforce is closed to the company (`workforce_closed_to_new_signups`), the command explains that the company stays on Starter unless it switches to usage pricing, says to add a card first with `hq billing checkout`, and opens the console's usage-switch page for the company (`--no-browser` prints it; `--json` returns `{ url, legacyPlansClosed: true, message }`). The open-checkout line no longer quotes a plan price. The over-limit notice (`hq whoami`) and the plan-limit refusal printed by `hq secrets`, `hq members` and sync now read hq-pro's `legacyPlansClosed` field: while Workforce is closed they offer "Switch to usage pricing" with the switch link and never the free Workforce month or an "Upgrade to HQ Workforce" line. Older servers keep today's wording.
12
+ - `hq-hook` gains the US-021 bridge used by master-hook.sh: `hq-hook bridge-plan`
13
+ lists the registry hooks whose `hook-engine-port-<id>` flag is `shadow` or
14
+ `on`, `hq-hook bridge <id>` runs one hook natively (shadow compares with bash
15
+ and returns the bash result; on returns the native result; a missing port or
16
+ a declined payload falls back to bash), and `hq-hook --self-check` reports the
17
+ engine version and bridge schema. The local kill switch `HQ_HOOK_ENGINE=off`
18
+ or the file `~/.hq/hook-engine.off` forces bash before any flag is read.
19
+ Shadow comparisons are appended to `~/.hq/logs/hook-engine.jsonl` with hook
20
+ ids, hashes, exit codes and timings only.
21
+ - `hq-hook run derive-trigger-facts` and `hq-hook run eval-trigger` are native
22
+ ports of the two policy-trigger helpers (US-008, first slice), byte-identical
23
+ to the bundled shell on the parity corpus, with git facts read in-process.
24
+ - `hq core hook-engine-path` now runs the engine's `--self-check` before
25
+ printing its path, and writes `~/.hq/bin/hq-hook/current` so hooks can find
26
+ the pinned engine without starting Node. A binary that fails its self-check
27
+ exits non-zero and removes that pointer, keeping bash hooks active.
28
+
29
+ ## [5.345.159] — 2026-10-22
30
+ - `hq lanes link read` and `wait` now require the selected conductor session to own the requested link, preventing accidental cross-link report consumption while preserving explicit session resume/delegation and child inbox access.
31
+ - `hq lanes resume` now uses the canonical lane ID after resolving a short ID. When low disk space queues a resume, it keeps the requested instruction for later admission and reports the lane as queued rather than started.
32
+ - `hq sync status` now accepts `--company <slug>` and shows only that
33
+ company's sync journal. A company with no journal gets a message that names
34
+ it, and a blank value is rejected. Before this, the option was refused as
35
+ unknown. Feedback 7dee0a07-55a6-43f1-9b76-b4293bdd371b.
36
+
37
+ - The bundled sync engine now uses hq-cloud 6.18.92. HQ Sync never uploads
38
+ or downloads company secret folders, even when an HQ root has an old or
39
+ missing `.hqignore`. The default ignore list now covers
40
+ `companies/<co>/secrets/`, `companies/<co>/settings/`, and any `.secrets`
41
+ folder or file at any depth. A folder named `secrets` inside company
42
+ knowledge still syncs. Files already on disk stay where they are.
43
+ - The bundled sync engine now uses hq-cloud 6.18.95 (hq-cloud #1019 and
44
+ #1021). HQ Sync excludes credentials files from sync, and the credentials
45
+ match is now case-insensitive.
46
+ - The bundled sync engine now uses hq-cloud 6.18.97, the current npm `latest`.
47
+ Sync now keeps local settings files intact when it repairs a sync record
48
+ (hq-cloud #1035). It also carries the 6.18.96 fixes: pull no longer deletes
49
+ or overwrites local files right after a sync record is recovered,
50
+ `hq sync --watch` waits to pull until a failed push is retried, sync stops
51
+ retrying when a sandbox blocks the network, the personal-vault sync record is
52
+ tied to the signed-in person, and pulls report every file they delete or skip.
53
+ - `hq core checkpoint` no longer leaves the checkpoint sibling without a queue file. The launcher now creates an empty `workspace/checkpoints/sibling/pending.jsonl` before it starts the sibling, and the sibling prompt treats a missing or empty queue as empty instead of reporting an error (feedback_5ad007c4).
54
+ - Stop `hq daemon` from restarting a failing service forever. A service that
55
+ fails for a cause a restart cannot fix (a missing or non-executable binary,
56
+ exit code 77/78/126/127, or an npm error code such as ETARGET or E404) is
57
+ held at once. Any other failure keeps the usual backoff until the sixth
58
+ failure without 30 minutes of healthy running, then the service is held.
59
+ A held service shows as `held` with its reason in `hq daemon status`, stays
60
+ held across daemon restarts (recorded in `~/.hq/daemon/holds.json`), and
61
+ starts again on `hq daemon restart <unit>`, a command change, or a CLI
62
+ update.
63
+
64
+ Held bots remain daemon-owned when started or stopped through `hq bot`; an explicit start asks the supervisor to retry instead of launching a separate process. A failed start reports the current hold reason and recovery command.
65
+
66
+ - Exhausted transient sync/mesh holds now wait the existing 30-minute healthy
67
+ episode window, then claim one supervised retry durably before starting it.
68
+ Failure re-holds for another window; only a continuously running probe earns
69
+ the healthy reset. Daemon restarts keep the deadline and claim. Missing or
70
+ invalid legacy deadlines get one full window instead of an immediate burst.
71
+ A durable-claim write failure keeps the service held. Permanent holds and
72
+ other services remain manual. An explicit stop of a recovering sync/mesh unit
73
+ persists across daemon restarts and command/version changes until an explicit
74
+ start/restart; a paused plan never launches a probe while blocked. This does
75
+ not change stop semantics for healthy units. The held reason includes the
76
+ next supervised retry time.
77
+ - `hq deploy access <app> --mode public` now asks for approval when the app is not already public, and any change that removes an app's password protection prints a warning and asks for approval. The prompt names the app and the old and new mode. Pass `--yes` to approve without a prompt. Without a terminal and without `--yes`, the command refuses, changes nothing, and exits 1. Changing the mode now keeps the saved member lists and company id, so switching back to selected restores the previous members. Unexpected failures now include the error type and message, and unexpected hq-deploy responses include the HTTP status, error code and server message.
78
+ - `hq deploy access app_<id> --mode ...` now reads the app's own record before changing access by direct app id, so it no longer clears a real password without approval when the saved access policy does not show the password. Removing a password still needs approval or `--yes`. If the app record cannot be read (403 or 404), belongs to a different id, or does not state whether the app has a password, the command refuses and changes nothing, even with `--yes`.
79
+ - When `hq dm channel`, `hq dm <channel>`, `hq channels members` or `hq files share --channel` cannot find a channel by name, the error now lists up to three of your channels with the closest names. Comparison ignores the `project-` prefix and the trailing 8-character id, so `desktop-agent-coordination` suggests `project-desktop-agent-creation-1331ed7f`. With `--json`, `hq dm channel` and `hq channels members` print the error with a `suggestions` array of `{id, name}`. The exit code is still 1.
80
+ - `hq sync status` now lists local files under a company's server-owned folders (`sources/`, `signals/`, and `ontology/` outside `entities/`) that are not in the sync journal. The cloud pipeline writes those folders and push skips them without counting them as pending, so meeting notes or signals filed there by a local skill never reached the vault while status reported a clean tree. The warning names up to 20 paths per company and says to keep self-written notes under `knowledge/` or add sources through an ingestion flow.
81
+ - `hq lanes resume` and `hq lanes message` now print a line to stderr before
82
+ the resume-time `/compact` starts, saying it can take up to 10 minutes and
83
+ that `--no-compact` skips it. Before this the command printed nothing while
84
+ compaction ran and looked hung.
85
+ - `hq files acl <prefix>` now lists the more specific rules under the prefix (for example a bare `knowledge` row or a `knowledge/agents/` folder row), including rules that carry no grants. Each of those rules replaces the prefix's grants for its own paths, so a grant on `knowledge/*` did not reach them, and the old view hid them because it only showed grants. `--json` carries them as `masking`.
86
+ - `hq files share <prefix> --with <principal>` warns when the new grant is shadowed by more specific rules that do not include that principal, names the paths, and shows the share command for them. Both need hq-pro with the `masking` / `maskedBy` fields; older servers print the old output. (feedback_466fe8b8)
87
+ - `hq secrets share` and `hq secrets unshare` now explain a permission refusal plainly: changing who can see a secret needs admin permission on it, and read access is not affected. The old "Not authorized to share this secret" text made people and agents think the secret itself was locked to them.
88
+ - `hq secrets share` now prints the server's explanation when a secret takes its access from a broader ACL (for example `SLACK/*`) and names the path to share instead. It used to print "Concurrent modification — please retry", which retrying would never fix.
89
+ - New `hq mesh project archive|complete|activate <projectId> --company <slug>`
90
+ moves a Board between active, completed and archived through the server's
91
+ lifecycle route, and `hq mesh project lifecycle [projectId] --company <slug>`
92
+ lists or shows that state. Use `archive` to put away a duplicate Board (for
93
+ example one registered under a board id next to the live one registered
94
+ under the folder slug). Nothing is deleted: stories, the project folder,
95
+ channel, memberships and history stay; the project only leaves the active
96
+ Board list and the organize candidates, and `activate` brings it back. When
97
+ the Board still has stories, `archive` stops and asks for
98
+ `--confirm-nonempty`. A project already in the requested state is reported
99
+ and left alone. Feedback 5d596dd0-6748-4aba-b437-f57ee914e311.
100
+ - `hq dm send` gains `--attach <path>` (repeatable, up to 5 files, 25 MB
101
+ each). Before this, sending an image to a teammate or agent meant copying it
102
+ into the company vault, running a sync push, and pasting the path into the DM
103
+ text. Now the CLI presign-uploads each file into the conversation's own vault
104
+ scope (`chat/attachments/dm/<uidA--uidB>/…` in the sender's personal vault
105
+ for a 1:1 DM, `chat/attachments/chan/<channelId>/…` in the channel's company
106
+ vault for a named channel, including `--thread` replies), sends it as
107
+ `attachments[]` on the message, and prints each file's vault path. The
108
+ message body may be empty when a file is attached. Missing, oversized, or
109
+ too many files are refused by name before anything is uploaded; group DMs,
110
+ scheduled sends (`--at`/`--in`), and agent senders on 1:1 DMs are refused
111
+ with a plain message.
112
+ - `hq secrets share` and `hq secrets unshare` now accept a fleet agent uid (`agt_` plus a 26-character upper-case Crockford ULID) as the principal, so `/delegate` to a fleet agent no longer stops at "Invalid principal 'agt_…'". The CLI sends the agent's machine email (`agt-<ulid>@agents.getindigo.ai`) and hq-pro stores the grant under the agent's `agt_…` principal (granteeType `person`), which is the row `hq secrets acl` then lists. `share` first checks the company agent roster and refuses, writing nothing, when the agent is not an active agent of this company; a malformed uid or machine email is rejected locally. The success line names both the uid and the email.
113
+ - `hq signals list` and `hq signals get` now accept `--company <slug>`, the
114
+ same flag `hq meetings` and `hq integrations` take. It means the same thing
115
+ as `--entity`. Before this, `hq signals list --company <slug>` failed with
116
+ `unknown option '--company'`. Passing `--entity` and `--company` with
117
+ different values is an error.
118
+ - `hq secrets script approve` no longer replaces an existing approval by accident. Approving an `--id` that is already approved for a different path or hash now fails, keeps the stored approval, and says what to do: pass `--replace` to replace it on purpose, use a new `--id`, or approve one HQ-relative `--remote-path` so a single approval matches on every machine. Re-sending the identical approval still succeeds. Needs hq-pro with the matching `script_id_exists` refusal; older servers keep replacing. (feedback_8224c4d5)
119
+ - When the minimum-version gate refuses an older local or user-level copy of `hq` (for example a pnpm user-global install in `~/.local` that shadows `/usr/bin/hq`), it now runs the command with a newer global `hq` from `/usr/bin`, `/usr/local/bin` or `/opt/homebrew/bin` instead of exiting 75. The old copy still never runs the command, and the gate still exits 75 when no qualifying global install exists.
120
+ - New `hq repo add <path> --company <slug>` registers a git checkout with HQ.
121
+ It links the checkout into `repos/private/<name>` (or `repos/public/` with
122
+ `--public`) and adds it to `companies.<slug>.repos` in
123
+ `companies/manifest.yaml`. Before this, you had to make the symlink and edit
124
+ the manifest by hand. A checkout that already sits directly under `repos/` is
125
+ registered where it is. Running it again changes nothing. `--dry-run` shows
126
+ the plan without changing anything. Comments and layout in the rest of the
127
+ manifest stay as they were.
128
+ - `hq lanes stop` (and every other lane write) no longer fails with
129
+ `store_lock_timeout` because of a lane record lock left behind by a process
130
+ that died. A lock is now reclaimed when the process that wrote it no longer
131
+ exists, or, when the lock file is empty or unreadable (a process killed
132
+ between creating and writing it), once the file is older than 30 seconds.
133
+ The lock now records its hostname, and the dead-process rule applies only
134
+ to locks written on this machine; a lock from another host or with no host
135
+ falls back to the 30-second age rule.
136
+ Before this, an empty or partial lock file was never reclaimed. A lock held
137
+ by a live process is still respected.
138
+ Reclaiming a stale lock is now atomic, so two waiters cannot both take it,
139
+ and a holder releases the lock only if it is still its own. A lock whose
140
+ timestamp is in the future ages out by file time. The 30-second window can
141
+ be changed with `HQ_LANES_LOCK_STALE_MS`.
142
+ - New `hq feedback show <id>` for HQ operators. It prints one feedback row
143
+ (title, status, company, reporter, body) and lists its screenshots, email
144
+ attachments and log bundle. `--download <dir>` saves the attachments to that
145
+ directory, and `--logs` adds the log bundle. `--json` prints the row with
146
+ download links removed. The server only allows operators, so anyone else
147
+ gets an "operators only" error, and the command never prints a download
148
+ link or token. Needs hq-pro#5143 for screenshot links. Feedback
149
+ b1d14d0a-caff-4bae-bee4-977fc6b1d6e6.
150
+ - New `hq deploy access` command manages who can open a deploy app.
151
+ `hq deploy access get <app>` shows the current mode and selected members.
152
+ `hq deploy access <app> --mode company|selected|public` changes the mode, and
153
+ `--add-user`, `--remove-user`, `--add-group`, `--remove-group` edit the
154
+ selected-members list. `<app>` is an app id, subdomain, or exact name. It
155
+ calls hq-deploy's existing `/api/apps/:id/access-policy` API with your HQ
156
+ sign-in, so only the app owner or a company admin can change access. Any
157
+ other mode is refused before a request is sent.
158
+ For an app with no saved policy, the company id comes from the app record
159
+ or from `--company` (or the active company), never the hq-deploy org id; if
160
+ it cannot be found the command stops with an error and changes nothing.
161
+ - `hq dm <name>` now sends to a person in your contacts whose name matches exactly, instead of a channel that shares that slug, and prints which person it chose. Use `#name` or `--channel name` to post to the channel.
162
+ - `hq dm <name> "..."` now says which channel the name resolved to when it
163
+ differs from the channel's display name. Before this, `hq dm setup "hi"`
164
+ printed only `Message posted to #welcome.`, with nothing linking `setup` to
165
+ `#welcome`. The line now reads
166
+ `Message posted to #welcome (resolved 'setup' to personal channel chn_...).`
167
+ - `hq lanes watch` keeps monitoring loop lanes between phase envelopes, even
168
+ when the last phase briefly records `done`. It continues account checks and
169
+ waits for the live loop controller to stop, fail, or exit before treating that phase as
170
+ terminal. Ordinary lanes and failed launches without loop metadata still exit.
171
+ - `hq feedback bug|feature` now prints a `Track:` link to the ticket's public page on status.hqforwork.com. New `hq feedback link <id>` prints that link for an existing ticket, and `hq feedback update <id> --state <state> --note "<text>"` posts a customer-visible status update that also lands in the ticket's #hq-feedback thread.
172
+ - Single-company local bots bind before DM, room, and scheduled-job turns. Company-setting changes pause new turns until restart; prior company conversations are archived before starting fresh. Binding failures retain pending work without invoking the model. Existing company bots archive unproven conversations once after upgrade while preserving pending reply and job recovery. Archives are retained for manual recovery.
173
+ - Agent kit URL wakes accept repeatable `--header-secret Header-Name=VAULT_SECRET_NAME` references, resolving values with the agent identity at each wake without storing or logging them.
174
+ - The bundled shell scripts `rebuild-company-knowledge-index.sh` and `rebuild-projects-index.sh` now follow the same argument rules as `hq core rebuild-index company-knowledge|projects`. `--help` or `-h` prints usage, exits 0, and writes nothing, even when `--accept` is also given. A company slug (positional, `--company <slug>` or `--company=<slug>`) limits writes to that company's INDEX.md; `projects` no longer rewrites other companies or `personal/projects/INDEX.md` in that case. `--accept` overwrites a hand-edited INDEX.md in the selected scope. Without it, `rebuild-company-knowledge-index.sh` now keeps a hand-edited INDEX.md the way `rebuild-projects-index.sh` already did. Unknown options, a second slug, an invalid slug or a missing company exit 1 before any write. A symlinked INDEX.md is refused with exit 1 and its target is not read or written. Each INDEX.md is now installed by renaming a temp file from the same directory over it, as the native renderer does, so another file hard-linked to the old INDEX.md keeps its bytes. An existing INDEX.md keeps its permission bits. Before, the bundled scripts ignored all arguments and rebuilt every index.
175
+ - Restrict lane moves and account recovery to accounts explicitly assigned to the lane company. Configure account ownership with `company` in `personal/lanes/config.json`; unavailable same-company recovery capacity leaves pending work queued. Preserve existing untagged account assignments while enforcing company ownership for creation, queued admission, first assignment, profile refresh, and account moves.
176
+ - Make account company enforcement opt-in per provider: while no account for a provider has a `company` tag, company lanes keep creating and assigning on untagged accounts, consistently at creation, first assignment, and queued admission. Once a provider has a tagged account and every enabled candidate is excluded only by company, creation and first assignment report `account_company_mismatch` with the lane company, the excluded accounts, and the `company` key in `personal/lanes/config.json`. Recorded Claude-account recovery, balance move suggestions, and the company account-retry hold now use the resolved lane company (lane, mesh company, then project owner).
177
+ - The pipeline driver no longer exits 22 (deadline) or 28 (stall) for a phase
178
+ that is still waiting in its loop lane's queue, for example while the lane is
179
+ queued behind the host throttle or busy on another envelope. Once the lane
180
+ picks the phase up it gets its full deadline budget. This ports
181
+ hq-core-staging #1357 to the native driver and also covers the stall check.
182
+ Queue time is bounded by `PC_QUEUE_MAX_MIN` (default 240): a phase still
183
+ waiting after that long ends the run with exit 22 and a QUEUED_TOO_LONG
184
+ message naming the lane. An admitted lane with an empty queue that is not
185
+ working on the phase no longer counts as holding it, so the deadline applies.
186
+ While the lane is busy on an envelope from another run, the phase waits on
187
+ that envelope's own watchdog, up to the queue ceiling.
188
+ - Loop lane workers, watchers and the drain hook registration that outlive a
189
+ Node upgrade now use a live Node binary when the one they were launched with
190
+ has been removed: the Homebrew prefix `bin/node` link for a Cellar path,
191
+ else `node` on PATH. This covers watcher queued-message recovery, terminal
192
+ keep-alive scheduling and senior message delivery.
193
+ - `hq sync push`, `hq sync push --all` and `hq sync now` no longer report
194
+ success when files failed to upload. When hq-cloud reports failed uploads
195
+ (for example a refused presign), the CLI prints how many files were not
196
+ uploaded and lists them, adds `filesUploadFailed` and `uploadFailedPaths`
197
+ to the `--json` complete event, and exits 1. A path you type on
198
+ `hq sync push` that an ignore rule drops (a `.hq-*` file or a generated
199
+ INDEX.md) now fails with the rule that dropped it instead of printing a
200
+ 0-file success. Both need the hq-cloud release that reports these cases.
201
+ Feedback 943456f4-56bf-43bc-94fa-bdbc7384ec41,
202
+ 63bfc302-ae00-4d32-9d94-0a5eaf8e1314,
203
+ 8c294805-8761-44c4-abf2-5ab14c263d84.
204
+ - `hq core rebuild-index --help`, `-h`, and `hq core rebuild-index <target> --help` now print usage and exit without rewriting any INDEX.md. Before, `rebuild-index all --help` (and other targets without their own help) rebuilt every index.
205
+ - `hq core worktree --base origin/<branch>` (and the scaffold `worktree.sh`)
206
+ now accepts the `origin/` prefix instead of building `origin/origin/<branch>`.
207
+ - The `hq monitor` expiry message now names `--timeout <duration>` and
208
+ `--persistent` for re-arming a longer watch. The 5-minute default is unchanged.
209
+ - The CLI no longer waits about 4 minutes on a leftover `update.pending.json`
210
+ after a reboot. A request written before the current boot, or whose pid is
211
+ gone or belongs to another user (EPERM), is treated as stale and removed.
212
+ - Fixed `hq integrations connect --link-only` ignoring a supplied OAuth client ID and stdin client secret. Link-only sign-in now uses the caller's registered app while keeping the console callback and returning the sign-in URL without opening a browser.
213
+ - `hq lanes loop` no longer stalls when a phase's watcher is still finishing a slow mesh call as the next phase starts. The finishing watcher now releases its single-owner claim right after the terminal write, before mesh and follow-up awaits. A new watcher that finds a live claimant from an older run retries the claim for up to 60 s instead of exiting, and a lost claim is logged as `watcher_claim_lost`. A live claimant on the same run still makes the new watcher exit at once.
214
+ - After it releases the claim, a finishing watcher's follow-up lane writes (status gate, blocker and KR freshness, repair continue) and queued-message recovery are skipped once the lane has moved to a newer run, so they cannot overwrite or re-queue the next phase.
215
+ - `hq lanes accounts list` no longer reports an expired Claude sign-in as logged in. For each Claude account it reads the access-token expiry of the credential the lane worker uses (the Keychain item for that account's `CLAUDE_CONFIG_DIR` on macOS, or `.credentials.json`). An access token past its expiry still counts as signed in when it has a refresh token that `hq capacity` has not recorded as revoked, since Claude Code renews it at startup. A sign-in that cannot be renewed shows `logged_in: false`, `sign_in_state: "expired"`, `sign_in_expires_at`, `error: "sign_in_expired"` and a `fix_command`. Lane auto-select skips that account, and a lane started on it fails before the worker starts with a message to open Claude Code on the account and run `/login`, instead of exiting 10 to 20 seconds in with an OAuth error.
216
+ - `hq capacity login` and the Remote Control lane check the same Claude sign-in expiry, and the quota-held explicit account pick passes the expiry check through to its status read.
217
+ - Loop lanes created with `--no-story` now run every phase. Before, each phase after the first failed with `--story must be non-empty` because the loop passed an empty story to the resume step. The loop now leaves the story unset for these lanes, and a phase envelope that names a `story_id` on a no-story lane is ignored with a warning instead of writing a lane record that could no longer be loaded or cancelled. Feedback: "Loop lanes created without a story fail every phase after the first".
218
+ - `hq lanes set --model/--effort` now works for a lane that runs on an Outpost.
219
+ The change is forwarded to the Outpost that owns the lane, which re-checks
220
+ that the caller is the lane or its senior before applying it. Before this, the
221
+ command refused any Outpost lane. An Outpost that has not been upgraded yet
222
+ returns `outpost_cli_upgrade_required` instead of an unknown-command error.
223
+ - The phase worker brief now tells the worker to copy each acceptance criterion
224
+ verbatim into its handoff `ac_evidence[].criterion`, keep its 0-based index,
225
+ and not shorten or paraphrase the text, so the criteria recheck matches them
226
+ (feedback fa21c737).
227
+ - The native pipeline driver no longer exits 23 when a worker writes a handoff
228
+ that fails validation. It moves the worker's file to
229
+ `handoffs/<story>-<phase>.invalid.<n>.json`, records the phase as failed with
230
+ the validation error, and keeps driving; the fail cap still bounds retries.
231
+ The loop lane runner no longer overwrites a handoff the worker already wrote
232
+ to `result_path`; its own failed handoff goes beside it as
233
+ `<name>.lane-failure.json`. The phase brief now says that `summary`,
234
+ `context_for_next` and each `ac_evidence` `evidence` are single strings.
235
+ - Company project and knowledge index rebuilds preserve manually curated legacy files even when they still carry an auto-generated header. Unstamped files are refreshed only when their body matches the current render; `--accept` remains the explicit overwrite path. Native rendering and bundled compatibility scripts apply the same protection (feedback f4468815).
236
+ - `scripts/scaffold-freshness.json` now records the current sha256 of the bundled `rebuild-company-knowledge-index.sh`, `rebuild-projects-index.sh` and `worktree.sh`, which #2115 and #2121 changed without updating the manifest. The tag-only scaffold bundle freshness check failed on v5.345.157 because of this.
237
+ - `hq scan secrets --staged` now reads `[[allowlists]]` (and the legacy
238
+ `[allowlist]`) from the repo's own `.gitleaks.toml`, with gitleaks semantics
239
+ for `paths`, `regexes`, `regexTarget`, `stopwords`, `commits`, `targetRules`
240
+ and `condition = "AND" | "OR"`, on top of the built-in rules, for that repo
241
+ only. The file is read from git, not the working tree: it applies only once
242
+ committed, so a commit that changes it is scanned with the previous version.
243
+ Every allowlist needs `paths` that start with `^` and a literal name
244
+ (such as `^fixtures/`); `regexes` and `stopwords` need `condition = "AND"`.
245
+ A file that breaks these rules, cannot be parsed, is a symlink, or has an
246
+ unknown key or an unsafe regex stops the scan with exit 2 and names the
247
+ problem.
248
+ - HQ staff can review marketplace listings from the CLI. `hq staff listings queue` lists pending listings, oldest first. `hq staff listings approve <lst_id> [--note <text>]` approves one and prints any older versions it replaced, and `hq staff listings reject <lst_id> --note <reason>` rejects one with a required reason. A `@vyg.ai` staff session is no longer refused locally by `hq staff` commands, matching the server.
249
+ - `hq company settings set --work-mesh-live-enabled` help now says live work tracking is on by default, matching the server change that turns Work Mesh Live on for every company unless it sets the value to false.
250
+ - Add `hq crm entity list` and `hq crm entity get` to read CRM entities, and enforce the session company lock for CRM reads and upserts.
251
+
5
252
  ## [5.345.156] — 2026-10-21
253
+ - Automatic Stop-gate checkpoints now start at most one background checkpoint
254
+ agent per session every 30 minutes. Each of those runs costs a fresh agent
255
+ session's worth of tokens, and most of them found nothing new. A skipped run
256
+ still writes the checkpoint thread, so the Stop gate behaves the same.
257
+ `/checkpoint` and runs without a session id are unchanged, and
258
+ `HQ_CHECKPOINT_AGENT=0` still turns the agent off. Feedback
259
+ 90c33997-5a41-4a23-a23e-f7172bfead88.
260
+
6
261
  - `hq doctor --fix` stops pulling a company the sync service could not find
7
262
  for your HQ account. When a pull fails because the company does not exist
8
263
  for you or access is refused, the next check shows that company's stale
@@ -16,9 +16,46 @@
16
16
 
17
17
  set -euo pipefail
18
18
 
19
+ for arg in "$@"; do
20
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
21
+ printf '%s\n' \
22
+ 'Usage: hq core rebuild-index company-knowledge [<company> | --company <company>] [--accept]' \
23
+ '' \
24
+ 'With a company, rebuild only that company'\''s index. Without a company, retain the all-company rebuild. Help performs no writes.'
25
+ exit 0
26
+ fi
27
+ done
28
+
19
29
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
20
30
  cd "$HQ_ROOT"
21
31
 
32
+ # Company scope, mirroring the native companyScope() contract: one slug as a
33
+ # positional, --company <slug>, or --company=<slug>. --accept overwrites a
34
+ # hand-edited INDEX.md in the selected scope. Anything else is refused before
35
+ # any write.
36
+ SCOPE=""
37
+ ACCEPT=0
38
+ scope_error() { printf 'hq: rebuild-index company-knowledge: %s\n' "$1" >&2; exit 1; }
39
+ while [[ $# -gt 0 ]]; do
40
+ value="$1"
41
+ if [[ "$value" == "--accept" ]]; then ACCEPT=1; shift; continue; fi
42
+ if [[ "$value" == "--company" ]]; then shift; value="${1:-}";
43
+ elif [[ "$value" == --company=* ]]; then value="${value#--company=}"; fi
44
+ [[ -n "$value" && "$value" != -* ]] || scope_error "expected a company slug; unknown or incomplete option"
45
+ [[ -z "$SCOPE" ]] || scope_error "supply only one company slug"
46
+ [[ "$value" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]] || scope_error "invalid company slug"
47
+ SCOPE="$value"
48
+ shift
49
+ done
50
+ if [[ -n "$SCOPE" ]]; then
51
+ [[ -e "companies/$SCOPE" || -L "companies/$SCOPE" ]] || scope_error "no company directory named \"$SCOPE\""
52
+ [[ -d "companies/$SCOPE" && ! -L "companies/$SCOPE" ]] || scope_error "company must be a real directory"
53
+ scope_content="companies/$SCOPE/knowledge"
54
+ if [[ -e "$scope_content" || -L "$scope_content" ]]; then
55
+ [[ -d "$scope_content" && ! -L "$scope_content" ]] || scope_error "content must be a real directory"
56
+ fi
57
+ fi
58
+
22
59
  DATE=$(date -u +%Y-%m-%d)
23
60
 
24
61
  titleize() {
@@ -86,6 +123,33 @@ sha256_file() {
86
123
  fi
87
124
  }
88
125
 
126
+ # replace_entry <content-file> <target>: install content as <target> by
127
+ # renaming a same-directory temp file over the directory entry, like native
128
+ # atomicWrite(). The old inode is never written, so a hardlinked alias keeps
129
+ # its bytes. Symlinks and directories at <target> are refused before the
130
+ # rename, so mv never follows or descends into them. An existing target's
131
+ # permission bits are kept; a new file gets 0666 minus the umask.
132
+ replace_entry() {
133
+ local content="$1" target="$2" dir tmp mode
134
+ if [[ -L "$target" || -d "$target" ]]; then
135
+ echo "rebuild-company-knowledge-index: refused ${target} (INDEX.md is a symlink or directory)" >&2
136
+ rm -f "$content"
137
+ exit 1
138
+ fi
139
+ dir=$(dirname "$target")
140
+ tmp=$(mktemp "${dir}/.INDEX.md.XXXXXX") || { rm -f "$content"; exit 1; }
141
+ if [[ -e "$target" ]]; then
142
+ mode=$(stat -c %a "$target" 2>/dev/null || stat -f %Lp "$target")
143
+ else
144
+ mode=$(printf '%o' $(( 0666 & ~$(umask) )))
145
+ fi
146
+ if ! { cat "$content" > "$tmp" && chmod "$mode" "$tmp" && mv -f "$tmp" "$target"; }; then
147
+ rm -f "$tmp" "$content"
148
+ exit 1
149
+ fi
150
+ rm -f "$content"
151
+ }
152
+
89
153
  stamp_generated() {
90
154
  local tmp="$1"
91
155
  local out="$2"
@@ -95,7 +159,10 @@ stamp_generated() {
95
159
  local hash
96
160
  hash=$(sha256_file "$norm")
97
161
  rm -f "$norm"
98
- awk -v h="$hash" '{print} /^> Auto-generated. Updated: / {print "<!-- hq-generated-sha256: " h " -->"}' "$tmp" > "$out"
162
+ local stamped
163
+ stamped=$(mktemp)
164
+ awk -v h="$hash" '{print} /^> Auto-generated. Updated: / {print "<!-- hq-generated-sha256: " h " -->"}' "$tmp" > "$stamped"
165
+ replace_entry "$stamped" "$out"
99
166
  }
100
167
 
101
168
  write_knowledge_index() {
@@ -103,6 +170,12 @@ write_knowledge_index() {
103
170
  local kdir="companies/${co}/knowledge"
104
171
  [[ -d "$kdir" ]] || return 0
105
172
  local out="${kdir}/INDEX.md"
173
+ # Never read or write through a symlinked INDEX.md: it could point at
174
+ # another company's file.
175
+ if [[ -L "$out" ]]; then
176
+ echo "rebuild-company-knowledge-index: refused ${out} (INDEX.md is a symlink)" >&2
177
+ exit 1
178
+ fi
106
179
  local title
107
180
  title="$(titleize "$co") Knowledge"
108
181
  local tmp
@@ -140,6 +213,41 @@ write_knowledge_index() {
140
213
  )
141
214
  } > "$tmp"
142
215
 
216
+ if [[ -e "$out" && "$ACCEPT" != 1 ]]; then
217
+ if grep -q '<!-- hq:keep -->' "$out" || ! grep -q '^> Auto-generated. Updated: ' "$out"; then
218
+ echo "rebuild-company-knowledge-index: refused to overwrite ${out} (hand-written or kept)" >&2
219
+ rm -f "$tmp"
220
+ return 0
221
+ fi
222
+ local stored actual normalized
223
+ stored=$(sed -n 's/^<!-- hq-generated-sha256: \([a-f0-9]*\) -->$/\1/p' "$out" | head -n 1)
224
+ if [[ -n "$stored" ]]; then
225
+ normalized=$(mktemp)
226
+ sed -e '/^<!-- hq-generated-sha256: [a-f0-9]* -->$/d' -e 's/^> Auto-generated. Updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/> Auto-generated. Updated: <date>/' "$out" > "$normalized"
227
+ actual=$(sha256_file "$normalized")
228
+ rm -f "$normalized"
229
+ if [[ "$stored" != "$actual" ]]; then
230
+ echo "rebuild-company-knowledge-index: refused to overwrite ${out} (generated content was edited)" >&2
231
+ rm -f "$tmp"
232
+ return 0
233
+ fi
234
+ else
235
+ # A legacy header is not proof that the generated body is untouched.
236
+ # Only upgrade an unstamped file when its complete body matches this render.
237
+ normalized=$(mktemp)
238
+ local candidate_normalized
239
+ candidate_normalized=$(mktemp)
240
+ sed 's/^> Auto-generated. Updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/> Auto-generated. Updated: <date>/' "$out" > "$normalized"
241
+ sed 's/^> Auto-generated. Updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/> Auto-generated. Updated: <date>/' "$tmp" > "$candidate_normalized"
242
+ if ! cmp -s "$normalized" "$candidate_normalized"; then
243
+ rm -f "$normalized" "$candidate_normalized" "$tmp"
244
+ echo "rebuild-company-knowledge-index: refused to overwrite ${out} (unstamped content differs; use --accept to overwrite)" >&2
245
+ return 0
246
+ fi
247
+ rm -f "$normalized" "$candidate_normalized"
248
+ fi
249
+ fi
250
+
143
251
  stamp_generated "$tmp" "$out"
144
252
  rm -f "$tmp"
145
253
 
@@ -155,6 +263,6 @@ while IFS= read -r dir; do
155
263
  write_knowledge_index "$co"
156
264
  COUNT=$((COUNT+1))
157
265
  fi
158
- done < <(find companies -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
266
+ done < <(if [[ -n "$SCOPE" ]]; then printf 'companies/%s\n' "$SCOPE"; else find companies -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort; fi)
159
267
 
160
268
  echo "rebuild-company-knowledge-index: regenerated ${COUNT} knowledge INDEX.md file(s)" >&2
@@ -8,9 +8,46 @@
8
8
 
9
9
  set -euo pipefail
10
10
 
11
+ for arg in "$@"; do
12
+ if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
13
+ printf '%s\n' \
14
+ 'Usage: hq core rebuild-index projects [<company> | --company <company>] [--accept]' \
15
+ '' \
16
+ 'With a company, rebuild only that company'\''s index. Without a company, retain the all-company rebuild. Help performs no writes.'
17
+ exit 0
18
+ fi
19
+ done
20
+
11
21
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
12
22
  cd "$HQ_ROOT"
13
23
 
24
+ # Company scope, mirroring the native companyScope() contract: one slug as a
25
+ # positional, --company <slug>, or --company=<slug>. --accept overwrites a
26
+ # hand-edited INDEX.md in the selected scope. Anything else is refused before
27
+ # any write.
28
+ SCOPE=""
29
+ ACCEPT=0
30
+ scope_error() { printf 'hq: rebuild-index projects: %s\n' "$1" >&2; exit 1; }
31
+ while [[ $# -gt 0 ]]; do
32
+ value="$1"
33
+ if [[ "$value" == "--accept" ]]; then ACCEPT=1; shift; continue; fi
34
+ if [[ "$value" == "--company" ]]; then shift; value="${1:-}";
35
+ elif [[ "$value" == --company=* ]]; then value="${value#--company=}"; fi
36
+ [[ -n "$value" && "$value" != -* ]] || scope_error "expected a company slug; unknown or incomplete option"
37
+ [[ -z "$SCOPE" ]] || scope_error "supply only one company slug"
38
+ [[ "$value" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]] || scope_error "invalid company slug"
39
+ SCOPE="$value"
40
+ shift
41
+ done
42
+ if [[ -n "$SCOPE" ]]; then
43
+ [[ -e "companies/$SCOPE" || -L "companies/$SCOPE" ]] || scope_error "no company directory named \"$SCOPE\""
44
+ [[ -d "companies/$SCOPE" && ! -L "companies/$SCOPE" ]] || scope_error "company must be a real directory"
45
+ scope_content="companies/$SCOPE/projects"
46
+ if [[ -e "$scope_content" || -L "$scope_content" ]]; then
47
+ [[ -d "$scope_content" && ! -L "$scope_content" ]] || scope_error "content must be a real directory"
48
+ fi
49
+ fi
50
+
14
51
  COMPANIES_DIR="companies"
15
52
  PROJECTS_DIR="personal/projects"
16
53
  OUT="${PROJECTS_DIR}/INDEX.md"
@@ -66,13 +103,42 @@ sha256_file() {
66
103
  fi
67
104
  }
68
105
 
106
+ # replace_entry <content-file> <target>: install content as <target> by
107
+ # renaming a same-directory temp file over the directory entry, like native
108
+ # atomicWrite(). The old inode is never written, so a hardlinked alias keeps
109
+ # its bytes. Symlinks and directories at <target> are refused before the
110
+ # rename, so mv never follows or descends into them. An existing target's
111
+ # permission bits are kept; a new file gets 0666 minus the umask.
112
+ replace_entry() {
113
+ local content="$1" target="$2" dir tmp mode
114
+ if [[ -L "$target" || -d "$target" ]]; then
115
+ echo "rebuild-projects-index: refused ${target} (INDEX.md is a symlink or directory)" >&2
116
+ rm -f "$content"
117
+ exit 1
118
+ fi
119
+ dir=$(dirname "$target")
120
+ tmp=$(mktemp "${dir}/.INDEX.md.XXXXXX") || { rm -f "$content"; exit 1; }
121
+ if [[ -e "$target" ]]; then
122
+ mode=$(stat -c %a "$target" 2>/dev/null || stat -f %Lp "$target")
123
+ else
124
+ mode=$(printf '%o' $(( 0666 & ~$(umask) )))
125
+ fi
126
+ if ! { cat "$content" > "$tmp" && chmod "$mode" "$tmp" && mv -f "$tmp" "$target"; }; then
127
+ rm -f "$tmp" "$content"
128
+ exit 1
129
+ fi
130
+ rm -f "$content"
131
+ }
132
+
69
133
  stamp_generated() {
70
- local input="$1" output="$2" normalized hash
134
+ local input="$1" output="$2" normalized hash stamped
71
135
  normalized=$(mktemp)
72
136
  sed 's/^> Auto-generated. Updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/> Auto-generated. Updated: <date>/' "$input" > "$normalized"
73
137
  hash=$(sha256_file "$normalized")
74
138
  rm -f "$normalized"
75
- awk -v h="$hash" '{print} /^> Auto-generated. Updated: / {print "<!-- hq-generated-sha256: " h " -->"}' "$input" > "$output"
139
+ stamped=$(mktemp)
140
+ awk -v h="$hash" '{print} /^> Auto-generated. Updated: / {print "<!-- hq-generated-sha256: " h " -->"}' "$input" > "$stamped"
141
+ replace_entry "$stamped" "$output"
76
142
  }
77
143
 
78
144
  company_title() {
@@ -84,6 +150,12 @@ write_company_projects_index() {
84
150
  company_root="${COMPANIES_DIR}/${company}"
85
151
  project_dir="${company_root}/projects"
86
152
  output="${project_dir}/INDEX.md"
153
+ # Never read or write through a symlinked INDEX.md: it could point at
154
+ # another company's file.
155
+ if [[ -L "$output" ]]; then
156
+ echo "rebuild-projects-index: refused ${output} (INDEX.md is a symlink)" >&2
157
+ exit 1
158
+ fi
87
159
  tmp=$(mktemp)
88
160
  {
89
161
  printf '# %s Projects\n\n' "$(company_title "$company")"
@@ -106,7 +178,7 @@ write_company_projects_index() {
106
178
  } > "$tmp"
107
179
  local existing=0 stored actual normalized
108
180
  [[ -e "$output" ]] && existing=1
109
- if [[ "$existing" == 1 ]]; then
181
+ if [[ "$existing" == 1 && "$ACCEPT" != 1 ]]; then
110
182
  if grep -q '<!-- hq:keep -->' "$output" || ! grep -q '^> Auto-generated. Updated: ' "$output"; then
111
183
  echo "rebuild-projects-index: refused to overwrite ${output} (hand-written or kept)" >&2
112
184
  rm -f "$tmp"
@@ -123,6 +195,20 @@ write_company_projects_index() {
123
195
  rm -f "$tmp"
124
196
  return 0
125
197
  fi
198
+ else
199
+ # A legacy header is not proof that the generated body is untouched.
200
+ # Only upgrade an unstamped file when its complete body matches this render.
201
+ normalized=$(mktemp)
202
+ local candidate_normalized
203
+ candidate_normalized=$(mktemp)
204
+ sed 's/^> Auto-generated. Updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/> Auto-generated. Updated: <date>/' "$output" > "$normalized"
205
+ sed 's/^> Auto-generated. Updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/> Auto-generated. Updated: <date>/' "$tmp" > "$candidate_normalized"
206
+ if ! cmp -s "$normalized" "$candidate_normalized"; then
207
+ rm -f "$normalized" "$candidate_normalized" "$tmp"
208
+ echo "rebuild-projects-index: refused to overwrite ${output} (unstamped content differs; use --accept to overwrite)" >&2
209
+ return 0
210
+ fi
211
+ rm -f "$normalized" "$candidate_normalized"
126
212
  fi
127
213
  fi
128
214
  stamp_generated "$tmp" "$output"
@@ -136,10 +222,19 @@ while IFS= read -r dir; do
136
222
  [[ "$co" == .* ]] && continue
137
223
  project_dir="${COMPANIES_DIR}/${co}/projects"
138
224
  [[ -d "$project_dir" ]] && write_company_projects_index "$co"
139
- done < <(find "$COMPANIES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
225
+ done < <(if [[ -n "$SCOPE" ]]; then printf 'companies/%s\n' "$SCOPE"; else find "$COMPANIES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort; fi)
226
+
227
+ # A company-scoped run writes only that company's index.
228
+ [[ -z "$SCOPE" ]] || exit 0
140
229
 
141
230
  [[ -d "$PROJECTS_DIR" ]] || { echo "rebuild-projects-index: ${PROJECTS_DIR}/ missing, skipping" >&2; exit 0; }
142
231
 
232
+ if [[ -L "$OUT" ]]; then
233
+ echo "rebuild-projects-index: refused ${OUT} (INDEX.md is a symlink)" >&2
234
+ exit 1
235
+ fi
236
+
237
+ personal_tmp=$(mktemp)
143
238
  {
144
239
  echo "# Projects"
145
240
  echo ""
@@ -186,7 +281,8 @@ done < <(find "$COMPANIES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sor
186
281
  [[ -z "$desc" ]] && desc="—"
187
282
  printf '| `%s/` | %s | %s | %s |\n' "$name" "$stories" "$status" "$desc"
188
283
  done < <(find "$PROJECTS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
189
- } > "$OUT"
284
+ } > "$personal_tmp"
285
+ replace_entry "$personal_tmp" "$OUT"
190
286
 
191
287
  count=$(find "$PROJECTS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')
192
288
  echo "rebuild-projects-index: wrote ${OUT} (${count} HQ project(s))" >&2
@@ -30,6 +30,10 @@ while [ $# -gt 0 ]; do
30
30
  esac
31
31
  done
32
32
 
33
+ # Accept "--base origin/<branch>" as well as "--base <branch>"; the fetch and
34
+ # ref lookups below add the origin/ prefix themselves.
35
+ BASE="${BASE#origin/}"
36
+
33
37
  [ -n "$NAME" ] || die "--name is required (kebab-case slug, e.g. 'fix-login-bug')"
34
38
 
35
39
  # Validate name: kebab-case-ish, no slashes, no spaces