@indigoai-us/hq-cli 5.345.155 → 5.345.159

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 (169) hide show
  1. package/CHANGELOG.md +289 -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 +536 -7
  6. package/dist/command-catalog.generated.js +676 -24
  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/agent-inbox.js +2 -2
  12. package/dist/commands/agent-kit.js +8 -6
  13. package/dist/commands/bot.js +7 -4
  14. package/dist/commands/channel-suggest.d.ts +56 -0
  15. package/dist/commands/channel-suggest.js +170 -0
  16. package/dist/commands/channels.js +7 -1
  17. package/dist/commands/cloud.d.ts +45 -0
  18. package/dist/commands/cloud.js +150 -10
  19. package/dist/commands/company.js +4 -3
  20. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  21. package/dist/commands/core-checkpoint-native.js +67 -2
  22. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  23. package/dist/commands/core-pipeline-driver-native.js +131 -5
  24. package/dist/commands/core.js +13 -1
  25. package/dist/commands/crm.js +83 -6
  26. package/dist/commands/deploy-access.d.ts +33 -0
  27. package/dist/commands/deploy-access.js +322 -0
  28. package/dist/commands/dm-attach.d.ts +106 -0
  29. package/dist/commands/dm-attach.js +184 -0
  30. package/dist/commands/dm.d.ts +37 -0
  31. package/dist/commands/dm.js +172 -10
  32. package/dist/commands/feedback-show.d.ts +40 -0
  33. package/dist/commands/feedback-show.js +261 -0
  34. package/dist/commands/feedback.d.ts +21 -0
  35. package/dist/commands/feedback.js +152 -2
  36. package/dist/commands/files-browse.d.ts +16 -0
  37. package/dist/commands/files-browse.js +52 -1
  38. package/dist/commands/files.d.ts +24 -0
  39. package/dist/commands/files.js +52 -1
  40. package/dist/commands/integrations-connect.js +7 -3
  41. package/dist/commands/integrations-core.d.ts +13 -1
  42. package/dist/commands/integrations-core.js +49 -4
  43. package/dist/commands/integrations.js +4 -2
  44. package/dist/commands/lanes-accounts.js +16 -1
  45. package/dist/commands/lanes-loop.js +14 -4
  46. package/dist/commands/lanes-message.js +6 -4
  47. package/dist/commands/lanes-resume.js +36 -13
  48. package/dist/commands/lanes-shared.d.ts +5 -0
  49. package/dist/commands/lanes-shared.js +13 -3
  50. package/dist/commands/lanes-watch.js +1 -0
  51. package/dist/commands/lanes.d.ts +20 -1
  52. package/dist/commands/lanes.js +201 -51
  53. package/dist/commands/mesh.js +149 -1
  54. package/dist/commands/monitor.d.ts +1 -0
  55. package/dist/commands/monitor.js +4 -1
  56. package/dist/commands/repo.d.ts +41 -0
  57. package/dist/commands/repo.js +331 -0
  58. package/dist/commands/scan.d.ts +3 -1
  59. package/dist/commands/scan.js +7 -3
  60. package/dist/commands/secrets.d.ts +37 -0
  61. package/dist/commands/secrets.js +185 -16
  62. package/dist/commands/signals.js +17 -5
  63. package/dist/commands/staff/listings.d.ts +41 -0
  64. package/dist/commands/staff/listings.js +160 -0
  65. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  66. package/dist/commands/staff/reset-test-account.js +4 -3
  67. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
  68. package/dist/hq-cloud-sync-runner/package.json +1 -1
  69. package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
  70. package/dist/lib/agent-kit/inbox-state.js +5 -1
  71. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  72. package/dist/lib/agent-kit/run/inbox.js +2 -1
  73. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  74. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  75. package/dist/lib/agent-kit/wake.d.ts +4 -0
  76. package/dist/lib/agent-kit/wake.js +49 -3
  77. package/dist/lib/bot/inflight.d.ts +7 -1
  78. package/dist/lib/bot/inflight.js +10 -2
  79. package/dist/lib/bot/prompt.js +3 -0
  80. package/dist/lib/bot/run.js +72 -20
  81. package/dist/lib/bot/session.d.ts +14 -0
  82. package/dist/lib/bot/session.js +70 -0
  83. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  84. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  85. package/dist/lib/capacity/lane-login.d.ts +6 -0
  86. package/dist/lib/capacity/lane-login.js +5 -0
  87. package/dist/lib/core-utils/worktree.js +2 -0
  88. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  89. package/dist/lib/daemon/exit-cause.js +88 -0
  90. package/dist/lib/daemon/handoff.d.ts +2 -0
  91. package/dist/lib/daemon/handoff.js +6 -1
  92. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  93. package/dist/lib/daemon/paths.d.ts +2 -0
  94. package/dist/lib/daemon/paths.js +1 -0
  95. package/dist/lib/daemon/run.js +13 -0
  96. package/dist/lib/daemon/supervisor.d.ts +54 -1
  97. package/dist/lib/daemon/supervisor.js +297 -21
  98. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  99. package/dist/lib/daemon/unit-holds.js +86 -0
  100. package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
  101. package/dist/lib/doctor/checks/sync-health.js +66 -1
  102. package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
  103. package/dist/lib/doctor/company-resolution-state.js +89 -0
  104. package/dist/lib/doctor/fix/apply.d.ts +36 -6
  105. package/dist/lib/doctor/fix/apply.js +128 -12
  106. package/dist/lib/doctor/types.d.ts +1 -1
  107. package/dist/lib/doctor/types.js +1 -0
  108. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  109. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  110. package/dist/lib/hook-engine/resolve.js +6 -2
  111. package/dist/lib/index-render/company-knowledge.d.ts +1 -1
  112. package/dist/lib/index-render/company-knowledge.js +4 -5
  113. package/dist/lib/index-render/index.js +44 -3
  114. package/dist/lib/index-render/projects.d.ts +1 -1
  115. package/dist/lib/index-render/projects.js +5 -5
  116. package/dist/lib/lanes/account-company.d.ts +28 -0
  117. package/dist/lib/lanes/account-company.js +53 -0
  118. package/dist/lib/lanes/accounts.d.ts +22 -3
  119. package/dist/lib/lanes/accounts.js +38 -2
  120. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  121. package/dist/lib/lanes/claude-balance.js +6 -1
  122. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  123. package/dist/lib/lanes/claude-credential.js +163 -0
  124. package/dist/lib/lanes/compaction.d.ts +2 -0
  125. package/dist/lib/lanes/compaction.js +9 -0
  126. package/dist/lib/lanes/config.js +6 -0
  127. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  128. package/dist/lib/lanes/hooks-install.js +2 -1
  129. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  130. package/dist/lib/lanes/host-capacity.js +1 -0
  131. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  132. package/dist/lib/lanes/keep-alive.js +3 -1
  133. package/dist/lib/lanes/links.js +10 -4
  134. package/dist/lib/lanes/loop.js +16 -1
  135. package/dist/lib/lanes/node-binary.d.ts +17 -0
  136. package/dist/lib/lanes/node-binary.js +30 -0
  137. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  138. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  139. package/dist/lib/lanes/reconcile.js +4 -0
  140. package/dist/lib/lanes/remote-control-lane.js +2 -0
  141. package/dist/lib/lanes/spawn.d.ts +3 -0
  142. package/dist/lib/lanes/spawn.js +119 -9
  143. package/dist/lib/lanes/store.d.ts +6 -0
  144. package/dist/lib/lanes/store.js +107 -18
  145. package/dist/lib/lanes/types.d.ts +2 -0
  146. package/dist/lib/lanes/watch.js +28 -5
  147. package/dist/lib/lanes/watcher-main.js +4 -3
  148. package/dist/lib/lanes/watcher.d.ts +4 -0
  149. package/dist/lib/lanes/watcher.js +110 -21
  150. package/dist/lib/mesh/api.d.ts +32 -0
  151. package/dist/lib/mesh/api.js +75 -0
  152. package/dist/lib/secret-scan/detect.js +9 -0
  153. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  154. package/dist/lib/secret-scan/repo-config.js +237 -0
  155. package/dist/lib/secret-scan/rules.d.ts +7 -1
  156. package/dist/lib/secret-scan/rules.js +3 -1
  157. package/dist/lib/secret-scan/staged.d.ts +5 -0
  158. package/dist/lib/secret-scan/staged.js +19 -2
  159. package/dist/lib/secret-scan/types.d.ts +2 -0
  160. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  161. package/dist/lib/sync/server-owned-local-only.js +75 -0
  162. package/dist/sentry-before-send.js +1 -0
  163. package/dist/utils/access-denied-hint.js +6 -0
  164. package/dist/utils/client-health.d.ts +34 -0
  165. package/dist/utils/client-health.js +96 -3
  166. package/dist/utils/package-use-lease.js +27 -9
  167. package/dist/utils/version-gate.d.ts +7 -0
  168. package/dist/utils/version-gate.js +66 -0
  169. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,295 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.159] — 2026-10-22
