@indigoai-us/hq-cli 5.345.74 → 5.345.76
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 +29 -0
- package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +11 -4
- package/assets/scaffold/core/scripts/rebuild-companies-index.sh +30 -37
- package/dist/commands/core-delegate-w1b.js +53 -11
- package/dist/commands/core-delegate.d.ts +5 -2
- package/dist/commands/core-delegate.js +43 -8
- package/dist/commands/core-native-family-flag.js +1 -1
- package/dist/commands/install-global.js +4 -2
- package/dist/commands/lanes.d.ts +1 -0
- package/dist/commands/lanes.js +13 -10
- package/dist/fast-scaffold.d.ts +1 -1
- package/dist/fast-scaffold.js +1 -0
- package/dist/lib/bot/config.js +4 -2
- package/dist/lib/bot/runtime/codex.d.ts +22 -0
- package/dist/lib/bot/runtime/codex.js +63 -1
- package/dist/lib/index-render/companies.js +33 -16
- package/dist/lib/index-render/shared.d.ts +18 -0
- package/dist/lib/index-render/shared.js +24 -0
- package/dist/lib/lanes/dropbox.js +1 -0
- package/dist/lib/lanes/monitor.js +30 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.76] — 2026-10-10
|
|
6
|
+
|
|
7
|
+
- `hq bot` on macOS now finds the Codex CLI bundled in the ChatGPT or Codex
|
|
8
|
+
desktop app when `codex` is not on PATH, so a Mac with only the desktop app
|
|
9
|
+
can run a Codex assistant. PATH still wins.
|
|
10
|
+
|
|
11
|
+
- `hq bot create --display-name` now accepts numbers after the first letter,
|
|
12
|
+
so names like "R2D2" or "StefanTest123" work.
|
|
13
|
+
- Core delegate native commands now use the default-on kill-switch behavior when the flag snapshot or company context is unavailable. An explicit `core-native-delegate=false` override still selects the bundled shell.
|
|
14
|
+
- `hq core rebuild-index companies` now renders the same company INDEX.md on every machine and every day, and skips the write
|
|
15
|
+
when the content is unchanged (logged as `unchanged`). The header line is now `> Auto-generated.` with no date. The `repos/`
|
|
16
|
+
and `data/` rows show fixed labels instead of local counts, `knowledge/` always shows a subdirectory count, symlinked
|
|
17
|
+
children are no longer counted, and project status comes from prd.json only, not local orchestrator state.
|
|
18
|
+
- `hq lanes monitor-check` now recognizes a live `hq lanes watch` Monitor process when the hq
|
|
19
|
+
CLI is installed under a path containing spaces (for example the desktop toolchain under
|
|
20
|
+
`~/Library/Application Support/Indigo HQ/`). Previously those lanes stayed "uncovered" and the
|
|
21
|
+
Stop hook kept blocking the session.
|
|
22
|
+
## [5.345.75] — 2026-10-10
|
|
23
|
+
|
|
24
|
+
- Native delegate manifest updates now use mode 0600 on POSIX, matching the
|
|
25
|
+
shell commands and preventing the manifest from becoming readable by other
|
|
26
|
+
local users.
|
|
27
|
+
- Lane lifecycle delivery, worker continuity, status-row fields, and operator-requested orphan stopping now stay enabled when their hq-flags registry rows are missing or unavailable.
|
|
28
|
+
- `hq uninstall --global --runtime codex` is now idempotent: on a machine with
|
|
29
|
+
no global Codex installation it reports that there is nothing to remove and
|
|
30
|
+
exits successfully.
|
|
31
|
+
- Delegate send now stops before DM delivery and reports the shell-compatible
|
|
32
|
+
error when saving the `sending` state fails.
|
|
33
|
+
|
|
5
34
|
## [5.345.74] — 2026-10-10
|
|
6
35
|
|
|
7
36
|
- Lane envelope repair prompts now identify parser locations and schema fields,
|
|
@@ -59,9 +59,13 @@ case "$STATUS" in
|
|
|
59
59
|
esac
|
|
60
60
|
|
|
61
61
|
record_pickup() { # <kind> <detail>
|
|
62
|
-
|
|
62
|
+
local TMP_MANIFEST
|
|
63
|
+
TMP_MANIFEST="$(mktemp "${MANIFEST}.tmp.XXXXXX")" || exit 1
|
|
64
|
+
if ! jq --arg now "$NOW" --arg k "$1" --arg d "$2" \
|
|
63
65
|
'.status = "picked-up" | .pickedUpAt = $now | .pickup = {kind: $k, detail: $d, recordedAt: $now}' \
|
|
64
|
-
"$MANIFEST" > "$
|
|
66
|
+
"$MANIFEST" > "$TMP_MANIFEST" || ! mv "$TMP_MANIFEST" "$MANIFEST"; then
|
|
67
|
+
rm -f "$TMP_MANIFEST"
|
|
68
|
+
fi
|
|
65
69
|
echo "hq-delegate-pickup: $NAME picked up the delegation ($1) — status advanced to 'picked-up'"
|
|
66
70
|
exit 0
|
|
67
71
|
}
|
|
@@ -93,7 +97,10 @@ if [ "$age_h" -lt "$WINDOW" ]; then
|
|
|
93
97
|
exit 6
|
|
94
98
|
fi
|
|
95
99
|
reason="no pickup evidence from $NAME within ${WINDOW}h of send (${SENT_AT:-unknown}): no acknowledgement, no commit by $TO on ${BRANCH:-the branch}"
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
TMP_MANIFEST="$(mktemp "${MANIFEST}.tmp.XXXXXX")" || exit 1
|
|
101
|
+
if ! jq --arg now "$NOW" --arg r "$reason" '.status = "failed" | .failedAt = $now | .failure = {reason: $r, recordedAt: $now}' \
|
|
102
|
+
"$MANIFEST" > "$TMP_MANIFEST" || ! mv "$TMP_MANIFEST" "$MANIFEST"; then
|
|
103
|
+
rm -f "$TMP_MANIFEST"
|
|
104
|
+
fi
|
|
98
105
|
echo "hq-delegate-pickup: FAILED — $reason. Re-send with a direct ask, or hand it to someone else." >&2
|
|
99
106
|
exit 5
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
# rebuild-companies-index.sh — regenerate companies/{co}/INDEX.md for each company.
|
|
3
3
|
#
|
|
4
4
|
# Pure bash + jq. Zero Claude context. Iterates companies/*/ (skips _template,
|
|
5
|
-
# dotfiles), summarizes top-level dirs, scans projects/*/prd.json
|
|
6
|
-
#
|
|
5
|
+
# dotfiles), summarizes top-level dirs, scans projects/*/prd.json to populate
|
|
6
|
+
# the Projects table.
|
|
7
|
+
#
|
|
8
|
+
# Output is deterministic: the same synced tree renders the same bytes on every
|
|
9
|
+
# machine and every day. There is no date line, nothing reads local-only state
|
|
10
|
+
# (repos/, data/, an embedded knowledge .git, workspace/orchestrator), and an
|
|
11
|
+
# unchanged file is not rewritten.
|
|
7
12
|
#
|
|
8
13
|
# Schema per knowledge/public/hq-core/index-md-spec.md (Company Root variant):
|
|
9
|
-
# - "# {Company Title}" + "> Auto-generated.
|
|
14
|
+
# - "# {Company Title}" + "> Auto-generated."
|
|
10
15
|
# - Top-level Name | Description table (dirs and 1-line desc)
|
|
11
16
|
# - "## Projects" table (Project | Status | Description)
|
|
12
17
|
# - "## Deployments" placeholder (None for now — manifest-driven enrichment deferred)
|
|
@@ -28,8 +33,6 @@ done
|
|
|
28
33
|
HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
29
34
|
cd "$HQ_ROOT"
|
|
30
35
|
|
|
31
|
-
DATE=$(date -u +%Y-%m-%d)
|
|
32
|
-
|
|
33
36
|
# Title-case a slug: "example-co" -> "Example Co"
|
|
34
37
|
titleize() {
|
|
35
38
|
echo "$1" | awk -F'-' '{
|
|
@@ -47,18 +50,13 @@ describe_dir() {
|
|
|
47
50
|
[[ -d "$path" ]] || return 0
|
|
48
51
|
case "$d" in
|
|
49
52
|
data)
|
|
50
|
-
|
|
51
|
-
n=$(find "$path" -mindepth 1 -maxdepth 1 ! -name '.*' 2>/dev/null | wc -l | tr -d ' ')
|
|
52
|
-
echo "${n} item(s)"
|
|
53
|
+
echo "Company data"
|
|
53
54
|
;;
|
|
54
55
|
knowledge)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
n=$(find "$path" -mindepth 1 -maxdepth 1 -type d ! -name '.*' 2>/dev/null | wc -l | tr -d ' ')
|
|
60
|
-
echo "Inline knowledge (${n} subdirs)"
|
|
61
|
-
fi
|
|
56
|
+
local n
|
|
57
|
+
# Trailing slash: a symlinked knowledge/ is listed through its target.
|
|
58
|
+
n=$(find "${path}/" -mindepth 1 -maxdepth 1 -type d ! -name '.*' 2>/dev/null | wc -l | tr -d ' ')
|
|
59
|
+
echo "Company knowledge (${n} subdirs)"
|
|
62
60
|
;;
|
|
63
61
|
settings)
|
|
64
62
|
local n
|
|
@@ -81,9 +79,7 @@ describe_dir() {
|
|
|
81
79
|
echo "${n} worker(s)"
|
|
82
80
|
;;
|
|
83
81
|
repos)
|
|
84
|
-
|
|
85
|
-
n=$(find "$path" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) ! -name '.*' 2>/dev/null | wc -l | tr -d ' ')
|
|
86
|
-
echo "${n} repo link(s)"
|
|
82
|
+
echo "Local repo links (not synced)"
|
|
87
83
|
;;
|
|
88
84
|
registry)
|
|
89
85
|
local n
|
|
@@ -101,22 +97,10 @@ describe_dir() {
|
|
|
101
97
|
esac
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
# Resolve project status
|
|
100
|
+
# Resolve project status from prd.json alone (story-pass check). The local
|
|
101
|
+
# workspace/orchestrator state is not synced, so it is not consulted.
|
|
105
102
|
project_status() {
|
|
106
|
-
local
|
|
107
|
-
local prd="$2"
|
|
108
|
-
local state="workspace/orchestrator/${project}/state.json"
|
|
109
|
-
if [[ -f "$state" ]]; then
|
|
110
|
-
local raw
|
|
111
|
-
raw=$(jq -r '.status // "active"' "$state" 2>/dev/null || echo "active")
|
|
112
|
-
case "$raw" in
|
|
113
|
-
completed|complete) echo "complete" ;;
|
|
114
|
-
in_progress|active|started) echo "active" ;;
|
|
115
|
-
archived) echo "archived" ;;
|
|
116
|
-
*) echo "$raw" ;;
|
|
117
|
-
esac
|
|
118
|
-
return
|
|
119
|
-
fi
|
|
103
|
+
local prd="$1"
|
|
120
104
|
if [[ -f "$prd" ]]; then
|
|
121
105
|
local total passes
|
|
122
106
|
total=$(jq '[.userStories[]?] | length' "$prd" 2>/dev/null || echo 0)
|
|
@@ -148,6 +132,7 @@ trunc() {
|
|
|
148
132
|
write_company_index() {
|
|
149
133
|
local co="$1"
|
|
150
134
|
local out="companies/${co}/INDEX.md"
|
|
135
|
+
local tmp="companies/${co}/.INDEX.md.$$.tmp"
|
|
151
136
|
local title
|
|
152
137
|
title=$(titleize "$co")
|
|
153
138
|
|
|
@@ -155,7 +140,7 @@ write_company_index() {
|
|
|
155
140
|
{
|
|
156
141
|
echo "# ${title}"
|
|
157
142
|
echo ""
|
|
158
|
-
echo "> Auto-generated.
|
|
143
|
+
echo "> Auto-generated."
|
|
159
144
|
echo ""
|
|
160
145
|
echo "| Name | Description |"
|
|
161
146
|
echo "|------|-------------|"
|
|
@@ -187,7 +172,7 @@ write_company_index() {
|
|
|
187
172
|
desc=$(jq -r '.description // ""' "$prd" 2>/dev/null || echo "")
|
|
188
173
|
fi
|
|
189
174
|
local status
|
|
190
|
-
status=$(project_status "$
|
|
175
|
+
status=$(project_status "$prd")
|
|
191
176
|
desc=$(sanitize_cell "$desc")
|
|
192
177
|
desc=$(trunc "$desc" 110)
|
|
193
178
|
[[ -z "$desc" ]] && desc="—"
|
|
@@ -201,9 +186,17 @@ write_company_index() {
|
|
|
201
186
|
echo "## Deployments"
|
|
202
187
|
echo ""
|
|
203
188
|
echo "_See \`companies/manifest.yaml\` for deployment targets._"
|
|
204
|
-
} > "$
|
|
189
|
+
} > "$tmp"
|
|
205
190
|
|
|
206
|
-
|
|
191
|
+
# Rewrite only when the content changed, so an unchanged index keeps its
|
|
192
|
+
# mtime and is not re-uploaded by HQ Sync.
|
|
193
|
+
if [[ -f "$out" ]] && cmp -s "$tmp" "$out"; then
|
|
194
|
+
rm -f "$tmp"
|
|
195
|
+
echo "rebuild-companies-index: unchanged ${out}" >&2
|
|
196
|
+
else
|
|
197
|
+
mv -f "$tmp" "$out"
|
|
198
|
+
echo "rebuild-companies-index: wrote ${out}" >&2
|
|
199
|
+
fi
|
|
207
200
|
}
|
|
208
201
|
|
|
209
202
|
COUNT=0
|
|
@@ -174,6 +174,23 @@ function parseFlags(args, allowed) {
|
|
|
174
174
|
}
|
|
175
175
|
function usage(name) { return USAGE[name] ?? ""; }
|
|
176
176
|
function usageFailure(name) { err(usage(name)); return 1; }
|
|
177
|
+
function grantHasDanglingManifestFlag(args) {
|
|
178
|
+
for (let i = 0; i < args.length;) {
|
|
179
|
+
const key = args[i] ?? "";
|
|
180
|
+
if (key === "--yes") {
|
|
181
|
+
i += 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (key === "--manifest") {
|
|
185
|
+
if (i + 1 >= args.length)
|
|
186
|
+
return true;
|
|
187
|
+
i += 2;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
177
194
|
function bashPath(value) {
|
|
178
195
|
if (process.platform !== "win32")
|
|
179
196
|
return value;
|
|
@@ -205,6 +222,10 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
205
222
|
const root = context.root ?? context.cwd;
|
|
206
223
|
if (args[0] !== "build")
|
|
207
224
|
return usageFailure("bundle");
|
|
225
|
+
// The shell uses `shift 2` for these options. With no value at end of argv,
|
|
226
|
+
// Bash exits nonzero before it reaches the required-field diagnostics.
|
|
227
|
+
if (["--company", "--project", "--to", "--to-kind", "--to-name", "--mode"].includes(args.at(-1) ?? ""))
|
|
228
|
+
return 1;
|
|
208
229
|
const parsed = parseFlags(args.slice(1), ["--company", "--project", "--to", "--to-kind", "--to-name", "--mode", "--dry-run"]);
|
|
209
230
|
if (parsed.help) {
|
|
210
231
|
out(usage("bundle"));
|
|
@@ -326,7 +347,7 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
326
347
|
const next = stories.filter((story) => !isRecord(story) || story.passes !== true)
|
|
327
348
|
.sort((a, b) => Number(getPath(a, "priority", 0)) - Number(getPath(b, "priority", 0))).slice(0, 3);
|
|
328
349
|
const doneList = done.map((story) => `- ${text(getPath(story, "id"))}: ${text(getPath(story, "title"))}`).join("\n");
|
|
329
|
-
const nextList = next.map((story) => `### ${text(getPath(story, "id"))}: ${text(getPath(story, "title"))}\n\n${text(getPath(story, "description"))}\n`).join("\n");
|
|
350
|
+
const nextList = next.map((story) => `### ${text(getPath(story, "id"))}: ${text(getPath(story, "title"))}\n\n${text(getPath(story, "description"))}\n`).join("\n").replace(/\n+$/, "");
|
|
330
351
|
const questions = list(getPath(prd, "metadata.openQuestions", [])).map((entry) => `- ${text(entry)}`).join("\n");
|
|
331
352
|
const traps = [
|
|
332
353
|
...list(getPath(prd, "metadata.securityNotes", [])),
|
|
@@ -410,6 +431,10 @@ const ACL_FILTER = `
|
|
|
410
431
|
(.direct | type == "array") and (.exists | type == "boolean")
|
|
411
432
|
`;
|
|
412
433
|
function runGrant(args, context) {
|
|
434
|
+
// The shell companion's `shift 2` runs under `set -e`; a final bare
|
|
435
|
+
// --manifest exits 1 without diagnostics. Preserve that observable contract.
|
|
436
|
+
if (grantHasDanglingManifestFlag(args))
|
|
437
|
+
return 1;
|
|
413
438
|
const parsed = parseFlags(args, ["--manifest", "--yes"]);
|
|
414
439
|
if (parsed.help) {
|
|
415
440
|
out(usage("grant"));
|
|
@@ -618,10 +643,14 @@ function runGrant(args, context) {
|
|
|
618
643
|
out(`hq-delegate-grant: all ${prefixes.length} grants verified — manifest status advanced to 'granted'\n`);
|
|
619
644
|
return 0;
|
|
620
645
|
}
|
|
621
|
-
function atomicCopy(source, target) {
|
|
646
|
+
function atomicCopy(source, target, mode) {
|
|
622
647
|
const temporary = `${target}.tmp.${process.pid}.${Math.random().toString(16).slice(2)}`;
|
|
623
648
|
try {
|
|
624
|
-
fs.
|
|
649
|
+
const contents = fs.readFileSync(source);
|
|
650
|
+
if (mode === undefined)
|
|
651
|
+
fs.writeFileSync(temporary, contents, { flag: "wx" });
|
|
652
|
+
else
|
|
653
|
+
fs.writeFileSync(temporary, contents, { flag: "wx", mode });
|
|
625
654
|
moveFile(temporary, target);
|
|
626
655
|
}
|
|
627
656
|
catch (error) {
|
|
@@ -745,9 +774,9 @@ function runPublish(args, context) {
|
|
|
745
774
|
if (shareSession.test(fs.readFileSync(stagedManifest, "utf8")) || shareSession.test(fs.readFileSync(stagedBrief, "utf8")))
|
|
746
775
|
return fail("publish", "share-session capability in dossier; nothing published");
|
|
747
776
|
fs.mkdirSync(dest, { recursive: true });
|
|
748
|
-
atomicCopy(stagedBrief, path.join(dest, "BRIEF.md"));
|
|
749
|
-
atomicCopy(stagedManifest, path.join(dest, "manifest.json"));
|
|
750
|
-
atomicCopy(stagedManifest, manifestPath);
|
|
777
|
+
atomicCopy(stagedBrief, path.join(dest, "BRIEF.md"), 0o600);
|
|
778
|
+
atomicCopy(stagedManifest, path.join(dest, "manifest.json"), 0o600);
|
|
779
|
+
atomicCopy(stagedManifest, manifestPath, 0o600);
|
|
751
780
|
const push = runHq(["sync", "push", `${relative}/`, "--company", company, "--on-conflict", "keep"], context, "inherit");
|
|
752
781
|
if (push.code !== 0)
|
|
753
782
|
return fail("publish", "final dossier push failed; resume publication");
|
|
@@ -775,7 +804,7 @@ function runPublish(args, context) {
|
|
|
775
804
|
const publication = { verifiedAt: currentTime(), manifestPath: `${relative}/delegation/${id}/manifest.json`, files: receipts };
|
|
776
805
|
const updated = { ...canonical, publication, recipientAccess: "unconfirmed" };
|
|
777
806
|
fs.writeFileSync(stagedManifest, jsonText(updated));
|
|
778
|
-
atomicCopy(stagedManifest, manifestPath);
|
|
807
|
+
atomicCopy(stagedManifest, manifestPath, 0o600);
|
|
779
808
|
out("hq-delegate-publish: canonical dossier bytes verified as sender; recipient access unconfirmed\n");
|
|
780
809
|
return 0;
|
|
781
810
|
}
|
|
@@ -794,6 +823,10 @@ function sortedJson(value) {
|
|
|
794
823
|
return serializeJqJson(sort(value), "compact").replace(/\n+$/, "");
|
|
795
824
|
}
|
|
796
825
|
function runSend(args, context) {
|
|
826
|
+
// The shell shifts two arguments for value options. Under `set -e`, a
|
|
827
|
+
// trailing option without its value exits 1 before printing diagnostics.
|
|
828
|
+
if (["--manifest", "--headline", "--note"].includes(args.at(-1) ?? ""))
|
|
829
|
+
return 1;
|
|
797
830
|
const parsed = parseFlags(args, ["--manifest", "--send", "--headline", "--note"]);
|
|
798
831
|
if (parsed.help) {
|
|
799
832
|
out(usage("send"));
|
|
@@ -884,7 +917,7 @@ function runSend(args, context) {
|
|
|
884
917
|
const access = jqRawPath(manifest, "repo.accessNote");
|
|
885
918
|
const branchCheck = branch ? subprocess("git", ["check-ref-format", "--branch", branch], context.cwd, { stdout: "ignore", stderr: "ignore" }) : { code: 1 };
|
|
886
919
|
if (branch && branchCheck.code === 0 && sha) {
|
|
887
|
-
repoSection = `## Get the code\n\nThe work lives on branch \`${branch}\` (base: \`${base}\`) at \`${sha}\`.\n\n\`\`\`bash\n# From your HQ root; clone first if the repo is not present${remote ? ` (remote: ${remote})` : ""}:\ngit -C ${quoteBash(repoPath)} fetch origin\ngit -C ${quoteBash(repoPath)} switch -- ${quoteBash(branch)}\n\`\`\`${access ? `\n\n> ${access}` : ""}`;
|
|
920
|
+
repoSection = `## Get the code\n\nThe work lives on branch \`${branch}\` (base: \`${base}\`) at \`${sha}\`.\n\n\`\`\`bash\n# From your HQ root; clone first if the repo is not present${remote ? ` (remote: ${remote})` : ""}:\ngit -C ${quoteBash(repoPath)} fetch origin\ngit -C ${quoteBash(repoPath)} switch -- ${quoteBash(branch)}\n\`\`\`${access ? `\n\n> ${access}` : "\n"}`;
|
|
888
921
|
}
|
|
889
922
|
}
|
|
890
923
|
const secretNames = list(getPath(manifest, "secrets", [])).map((entry) => text(entry)).join(",");
|
|
@@ -906,7 +939,7 @@ function runSend(args, context) {
|
|
|
906
939
|
if (repoSection)
|
|
907
940
|
body += `${repoSection}\n\n`;
|
|
908
941
|
if (secretsSection)
|
|
909
|
-
body += `${secretsSection}\n`;
|
|
942
|
+
body += `${secretsSection}\n\n`;
|
|
910
943
|
if (nextSteps)
|
|
911
944
|
body += `## Your next three steps\n\n${nextSteps}\n\n`;
|
|
912
945
|
body += `## If something is off\n\nThe manifest and BRIEF.md are published under \`delegation/${delegationId}/\` within \`companies/${company}/projects/${project}/\`. If a pull fails or a path looks empty, report it to ${from}.\n`;
|
|
@@ -956,7 +989,12 @@ function runSend(args, context) {
|
|
|
956
989
|
if (checksum(brief) !== checksum(path.join(path.dirname(publishedManifestPath), "BRIEF.md")))
|
|
957
990
|
return fail("send", "brief changed since publication; run verification again");
|
|
958
991
|
manifest.status = "sending";
|
|
959
|
-
|
|
992
|
+
try {
|
|
993
|
+
writeJsonAtomically(manifestPath, manifest, true, context.atomicWriteFileSystem);
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
return fail("send", "could not persist sending state; no DM attempted");
|
|
997
|
+
}
|
|
960
998
|
const dm = runHq(["dm", principal, headline, "--prompt-file", prompt, "--details-file", brief], context);
|
|
961
999
|
if (dm.code !== 0)
|
|
962
1000
|
return fail("send", "hq dm delivery outcome is uncertain; status is sending, inspect DM history before retrying");
|
|
@@ -994,6 +1032,10 @@ function captureRun(run) {
|
|
|
994
1032
|
}
|
|
995
1033
|
}
|
|
996
1034
|
function runVerify(args, context) {
|
|
1035
|
+
// The shell shifts a value-taking option and its value under `set -e`.
|
|
1036
|
+
// A trailing option therefore exits 1 before any diagnostic is printed.
|
|
1037
|
+
if (["--manifest", "--company", "--project", "--to", "--mode"].includes(args.at(-1) ?? ""))
|
|
1038
|
+
return 1;
|
|
997
1039
|
const parsed = parseFlags(args, ["--manifest", "--dry-run", "--company", "--project", "--to", "--mode"]);
|
|
998
1040
|
if (parsed.help) {
|
|
999
1041
|
out(usage("verify"));
|
|
@@ -1051,7 +1093,7 @@ function runVerify(args, context) {
|
|
|
1051
1093
|
out(` Recipient: ${recipient}\n Mode: ${mode}\n Company: ${company}\n Project: ${project}\n\n`);
|
|
1052
1094
|
out(" Vault grants that would be written:\n");
|
|
1053
1095
|
for (const entry of list(getPath(manifest, "vaultPrefixes", [])))
|
|
1054
|
-
out(` ${jqRawPath(entry, "permission")}\
|
|
1096
|
+
out(` ${jqRawPath(entry, "permission")}\ton ${jqRawPath(entry, "prefix")}\t(${jqRawPath(entry, "reason")})\n`);
|
|
1055
1097
|
out(`\n Secret names that would be granted (read): ${secretNames}\n\n`);
|
|
1056
1098
|
const repoValue = getPath(manifest, "repo");
|
|
1057
1099
|
if (repoValue !== null && repoValue !== undefined)
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/** Native implementations of the five `hq core delegate` shell companions. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
2
3
|
import { Command } from "commander";
|
|
3
4
|
import { type SpawnPlan } from "../utils/windows-spawn.js";
|
|
4
5
|
export declare const CORE_NATIVE_DELEGATE_FLAG = "core-native-delegate";
|
|
5
6
|
export type DelegateContext = {
|
|
6
7
|
cwd: string;
|
|
7
8
|
root?: string;
|
|
9
|
+
atomicWriteFileSystem?: DelegateAtomicWriteFileSystem;
|
|
8
10
|
};
|
|
11
|
+
export type DelegateAtomicWriteFileSystem = Pick<typeof fs, "openSync" | "writeFileSync" | "closeSync" | "renameSync" | "copyFileSync" | "unlinkSync" | "rmSync">;
|
|
9
12
|
type CommandResult = {
|
|
10
13
|
code: number;
|
|
11
14
|
stdout?: string;
|
|
@@ -38,8 +41,8 @@ export declare function rawJsonValue(value: unknown): string;
|
|
|
38
41
|
export declare function jqRawPath(value: unknown, dottedPath: string, fallback?: unknown): string;
|
|
39
42
|
export declare function jqRawPaths(value: unknown, dottedPaths: string[], fallback?: unknown): string;
|
|
40
43
|
export declare function requireRecord(value: unknown, property: string): JsonRecord;
|
|
41
|
-
export declare function moveFile(source: string, target: string): void;
|
|
42
|
-
export declare function writeJsonAtomically(target: string, value: unknown, siblingTemp?: boolean): void;
|
|
44
|
+
export declare function moveFile(source: string, target: string, fileSystem?: Pick<DelegateAtomicWriteFileSystem, "renameSync" | "copyFileSync" | "unlinkSync">): void;
|
|
45
|
+
export declare function writeJsonAtomically(target: string, value: unknown, siblingTemp?: boolean, fileSystem?: DelegateAtomicWriteFileSystem): void;
|
|
43
46
|
export declare function delegateDiagnosticPath(argument: string): string;
|
|
44
47
|
export declare function scanSecretFiles(file: string): string[] | undefined;
|
|
45
48
|
/** Fast-entry adapter; family gating and shell fallback happen before import. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Native implementations of the five `hq core delegate` shell companions. */
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
3
4
|
import * as fs from "node:fs";
|
|
4
5
|
import * as os from "node:os";
|
|
5
6
|
import * as path from "node:path";
|
|
@@ -174,25 +175,59 @@ export function requireRecord(value, property) {
|
|
|
174
175
|
throw new TypeError(`Cannot update non-object field ${property}`);
|
|
175
176
|
return result;
|
|
176
177
|
}
|
|
177
|
-
export function moveFile(source, target) {
|
|
178
|
+
export function moveFile(source, target, fileSystem = fs) {
|
|
178
179
|
try {
|
|
179
|
-
|
|
180
|
+
fileSystem.renameSync(source, target);
|
|
180
181
|
}
|
|
181
182
|
catch (error) {
|
|
182
183
|
if (error.code !== "EXDEV")
|
|
183
184
|
throw error;
|
|
184
185
|
// The shell companion uses `mv`, which copies across filesystem boundaries.
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
fileSystem.copyFileSync(source, target);
|
|
187
|
+
fileSystem.unlinkSync(source);
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
|
-
export function writeJsonAtomically(target, value, siblingTemp = false) {
|
|
190
|
+
export function writeJsonAtomically(target, value, siblingTemp = false, fileSystem = fs) {
|
|
190
191
|
const serialized = serializeJqJson(value, "pretty");
|
|
191
192
|
// Native delegate JSON writers use LF on every platform.
|
|
192
193
|
if (siblingTemp) {
|
|
193
|
-
const temporary = `${target}.tmp`;
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
const temporary = `${target}.tmp.${randomBytes(6).toString("hex")}`;
|
|
195
|
+
let fd;
|
|
196
|
+
let descriptorOpen = false;
|
|
197
|
+
let temporaryCreated = false;
|
|
198
|
+
try {
|
|
199
|
+
fd = fileSystem.openSync(temporary, "wx", 0o600);
|
|
200
|
+
descriptorOpen = true;
|
|
201
|
+
temporaryCreated = true;
|
|
202
|
+
fileSystem.writeFileSync(fd, serialized);
|
|
203
|
+
fileSystem.closeSync(fd);
|
|
204
|
+
descriptorOpen = false;
|
|
205
|
+
moveFile(temporary, target, fileSystem);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const failures = [error];
|
|
209
|
+
if (descriptorOpen && fd !== undefined) {
|
|
210
|
+
try {
|
|
211
|
+
fileSystem.closeSync(fd);
|
|
212
|
+
descriptorOpen = false;
|
|
213
|
+
}
|
|
214
|
+
catch (closeError) {
|
|
215
|
+
failures.push(closeError);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (temporaryCreated) {
|
|
219
|
+
try {
|
|
220
|
+
fileSystem.rmSync(temporary, { force: true });
|
|
221
|
+
}
|
|
222
|
+
catch (cleanupError) {
|
|
223
|
+
failures.push(cleanupError);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (failures.length > 1) {
|
|
227
|
+
throw new AggregateError(failures, "Failed to write and clean up delegate manifest temporary file");
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
196
231
|
return;
|
|
197
232
|
}
|
|
198
233
|
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), "hq-delegate-"));
|
|
@@ -13,7 +13,7 @@ export async function coreNativeFamilyEnabled(flagKey) {
|
|
|
13
13
|
export const CORE_NATIVE_DELEGATE_FLAG = "core-native-delegate";
|
|
14
14
|
export const CORE_NATIVE_ONTOLOGY_FLAG = "core-native-ontology";
|
|
15
15
|
export function coreNativeDelegateEnabled() {
|
|
16
|
-
return
|
|
16
|
+
return coreNativeFamilyEnabled(CORE_NATIVE_DELEGATE_FLAG);
|
|
17
17
|
}
|
|
18
18
|
export function coreNativeOntologyEnabled() {
|
|
19
19
|
return coreNativeFamilyEnabled(CORE_NATIVE_ONTOLOGY_FLAG);
|
|
@@ -919,8 +919,10 @@ export function uninstallGlobalCodex(opts = {}) {
|
|
|
919
919
|
const hqPointer = claudeTargets(home).hqPointer;
|
|
920
920
|
const state = readCodexState(t);
|
|
921
921
|
const hqRoot = state?.hqRoot ?? opts.hqRoot ?? readOrNull(hqPointer)?.trim();
|
|
922
|
-
if (!hqRoot)
|
|
923
|
-
|
|
922
|
+
if (!hqRoot) {
|
|
923
|
+
log('No HQ global Codex install found; nothing to remove.');
|
|
924
|
+
return [];
|
|
925
|
+
}
|
|
924
926
|
const regFile = opts.registryFile ?? registryFile(registryDir(home, env));
|
|
925
927
|
const claudeStillInstalled = fs.existsSync(claudeTargets(home).stateFile);
|
|
926
928
|
const providerBin = path.join(home, '.hq', 'bin');
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -158,5 +158,6 @@ export declare function pickupFailureUpdater(reasonRaw: string, details: Record<
|
|
|
158
158
|
export declare function applyOutpostAccountPickup(hqRoot: string, laneId: string, modelDeps?: KnownModelsDeps): LaneRecord;
|
|
159
159
|
export declare function cancelPendingQuestionsForLane(hqRoot: string, laneId: string, nowIso: string, operatorReason?: string): QuestionRecord[];
|
|
160
160
|
export declare function stopCommittedState(state: LaneRecord["state"]): "cancelled" | "killed";
|
|
161
|
+
export declare function readLaneReapStopEnabled(hqRoot: string, lane: LaneRecord, env: NodeJS.ProcessEnv): Promise<boolean>;
|
|
161
162
|
export declare function registerLanesCommand(parent: Command, deps?: LanesCommandDeps): Command;
|
|
162
163
|
//# sourceMappingURL=lanes.d.ts.map
|
package/dist/commands/lanes.js
CHANGED
|
@@ -3392,7 +3392,7 @@ async function runCreate(ctx, opts) {
|
|
|
3392
3392
|
const provider = explicitProvider ??
|
|
3393
3393
|
profile?.meta.provider ??
|
|
3394
3394
|
parseLaneProvider("codex");
|
|
3395
|
-
const continuityEnabled = opts.worker !== undefined && await readLanesRegistryFlagFailOpen(LANE_WORKER_CONTINUITY_FLAG, { company: company ?? meshCompany, env: ctx.env });
|
|
3395
|
+
const continuityEnabled = opts.worker !== undefined && await readLanesRegistryFlagFailOpen(LANE_WORKER_CONTINUITY_FLAG, { company: company ?? meshCompany, env: ctx.env, defaultValue: true });
|
|
3396
3396
|
let workerContinuity = continuityEnabled
|
|
3397
3397
|
? findWorkerContinuityLane(loadAllLanes(hqRoot), {
|
|
3398
3398
|
senior,
|
|
@@ -5456,6 +5456,7 @@ async function laneStatusFieldsForCommand(ctx, hqRoot, lane, now) {
|
|
|
5456
5456
|
enabled = await readLanesRegistryFlagFailOpen(LANE_STATUS_ROW_FIELDS_FLAG, {
|
|
5457
5457
|
...(resolved ? { company: resolved } : {}),
|
|
5458
5458
|
env: ctx.env,
|
|
5459
|
+
defaultValue: true,
|
|
5459
5460
|
});
|
|
5460
5461
|
}
|
|
5461
5462
|
catch (err) {
|
|
@@ -9365,6 +9366,16 @@ function attachJsonNoop(cmd) {
|
|
|
9365
9366
|
for (const child of cmd.commands)
|
|
9366
9367
|
attachJsonNoop(child);
|
|
9367
9368
|
}
|
|
9369
|
+
export async function readLaneReapStopEnabled(hqRoot, lane, env) {
|
|
9370
|
+
const resolution = resolveLaneCompany(hqRoot, lane);
|
|
9371
|
+
const company = resolution.company ?? undefined;
|
|
9372
|
+
return readLanesRegistryFlagFailOpen(LANE_REAP_STOP_FLAG, {
|
|
9373
|
+
company,
|
|
9374
|
+
...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
|
|
9375
|
+
env,
|
|
9376
|
+
defaultValue: true,
|
|
9377
|
+
});
|
|
9378
|
+
}
|
|
9368
9379
|
export function registerLanesCommand(parent, deps = {}) {
|
|
9369
9380
|
const ctx = resolveDeps(deps);
|
|
9370
9381
|
const storyHandlers = createLaneStoryHandlers({
|
|
@@ -9566,15 +9577,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9566
9577
|
presenceConnected: ctx.presenceConnected ?? false,
|
|
9567
9578
|
deps: ctx.livenessDeps,
|
|
9568
9579
|
}),
|
|
9569
|
-
stopEnabled: (lane) =>
|
|
9570
|
-
const resolution = resolveLaneCompany(ctx.hqRoot(), lane);
|
|
9571
|
-
const company = resolution.company ?? undefined;
|
|
9572
|
-
return readLanesRegistryFlag(LANE_REAP_STOP_FLAG, {
|
|
9573
|
-
company,
|
|
9574
|
-
...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
|
|
9575
|
-
env: ctx.env,
|
|
9576
|
-
});
|
|
9577
|
-
},
|
|
9580
|
+
stopEnabled: (lane) => readLaneReapStopEnabled(ctx.hqRoot(), lane, ctx.env),
|
|
9578
9581
|
}, wrap);
|
|
9579
9582
|
registerLaneDropboxRepairCommand(lanes, {
|
|
9580
9583
|
hqRoot: ctx.hqRoot,
|
package/dist/fast-scaffold.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export type FastScaffoldScript = {
|
|
|
50
50
|
requiresNodeNetworkCompatibility?: boolean;
|
|
51
51
|
};
|
|
52
52
|
/** Family kill switches whose owner-approved default is enabled. */
|
|
53
|
-
export declare const CORE_NATIVE_DEFAULT_ON_FAMILY_FLAGS: readonly ["core-native-session", "core-native-hook-helpers", "core-native-policy", "core-native-utilities", "core-native-jobs", "core-native-ontology", "core-native-handoff"];
|
|
53
|
+
export declare const CORE_NATIVE_DEFAULT_ON_FAMILY_FLAGS: readonly ["core-native-session", "core-native-hook-helpers", "core-native-policy", "core-native-utilities", "core-native-jobs", "core-native-ontology", "core-native-handoff", "core-native-delegate"];
|
|
54
54
|
export declare function isDefaultOnFastFamily(flagKey: string): boolean;
|
|
55
55
|
/** The relocated scripts that qualify for the entrypoint fast path. */
|
|
56
56
|
export declare const FAST_SCAFFOLD_SCRIPTS: readonly FastScaffoldScript[];
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -18,6 +18,7 @@ export const CORE_NATIVE_DEFAULT_ON_FAMILY_FLAGS = [
|
|
|
18
18
|
"core-native-jobs",
|
|
19
19
|
"core-native-ontology",
|
|
20
20
|
"core-native-handoff",
|
|
21
|
+
"core-native-delegate",
|
|
21
22
|
];
|
|
22
23
|
const defaultOnFamilyFlags = new Set(CORE_NATIVE_DEFAULT_ON_FAMILY_FLAGS);
|
|
23
24
|
export function isDefaultOnFastFamily(flagKey) {
|
package/dist/lib/bot/config.js
CHANGED
|
@@ -138,8 +138,10 @@ export function validateBotDisplayName(text) {
|
|
|
138
138
|
expected: true,
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
// A letter first, then letters, numbers, spaces, apostrophes, periods and
|
|
142
|
+
// hyphens ("R2D2", "Scout 2").
|
|
143
|
+
if (!/^[\p{L}\p{M}][\p{L}\p{M}\p{N} .'-]*$/u.test(trimmed)) {
|
|
144
|
+
throw Object.assign(new Error("--display-name can use letters, numbers, spaces, apostrophes, periods and hyphens."), {
|
|
143
145
|
expected: true,
|
|
144
146
|
});
|
|
145
147
|
}
|
|
@@ -24,5 +24,27 @@ export declare function codexPrompt(input: RuntimeTurnInput): string;
|
|
|
24
24
|
* warnings (`item.completed` with `item.type === "error"`) are not failures.
|
|
25
25
|
*/
|
|
26
26
|
export declare function codexFailureReason(stdout: string): string | undefined;
|
|
27
|
+
/** True when `candidate` is a regular file the current user can execute. */
|
|
28
|
+
export type IsExecutableFile = (candidate: string) => boolean;
|
|
29
|
+
export declare function isExecutableFile(candidate: string): boolean;
|
|
30
|
+
/** App bundles that carry the Codex CLI, in the HQ desktop app's lookup order. */
|
|
31
|
+
export declare const CODEX_APP_BUNDLES: readonly ["ChatGPT.app", "Codex.app"];
|
|
32
|
+
/** Where the CLI sits inside a bundle: current layout first, then the older one. */
|
|
33
|
+
export declare const CODEX_BUNDLE_CLI_PATHS: readonly [string, string];
|
|
34
|
+
/**
|
|
35
|
+
* The Codex CLI shipped inside the ChatGPT or Codex desktop app. A Mac that
|
|
36
|
+
* only has the desktop app has no `codex` on PATH, yet the HQ app counts it as
|
|
37
|
+
* installed, so a bot must be able to use it too. Order matches the HQ desktop
|
|
38
|
+
* app (apps/sync/src-tauri/src/commands/launch.rs, bundled_codex_bin_in):
|
|
39
|
+
* /Applications before ~/Applications, ChatGPT.app before Codex.app, and the
|
|
40
|
+
* codex-cli/bin layout before the older Resources/codex one.
|
|
41
|
+
*/
|
|
42
|
+
export declare function bundledCodexBin(homeDir?: string, isExecutable?: IsExecutableFile): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* PATH wins. On macOS, fall back to the desktop app's bundled CLI. When
|
|
45
|
+
* nothing is found the bare name is returned, so the spawn fails with the
|
|
46
|
+
* usual "codex is not installed or not on PATH" error.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveCodexBinary(env: NodeJS.ProcessEnv, homeDir?: string, platform?: NodeJS.Platform, cwd?: string, isExecutable?: IsExecutableFile): string;
|
|
27
49
|
export declare function createCodexRuntime(): BotRuntime;
|
|
28
50
|
//# sourceMappingURL=codex.d.ts.map
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* Verified against Codex CLI on 2026-09-10 (event names; the model itself
|
|
14
14
|
* cannot run under this machine's account).
|
|
15
15
|
*/
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as os from "node:os";
|
|
18
|
+
import * as path from "node:path";
|
|
16
19
|
import { parseJsonLine, RuntimeError } from "./types.js";
|
|
17
20
|
import { resolveRuntimeBinary } from "./windows.js";
|
|
18
21
|
/** Progress events in one codex JSONL line: finished agent messages and tool starts. */
|
|
@@ -79,11 +82,70 @@ function unwrapApiError(raw) {
|
|
|
79
82
|
}
|
|
80
83
|
return t;
|
|
81
84
|
}
|
|
85
|
+
export function isExecutableFile(candidate) {
|
|
86
|
+
try {
|
|
87
|
+
if (!fs.statSync(candidate).isFile())
|
|
88
|
+
return false;
|
|
89
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** App bundles that carry the Codex CLI, in the HQ desktop app's lookup order. */
|
|
97
|
+
export const CODEX_APP_BUNDLES = ["ChatGPT.app", "Codex.app"];
|
|
98
|
+
/** Where the CLI sits inside a bundle: current layout first, then the older one. */
|
|
99
|
+
export const CODEX_BUNDLE_CLI_PATHS = [
|
|
100
|
+
path.join("Contents", "Resources", "codex-cli", "bin", "codex"),
|
|
101
|
+
path.join("Contents", "Resources", "codex"),
|
|
102
|
+
];
|
|
103
|
+
/**
|
|
104
|
+
* The Codex CLI shipped inside the ChatGPT or Codex desktop app. A Mac that
|
|
105
|
+
* only has the desktop app has no `codex` on PATH, yet the HQ app counts it as
|
|
106
|
+
* installed, so a bot must be able to use it too. Order matches the HQ desktop
|
|
107
|
+
* app (apps/sync/src-tauri/src/commands/launch.rs, bundled_codex_bin_in):
|
|
108
|
+
* /Applications before ~/Applications, ChatGPT.app before Codex.app, and the
|
|
109
|
+
* codex-cli/bin layout before the older Resources/codex one.
|
|
110
|
+
*/
|
|
111
|
+
export function bundledCodexBin(homeDir = os.homedir(), isExecutable = isExecutableFile) {
|
|
112
|
+
const appDirs = ["/Applications", path.join(homeDir, "Applications")];
|
|
113
|
+
for (const dir of appDirs) {
|
|
114
|
+
for (const bundle of CODEX_APP_BUNDLES) {
|
|
115
|
+
for (const rel of CODEX_BUNDLE_CLI_PATHS) {
|
|
116
|
+
const candidate = path.join(dir, bundle, rel);
|
|
117
|
+
if (isExecutable(candidate))
|
|
118
|
+
return candidate;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function codexOnPath(env, isExecutable) {
|
|
125
|
+
for (const dir of (env.PATH ?? "").split(path.delimiter)) {
|
|
126
|
+
if (dir && isExecutable(path.join(dir, "codex")))
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* PATH wins. On macOS, fall back to the desktop app's bundled CLI. When
|
|
133
|
+
* nothing is found the bare name is returned, so the spawn fails with the
|
|
134
|
+
* usual "codex is not installed or not on PATH" error.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveCodexBinary(env, homeDir, platform = process.platform, cwd = process.cwd(), isExecutable = isExecutableFile) {
|
|
137
|
+
if (platform === "win32")
|
|
138
|
+
return resolveRuntimeBinary("codex", env, platform, undefined, cwd);
|
|
139
|
+
if (platform !== "darwin" || codexOnPath(env, isExecutable))
|
|
140
|
+
return "codex";
|
|
141
|
+
// `||`, not `??`: an empty HOME must not turn ~/Applications into a relative path.
|
|
142
|
+
return bundledCodexBin(homeDir || env.HOME || os.homedir(), isExecutable) ?? "codex";
|
|
143
|
+
}
|
|
82
144
|
export function createCodexRuntime() {
|
|
83
145
|
return {
|
|
84
146
|
id: "codex",
|
|
85
147
|
binary: "codex",
|
|
86
|
-
resolveBinary: (env, cwd, platform) =>
|
|
148
|
+
resolveBinary: (env, cwd, platform) => resolveCodexBinary(env, undefined, platform ?? process.platform, cwd),
|
|
87
149
|
buildArgs(input, scratch) {
|
|
88
150
|
const promptArg = input.platform === "win32" ? "-" : codexPrompt(input);
|
|
89
151
|
const common = ["--json", "--skip-git-repo-check", "-o", scratch.outputFile];
|
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { basename, countImmediate,
|
|
3
|
+
import { basename, countImmediate, exists, immediateEntries, isHidden, log, prdProjectStatus, readJson, sanitize, titleize, truncate, writeIfChanged } from "./shared.js";
|
|
4
4
|
export class CompanyIndexUserError extends Error {
|
|
5
5
|
constructor(message) {
|
|
6
6
|
super(message);
|
|
7
7
|
this.name = "CompanyIndexUserError";
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* One-line summary for a top-level company directory.
|
|
12
|
+
*
|
|
13
|
+
* Output must be identical on every machine that holds the same synced tree,
|
|
14
|
+
* because company INDEX.md files are synced. So nothing here reads local-only
|
|
15
|
+
* state: `repos/` (never synced), `data/` (partly synced), whether
|
|
16
|
+
* `knowledge/` is an embedded git checkout, or symlink targets. Counts use
|
|
17
|
+
* lstat so a symlinked child is never counted as a directory or file.
|
|
18
|
+
*/
|
|
10
19
|
function description(root, company, name) {
|
|
11
20
|
const base = `companies/${company}/${name}`;
|
|
12
|
-
const
|
|
21
|
+
const realDirectory = (n, f) => !isHidden(n) && fs.lstatSync(f).isDirectory();
|
|
13
22
|
switch (name) {
|
|
14
|
-
case "data": return
|
|
15
|
-
case "knowledge": return
|
|
16
|
-
case "settings": return `${countImmediate(root, base,
|
|
23
|
+
case "data": return "Company data";
|
|
24
|
+
case "knowledge": return `Company knowledge (${countImmediate(root, base, realDirectory)} subdirs)`;
|
|
25
|
+
case "settings": return `${countImmediate(root, base, realDirectory)} integration(s)`;
|
|
17
26
|
case "policies": return `${countImmediate(root, base, (n) => n.endsWith(".md"))} policy file(s)`;
|
|
18
27
|
// Match `find -type d`: count actual directories, not symlinks to directories.
|
|
19
|
-
case "projects": return `${countImmediate(root, base, (n, f) => !n.startsWith("_") &&
|
|
20
|
-
case "workers": return `${countImmediate(root, base,
|
|
21
|
-
case "repos": return
|
|
28
|
+
case "projects": return `${countImmediate(root, base, (n, f) => !n.startsWith("_") && realDirectory(n, f))} project(s)`;
|
|
29
|
+
case "workers": return `${countImmediate(root, base, realDirectory)} worker(s)`;
|
|
30
|
+
case "repos": return "Local repo links (not synced)";
|
|
22
31
|
case "registry": return `Resource registry (${countImmediate(root, `${base}/resources`, (n) => n.endsWith(".yaml"))} resource(s))`;
|
|
23
|
-
case "scripts": return `${countImmediate(root, base, (_n, f) => fs.
|
|
32
|
+
case "scripts": return `${countImmediate(root, base, (_n, f) => fs.lstatSync(f).isFile())} script(s)`;
|
|
24
33
|
default: return "—";
|
|
25
34
|
}
|
|
26
35
|
}
|
|
@@ -56,6 +65,7 @@ export function renderCompanies(context, args = []) {
|
|
|
56
65
|
}
|
|
57
66
|
}
|
|
58
67
|
const written = [];
|
|
68
|
+
const unchanged = [];
|
|
59
69
|
let scopedDirectories;
|
|
60
70
|
if (selectedCompany === undefined) {
|
|
61
71
|
scopedDirectories = immediateEntries(context.root, "companies", "dir");
|
|
@@ -87,7 +97,9 @@ export function renderCompanies(context, args = []) {
|
|
|
87
97
|
const company = basename(directory);
|
|
88
98
|
if (company.startsWith("_") || isHidden(company))
|
|
89
99
|
continue;
|
|
90
|
-
|
|
100
|
+
// No date line: the file must only change when its content changes. The
|
|
101
|
+
// `> Auto-generated.` marker stays, since hq-cloud detects generated INDEX.md by it.
|
|
102
|
+
const lines = [`# ${titleize(company)}`, "", "> Auto-generated.", "", "| Name | Description |", "|------|-------------|"];
|
|
91
103
|
for (const name of ["data", "knowledge", "policies", "projects", "registry", "repos", "scripts", "settings", "workers"]) {
|
|
92
104
|
if (exists(context.root, `companies/${company}/${name}`))
|
|
93
105
|
lines.push(`| \`${name}/\` | ${sanitize(description(context.root, company, name))} |`);
|
|
@@ -104,18 +116,23 @@ export function renderCompanies(context, args = []) {
|
|
|
104
116
|
const parsed = readJson(prd);
|
|
105
117
|
let text = typeof parsed?.description === "string" ? parsed.description : "";
|
|
106
118
|
text = truncate(sanitize(text), 110) || "—";
|
|
107
|
-
lines.push(`| \`${project}\` | ${
|
|
119
|
+
lines.push(`| \`${project}\` | ${prdProjectStatus(prd, "active")} | ${text} |`);
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
else
|
|
111
123
|
lines.push("_No projects directory._");
|
|
112
124
|
lines.push("", "## Deployments", "", "_See `companies/manifest.yaml` for deployment targets._", "");
|
|
113
125
|
const output = `companies/${company}/INDEX.md`;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
if (writeIfChanged(context, output, lines.join("\n"))) {
|
|
127
|
+
written.push(output);
|
|
128
|
+
log(context, `rebuild-companies-index: wrote ${output}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
unchanged.push(output);
|
|
132
|
+
log(context, `rebuild-companies-index: unchanged ${output}`);
|
|
133
|
+
}
|
|
117
134
|
}
|
|
118
|
-
log(context, `rebuild-companies-index: regenerated ${written.length} company INDEX.md file(s)`);
|
|
119
|
-
return { written };
|
|
135
|
+
log(context, `rebuild-companies-index: regenerated ${written.length + unchanged.length} company INDEX.md file(s)`);
|
|
136
|
+
return { written, unchanged };
|
|
120
137
|
}
|
|
121
138
|
//# sourceMappingURL=companies.js.map
|
|
@@ -10,6 +10,8 @@ export type RenderContext = {
|
|
|
10
10
|
};
|
|
11
11
|
export type RenderResult = {
|
|
12
12
|
written: string[];
|
|
13
|
+
/** Relative paths whose rendered content matched the file on disk, so no write happened. */
|
|
14
|
+
unchanged?: string[];
|
|
13
15
|
};
|
|
14
16
|
export declare function rel(root: string, file: string): string;
|
|
15
17
|
export declare function at(root: string, relative: string): string;
|
|
@@ -32,6 +34,15 @@ export declare function atomicWrite(file: string, content: string | Buffer, opti
|
|
|
32
34
|
mode?: number;
|
|
33
35
|
}): void;
|
|
34
36
|
export declare function write(context: RenderContext, relative: string, content: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Write only when the rendered bytes differ from the file on disk. Returns
|
|
39
|
+
* true when the file was written, false when it was already identical.
|
|
40
|
+
*
|
|
41
|
+
* The comparison is exact rather than `generatedFingerprint`: that fingerprint
|
|
42
|
+
* drops keep fences and trailing newlines, which would hide real differences.
|
|
43
|
+
* Callers that want a skip must render deterministic content (no dates).
|
|
44
|
+
*/
|
|
45
|
+
export declare function writeIfChanged(context: RenderContext, relative: string, content: string): boolean;
|
|
35
46
|
/** Drop keep fences, the generated stamp, and the rolling date so two generated bodies can be compared. */
|
|
36
47
|
export declare function generatedFingerprint(content: string): string;
|
|
37
48
|
/** Insert a content stamp after the Auto-generated line. Idempotent. */
|
|
@@ -42,6 +53,13 @@ export declare function looksAutoGenerated(content: string): boolean;
|
|
|
42
53
|
export declare function generatedStamp(content: string): string | undefined;
|
|
43
54
|
export declare function log(context: RenderContext, message: string): void;
|
|
44
55
|
export declare function projectStatus(root: string, project: string, prdPath: string, fallback: string, expectedStatePrdPath?: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Project status from prd.json alone: "complete" when every story passes,
|
|
58
|
+
* otherwise `fallback`. Unlike `projectStatus`, this never reads the
|
|
59
|
+
* machine-local `workspace/orchestrator/<project>/state.json`, so synced
|
|
60
|
+
* output renders the same on every machine.
|
|
61
|
+
*/
|
|
62
|
+
export declare function prdProjectStatus(prdPath: string, fallback: string): string;
|
|
45
63
|
export declare function basename(file: string): string;
|
|
46
64
|
export declare function isHidden(name: string): boolean;
|
|
47
65
|
export declare function mtime(file: string): number;
|
|
@@ -100,6 +100,21 @@ export function write(context, relative, content) {
|
|
|
100
100
|
atomicWrite(at(context.root, relative), content);
|
|
101
101
|
return relative;
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Write only when the rendered bytes differ from the file on disk. Returns
|
|
105
|
+
* true when the file was written, false when it was already identical.
|
|
106
|
+
*
|
|
107
|
+
* The comparison is exact rather than `generatedFingerprint`: that fingerprint
|
|
108
|
+
* drops keep fences and trailing newlines, which would hide real differences.
|
|
109
|
+
* Callers that want a skip must render deterministic content (no dates).
|
|
110
|
+
*/
|
|
111
|
+
export function writeIfChanged(context, relative, content) {
|
|
112
|
+
const file = at(context.root, relative);
|
|
113
|
+
if (readText(file) === content)
|
|
114
|
+
return false;
|
|
115
|
+
atomicWrite(file, content);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
103
118
|
const GENERATED_DATE_LINE = /^> Auto-generated\. Updated: \d{4}-\d{2}-\d{2}$/m;
|
|
104
119
|
const GENERATED_STAMP_LINE = /^<!-- hq-generated-sha256: [a-f0-9]{64} -->\n?/m;
|
|
105
120
|
function keepFencePattern() {
|
|
@@ -149,6 +164,15 @@ export function projectStatus(root, project, prdPath, fallback, expectedStatePrd
|
|
|
149
164
|
return "active";
|
|
150
165
|
return raw;
|
|
151
166
|
}
|
|
167
|
+
return prdProjectStatus(prdPath, fallback);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Project status from prd.json alone: "complete" when every story passes,
|
|
171
|
+
* otherwise `fallback`. Unlike `projectStatus`, this never reads the
|
|
172
|
+
* machine-local `workspace/orchestrator/<project>/state.json`, so synced
|
|
173
|
+
* output renders the same on every machine.
|
|
174
|
+
*/
|
|
175
|
+
export function prdProjectStatus(prdPath, fallback) {
|
|
152
176
|
const prd = readJson(prdPath);
|
|
153
177
|
if (prd && Array.isArray(prd.userStories) && prd.userStories.length > 0 && prd.userStories.every((story) => Boolean(story.passes)))
|
|
154
178
|
return "complete";
|
|
@@ -347,26 +347,41 @@ function defaultProcessCommands() {
|
|
|
347
347
|
return [];
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
|
+
const WATCH_RUNTIME_EXECUTABLES = ["bun", "deno", "node", "nodejs", "ts-node", "tsx"];
|
|
351
|
+
// The hq executable path may contain spaces (the desktop toolchain installs
|
|
352
|
+
// under "~/Library/Application Support/Indigo HQ/..."), so the ps line cannot
|
|
353
|
+
// be split on whitespace to find it. Match the fixed "lanes watch <ids>" suffix
|
|
354
|
+
// on the raw line, then validate whatever precedes it as an hq invocation.
|
|
355
|
+
// A path form (absolute or ./, ../) is required so an unrelated leading
|
|
356
|
+
// command such as "vim /tmp/hq" is not absorbed into the path.
|
|
357
|
+
function isHqExecutablePath(candidate) {
|
|
358
|
+
if (candidate === "hq")
|
|
359
|
+
return true;
|
|
360
|
+
if (!/^\.{0,2}\//u.test(candidate) || path.basename(candidate) !== "hq") {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
return !candidate.split(" ").some((piece) => piece.startsWith("-"));
|
|
364
|
+
}
|
|
365
|
+
function isHqInvocationPrefix(prefix) {
|
|
366
|
+
if (isHqExecutablePath(prefix))
|
|
367
|
+
return true;
|
|
368
|
+
const runtimeEnd = prefix.indexOf(" ");
|
|
369
|
+
if (runtimeEnd === -1)
|
|
370
|
+
return false;
|
|
371
|
+
const runtime = path.basename(prefix.slice(0, runtimeEnd));
|
|
372
|
+
return (WATCH_RUNTIME_EXECUTABLES.includes(runtime) &&
|
|
373
|
+
isHqExecutablePath(prefix.slice(runtimeEnd + 1)));
|
|
374
|
+
}
|
|
350
375
|
function processHasExactCommand(processCommands, command) {
|
|
351
376
|
const laneIds = parseCanonicalWatchCommand(command);
|
|
352
377
|
if (!laneIds)
|
|
353
378
|
return false;
|
|
354
|
-
const expectedSuffix =
|
|
355
|
-
return processCommands.some((
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
if (suffixStart < 1)
|
|
359
|
-
return false;
|
|
360
|
-
if (!expectedSuffix.every((token, index) => tokens[suffixStart + index] === token)) {
|
|
361
|
-
return false;
|
|
362
|
-
}
|
|
363
|
-
const hqToken = tokens[suffixStart - 1];
|
|
364
|
-
if (path.basename(hqToken) !== "hq")
|
|
379
|
+
const expectedSuffix = ` lanes watch ${laneIds.join(" ")}`;
|
|
380
|
+
return processCommands.some((rawLine) => {
|
|
381
|
+
const line = rawLine.trim().replace(/\s+/gu, " ");
|
|
382
|
+
if (!line.endsWith(expectedSuffix))
|
|
365
383
|
return false;
|
|
366
|
-
|
|
367
|
-
return (prefix.length === 0 ||
|
|
368
|
-
(prefix.length === 1 &&
|
|
369
|
-
["bun", "deno", "node", "nodejs", "ts-node", "tsx"].includes(path.basename(prefix[0]))));
|
|
384
|
+
return isHqInvocationPrefix(line.slice(0, -expectedSuffix.length));
|
|
370
385
|
});
|
|
371
386
|
}
|
|
372
387
|
export function readLiveMonitorEvidence(sessionId, options = {}) {
|