@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.
- package/CHANGELOG.md +289 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +536 -7
- package/dist/command-catalog.generated.js +676 -24
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/agent-inbox.js +2 -2
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +150 -10
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +13 -1
- package/dist/commands/crm.js +83 -6
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files-browse.d.ts +16 -0
- package/dist/commands/files-browse.js +52 -1
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +52 -1
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/integrations-core.d.ts +13 -1
- package/dist/commands/integrations-core.js +49 -4
- package/dist/commands/integrations.js +4 -2
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +37 -0
- package/dist/commands/secrets.js +185 -16
- package/dist/commands/signals.js +17 -5
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
- package/dist/lib/agent-kit/inbox-state.js +5 -1
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
- package/dist/lib/doctor/checks/sync-health.js +66 -1
- package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
- package/dist/lib/doctor/company-resolution-state.js +89 -0
- package/dist/lib/doctor/fix/apply.d.ts +36 -6
- package/dist/lib/doctor/fix/apply.js +128 -12
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hook-engine/resolve.js +6 -2
- package/dist/lib/index-render/company-knowledge.d.ts +1 -1
- package/dist/lib/index-render/company-knowledge.js +4 -5
- package/dist/lib/index-render/index.js +44 -3
- package/dist/lib/index-render/projects.d.ts +1 -1
- package/dist/lib/index-render/projects.js +5 -5
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/sentry-before-send.js +1 -0
- package/dist/utils/access-denied-hint.js +6 -0
- package/dist/utils/client-health.d.ts +34 -0
- package/dist/utils/client-health.js +96 -3
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
} > "$
|
|
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
|