6
+ - `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.
7
+ - `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.
8
+ - `hq sync status` now accepts `--company <slug>` and shows only that
9
+ company's sync journal. A company with no journal gets a message that names
10
+ it, and a blank value is rejected. Before this, the option was refused as
11
+ unknown. Feedback 7dee0a07-55a6-43f1-9b76-b4293bdd371b.
12
+
13
+ - The bundled sync engine now uses hq-cloud 6.18.92. HQ Sync never uploads
14
+ or downloads company secret folders, even when an HQ root has an old or
15
+ missing `.hqignore`. The default ignore list now covers
16
+ `companies/<co>/secrets/`, `companies/<co>/settings/`, and any `.secrets`
17
+ folder or file at any depth. A folder named `secrets` inside company
18
+ knowledge still syncs. Files already on disk stay where they are.
19
+ - The bundled sync engine now uses hq-cloud 6.18.95 (hq-cloud #1019 and
20
+ #1021). HQ Sync excludes credentials files from sync, and the credentials
21
+ match is now case-insensitive.
22
+ - The bundled sync engine now uses hq-cloud 6.18.97, the current npm `latest`.
23
+ Sync now keeps local settings files intact when it repairs a sync record
24
+ (hq-cloud #1035). It also carries the 6.18.96 fixes: pull no longer deletes
25
+ or overwrites local files right after a sync record is recovered,
26
+ `hq sync --watch` waits to pull until a failed push is retried, sync stops
27
+ retrying when a sandbox blocks the network, the personal-vault sync record is
28
+ tied to the signed-in person, and pulls report every file they delete or skip.
29
+ - `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).
30
+ - Stop `hq daemon` from restarting a failing service forever. A service that
31
+ fails for a cause a restart cannot fix (a missing or non-executable binary,
32
+ exit code 77/78/126/127, or an npm error code such as ETARGET or E404) is
33
+ held at once. Any other failure keeps the usual backoff until the sixth
34
+ failure without 30 minutes of healthy running, then the service is held.
35
+ A held service shows as `held` with its reason in `hq daemon status`, stays
36
+ held across daemon restarts (recorded in `~/.hq/daemon/holds.json`), and
37
+ starts again on `hq daemon restart <unit>`, a command change, or a CLI
38
+ update.
39
+
40
+ 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.
41
+
42
+ - Exhausted transient sync/mesh holds now wait the existing 30-minute healthy
43
+ episode window, then claim one supervised retry durably before starting it.
44
+ Failure re-holds for another window; only a continuously running probe earns
45
+ the healthy reset. Daemon restarts keep the deadline and claim. Missing or
46
+ invalid legacy deadlines get one full window instead of an immediate burst.
47
+ A durable-claim write failure keeps the service held. Permanent holds and
48
+ other services remain manual. An explicit stop of a recovering sync/mesh unit
49
+ persists across daemon restarts and command/version changes until an explicit
50
+ start/restart; a paused plan never launches a probe while blocked. This does
51
+ not change stop semantics for healthy units. The held reason includes the
52
+ next supervised retry time.
53
+ - `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.
54
+ - `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`.
55
+ - 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.
56
+ - `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.
57
+ - `hq lanes resume` and `hq lanes message` now print a line to stderr before
58
+ the resume-time `/compact` starts, saying it can take up to 10 minutes and
59
+ that `--no-compact` skips it. Before this the command printed nothing while
60
+ compaction ran and looked hung.
61
+ - `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`.
62
+ - `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)
63
+ - `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.
64
+ - `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.
65
+ - New `hq mesh project archive|complete|activate <projectId> --company <slug>`
66
+ moves a Board between active, completed and archived through the server's
67
+ lifecycle route, and `hq mesh project lifecycle [projectId] --company <slug>`
68
+ lists or shows that state. Use `archive` to put away a duplicate Board (for
69
+ example one registered under a board id next to the live one registered
70
+ under the folder slug). Nothing is deleted: stories, the project folder,
71
+ channel, memberships and history stay; the project only leaves the active
72
+ Board list and the organize candidates, and `activate` brings it back. When
73
+ the Board still has stories, `archive` stops and asks for
74
+ `--confirm-nonempty`. A project already in the requested state is reported
75
+ and left alone. Feedback 5d596dd0-6748-4aba-b437-f57ee914e311.
76
+ - `hq dm send` gains `--attach <path>` (repeatable, up to 5 files, 25 MB
77
+ each). Before this, sending an image to a teammate or agent meant copying it
78
+ into the company vault, running a sync push, and pasting the path into the DM
79
+ text. Now the CLI presign-uploads each file into the conversation's own vault
80
+ scope (`chat/attachments/dm/<uidA--uidB>/…` in the sender's personal vault
81
+ for a 1:1 DM, `chat/attachments/chan/<channelId>/…` in the channel's company
82
+ vault for a named channel, including `--thread` replies), sends it as
83
+ `attachments[]` on the message, and prints each file's vault path. The
84
+ message body may be empty when a file is attached. Missing, oversized, or
85
+ too many files are refused by name before anything is uploaded; group DMs,
86
+ scheduled sends (`--at`/`--in`), and agent senders on 1:1 DMs are refused
87
+ with a plain message.
88
+ - `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.
89
+ - `hq signals list` and `hq signals get` now accept `--company <slug>`, the
90
+ same flag `hq meetings` and `hq integrations` take. It means the same thing
91
+ as `--entity`. Before this, `hq signals list --company <slug>` failed with
92
+ `unknown option '--company'`. Passing `--entity` and `--company` with
93
+ different values is an error.
94
+ - `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)
95
+ - 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.
96
+ - New `hq repo add <path> --company <slug>` registers a git checkout with HQ.
97
+ It links the checkout into `repos/private/<name>` (or `repos/public/` with
98
+ `--public`) and adds it to `companies.<slug>.repos` in
99
+ `companies/manifest.yaml`. Before this, you had to make the symlink and edit
100
+ the manifest by hand. A checkout that already sits directly under `repos/` is
101
+ registered where it is. Running it again changes nothing. `--dry-run` shows
102
+ the plan without changing anything. Comments and layout in the rest of the
103
+ manifest stay as they were.
104
+ - `hq lanes stop` (and every other lane write) no longer fails with
105
+ `store_lock_timeout` because of a lane record lock left behind by a process
106
+ that died. A lock is now reclaimed when the process that wrote it no longer
107
+ exists, or, when the lock file is empty or unreadable (a process killed
108
+ between creating and writing it), once the file is older than 30 seconds.
109
+ The lock now records its hostname, and the dead-process rule applies only
110
+ to locks written on this machine; a lock from another host or with no host
111
+ falls back to the 30-second age rule.
112
+ Before this, an empty or partial lock file was never reclaimed. A lock held
113
+ by a live process is still respected.
114
+ Reclaiming a stale lock is now atomic, so two waiters cannot both take it,
115
+ and a holder releases the lock only if it is still its own. A lock whose
116
+ timestamp is in the future ages out by file time. The 30-second window can
117
+ be changed with `HQ_LANES_LOCK_STALE_MS`.
118
+ - New `hq feedback show <id>` for HQ operators. It prints one feedback row
119
+ (title, status, company, reporter, body) and lists its screenshots, email
120
+ attachments and log bundle. `--download <dir>` saves the attachments to that
121
+ directory, and `--logs` adds the log bundle. `--json` prints the row with
122
+ download links removed. The server only allows operators, so anyone else
123
+ gets an "operators only" error, and the command never prints a download
124
+ link or token. Needs hq-pro#5143 for screenshot links. Feedback
125
+ b1d14d0a-caff-4bae-bee4-977fc6b1d6e6.
126
+ - New `hq deploy access` command manages who can open a deploy app.
127
+ `hq deploy access get <app>` shows the current mode and selected members.
128
+ `hq deploy access <app> --mode company|selected|public` changes the mode, and
129
+ `--add-user`, `--remove-user`, `--add-group`, `--remove-group` edit the
130
+ selected-members list. `<app>` is an app id, subdomain, or exact name. It
131
+ calls hq-deploy's existing `/api/apps/:id/access-policy` API with your HQ
132
+ sign-in, so only the app owner or a company admin can change access. Any
133
+ other mode is refused before a request is sent.
134
+ For an app with no saved policy, the company id comes from the app record
135
+ or from `--company` (or the active company), never the hq-deploy org id; if
136
+ it cannot be found the command stops with an error and changes nothing.
137
+ - `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.
138
+ - `hq dm <name> "..."` now says which channel the name resolved to when it
139
+ differs from the channel's display name. Before this, `hq dm setup "hi"`
140
+ printed only `Message posted to #welcome.`, with nothing linking `setup` to
141
+ `#welcome`. The line now reads
142
+ `Message posted to #welcome (resolved 'setup' to personal channel chn_...).`
143
+ - `hq lanes watch` keeps monitoring loop lanes between phase envelopes, even
144
+ when the last phase briefly records `done`. It continues account checks and
145
+ waits for the live loop controller to stop, fail, or exit before treating that phase as
146
+ terminal. Ordinary lanes and failed launches without loop metadata still exit.
147
+ - `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.
148
+ - 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.
149
+ - 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.
150
+ - 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.
151
+ - 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.
152
+ - 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).
153
+ - The pipeline driver no longer exits 22 (deadline) or 28 (stall) for a phase
154
+ that is still waiting in its loop lane's queue, for example while the lane is
155
+ queued behind the host throttle or busy on another envelope. Once the lane
156
+ picks the phase up it gets its full deadline budget. This ports
157
+ hq-core-staging #1357 to the native driver and also covers the stall check.
158
+ Queue time is bounded by `PC_QUEUE_MAX_MIN` (default 240): a phase still
159
+ waiting after that long ends the run with exit 22 and a QUEUED_TOO_LONG
160
+ message naming the lane. An admitted lane with an empty queue that is not
161
+ working on the phase no longer counts as holding it, so the deadline applies.
162
+ While the lane is busy on an envelope from another run, the phase waits on
163
+ that envelope's own watchdog, up to the queue ceiling.
164
+ - Loop lane workers, watchers and the drain hook registration that outlive a
165
+ Node upgrade now use a live Node binary when the one they were launched with
166
+ has been removed: the Homebrew prefix `bin/node` link for a Cellar path,
167
+ else `node` on PATH. This covers watcher queued-message recovery, terminal
168
+ keep-alive scheduling and senior message delivery.
169
+ - `hq sync push`, `hq sync push --all` and `hq sync now` no longer report
170
+ success when files failed to upload. When hq-cloud reports failed uploads
171
+ (for example a refused presign), the CLI prints how many files were not
172
+ uploaded and lists them, adds `filesUploadFailed` and `uploadFailedPaths`
173
+ to the `--json` complete event, and exits 1. A path you type on
174
+ `hq sync push` that an ignore rule drops (a `.hq-*` file or a generated
175
+ INDEX.md) now fails with the rule that dropped it instead of printing a
176
+ 0-file success. Both need the hq-cloud release that reports these cases.
177
+ Feedback 943456f4-56bf-43bc-94fa-bdbc7384ec41,
178
+ 63bfc302-ae00-4d32-9d94-0a5eaf8e1314,
179
+ 8c294805-8761-44c4-abf2-5ab14c263d84.
180
+ - `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.
181
+ - `hq core worktree --base origin/<branch>` (and the scaffold `worktree.sh`)
182
+ now accepts the `origin/` prefix instead of building `origin/origin/<branch>`.
183
+ - The `hq monitor` expiry message now names `--timeout <duration>` and
184
+ `--persistent` for re-arming a longer watch. The 5-minute default is unchanged.
185
+ - The CLI no longer waits about 4 minutes on a leftover `update.pending.json`
186
+ after a reboot. A request written before the current boot, or whose pid is
187
+ gone or belongs to another user (EPERM), is treated as stale and removed.
188
+ - 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.
189
+ - `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.
190
+ - 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.
191
+ - `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.
192
+ - `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.
193
+ - 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".
194
+ - `hq lanes set --model/--effort` now works for a lane that runs on an Outpost.
195
+ The change is forwarded to the Outpost that owns the lane, which re-checks
196
+ that the caller is the lane or its senior before applying it. Before this, the
197
+ command refused any Outpost lane. An Outpost that has not been upgraded yet
198
+ returns `outpost_cli_upgrade_required` instead of an unknown-command error.
199
+ - The phase worker brief now tells the worker to copy each acceptance criterion
200
+ verbatim into its handoff `ac_evidence[].criterion`, keep its 0-based index,
201
+ and not shorten or paraphrase the text, so the criteria recheck matches them
202
+ (feedback fa21c737).
203
+ - The native pipeline driver no longer exits 23 when a worker writes a handoff
204
+ that fails validation. It moves the worker's file to
205
+ `handoffs/<story>-<phase>.invalid.<n>.json`, records the phase as failed with
206
+ the validation error, and keeps driving; the fail cap still bounds retries.
207
+ The loop lane runner no longer overwrites a handoff the worker already wrote
208
+ to `result_path`; its own failed handoff goes beside it as
209
+ `<name>.lane-failure.json`. The phase brief now says that `summary`,
210
+ `context_for_next` and each `ac_evidence` `evidence` are single strings.
211
+ - 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).
212
+ - `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.
213
+ - `hq scan secrets --staged` now reads `[[allowlists]]` (and the legacy
214
+ `[allowlist]`) from the repo's own `.gitleaks.toml`, with gitleaks semantics
215
+ for `paths`, `regexes`, `regexTarget`, `stopwords`, `commits`, `targetRules`
216
+ and `condition = "AND" | "OR"`, on top of the built-in rules, for that repo
217
+ only. The file is read from git, not the working tree: it applies only once
218
+ committed, so a commit that changes it is scanned with the previous version.
219
+ Every allowlist needs `paths` that start with `^` and a literal name
220
+ (such as `^fixtures/`); `regexes` and `stopwords` need `condition = "AND"`.
221
+ A file that breaks these rules, cannot be parsed, is a symlink, or has an
222
+ unknown key or an unsafe regex stops the scan with exit 2 and names the
223
+ problem.
224
+ - 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.
225
+ - `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.
226
+ - 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.
227
+
228
+ ## [5.345.156] — 2026-10-21
229
+ - Automatic Stop-gate checkpoints now start at most one background checkpoint
230
+ agent per session every 30 minutes. Each of those runs costs a fresh agent
231
+ session's worth of tokens, and most of them found nothing new. A skipped run
232
+ still writes the checkpoint thread, so the Stop gate behaves the same.
233
+ `/checkpoint` and runs without a session id are unchanged, and
234
+ `HQ_CHECKPOINT_AGENT=0` still turns the agent off. Feedback
235
+ 90c33997-5a41-4a23-a23e-f7172bfead88.
236
+
237
+ - `hq doctor --fix` stops pulling a company the sync service could not find
238
+ for your HQ account. When a pull fails because the company does not exist
239
+ for you or access is refused, the next check shows that company's stale
240
+ sync journal as "not on this machine" with one next step: check you are
241
+ signed in to the right account and ask to be re-invited, or delete the old
242
+ journal. It is no longer auto-fixable, so the client-health hook stops
243
+ reporting it. This classification requires another company pull to succeed
244
+ in the same run; an outage affecting every company remains visible. Any
245
+ later successful sync invalidates the cached failure, and `--fix` retries
246
+ unresolved companies after 14 days.
247
+ - The same company's stale sync manifest now follows its journal: it shows as
248
+ "not on this machine" and `--fix` stops re-uploading it. A manifest upload
249
+ that fails with "No membership found" counts as the same evidence, under the
250
+ same rule that another company must succeed in that run. A later successful
251
+ sync or manifest upload clears it.
252
+ - One `hq doctor --fix` run now refreshes up to 10 stale sync journals
253
+ instead of 2. Unattended runs (`--yes`) stop starting new pulls after 120
254
+ seconds so they still finish inside the health hook's time limit. When a
255
+ run stops early it says how many are left and asks you to run it again.
256
+ - `hq files cat` / `get` and the agent file tools now say why a signed vault download failed after access was granted: a response that did not come from the storage host is reported as a likely proxy or egress block naming the host to allow, and a storage refusal shows S3's error code and request id. These failures no longer send you to `hq access`, which only re-checked the grant that had already passed.
257
+ - `hq agent inbox --json` and the agent `hq_inbox_read` tool now include `source`: `server` when the server inbox was read, `local-mirror` when the server could not be reached and the list may be stale, so automation can tell a stale fallback from a real empty inbox.
258
+ - Fix company knowledge and project index rebuilds to honor help and explicit
259
+ company arguments. Invalid arguments fail before writing, and a
260
+ company-scoped project rebuild leaves the personal index untouched.
261
+ - `hq integrations call` has a new `--timeout <seconds>` option (default 270,
262
+ max 1800) that sets how long it waits for a call the gateway runs as a
263
+ background job. When a call that asked for background execution instead
264
+ comes back as an inline HTTP 500, 502 or 504, the error now says the call
265
+ likely hit the gateway's 30-second inline limit and that async integration
266
+ jobs must be enabled for the company, instead of only suggesting a retry.
267
+ Feedback 4b2573d6-fab7-4b63-8fa6-f50b472a887f.
268
+ - Every `hq` command (including `hq secrets exec`) sends a client-health
269
+ heartbeat, and about once every 15 minutes that heartbeat re-reads the local
270
+ sync journals. That read loads the whole local sync state store into memory,
271
+ and on a large store it took the `hq` process itself to several GB. Commands
272
+ that are not sync commands now do this read in a separate child process
273
+ capped at a 512 MB heap and 20 seconds. If the child hits a cap, the command
274
+ keeps the last known sync time and does not retry the read for 15 minutes.
275
+ Sync commands still read the journals in-process.
276
+ Feedback d70e9d63-7b46-42bc-8c28-a545235cd7c6.
277
+ - `hq secrets script approve --remote-path` now accepts a path relative to the
278
+ HQ root (for example `companies/<co>/skills/<skill>/<file>.mjs`). Once
279
+ hq-pro has the matching change deployed, that one approval matches the
280
+ script on every HQ root, including a hosted agent's. For a relative path a
281
+ leading `./` is dropped and `\` becomes `/`. Approve refuses, with a plain
282
+ error and no API call, a relative path that could never match: a bare file
283
+ name with no folder, a `.`, `..` or empty segment, a trailing `/`, a leading
284
+ `~`, a drive letter without a separator, or surrounding whitespace. An
285
+ absolute path is sent exactly as typed and must match exactly. If the
286
+ server answers without an active approval for the id (for example the id was
287
+ revoked earlier), approve now fails with a hint to use a new `--id` instead
288
+ of printing success. The success line names the stored path.
289
+ Feedback 6e0a1040-4d8e-4743-863e-386eb98aa0a4.
290
+ - Installing the native hook engine no longer fails with `ERR_FS_CP_EEXIST` on
291
+ Node 24.22 and later. The engine is now copied into a new directory inside the
292
+ staging folder instead of onto the staging folder itself.
293
+
5
294
  ## [5.345.155] — 2026-10-21
6
295
  - `hq secrets exec --personal` now works in a session locked to one company.
7
296
  It injects your own personal-vault secrets into the command, read-only, and
@@ -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