@indigoai-us/hq-cli 5.345.117 → 5.345.119

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/assets/scaffold/core/scripts/pipeline-conductor.sh +2671 -0
  3. package/assets/scaffold/core/scripts/pipeline-driver.sh +820 -0
  4. package/assets/scaffold/core/scripts/pipeline-envelope.sh +211 -0
  5. package/dist/command-catalog.generated.d.ts +34 -0
  6. package/dist/command-catalog.generated.js +40 -0
  7. package/dist/commands/core-checkpoint-native.d.ts +17 -3
  8. package/dist/commands/core-checkpoint-native.js +48 -8
  9. package/dist/commands/core-derive-trigger-facts-native.js +15 -7
  10. package/dist/commands/core-migrate-policy-triggers-native.js +3 -1
  11. package/dist/commands/core-pipeline-conductor-command.d.ts +5 -0
  12. package/dist/commands/core-pipeline-conductor-command.js +138 -0
  13. package/dist/commands/core-pipeline-conductor-native.d.ts +7 -0
  14. package/dist/commands/core-pipeline-conductor-native.js +2950 -0
  15. package/dist/commands/core-pipeline-driver-native.d.ts +24 -0
  16. package/dist/commands/core-pipeline-driver-native.js +843 -0
  17. package/dist/commands/core-pipeline-driver-process.d.ts +4 -0
  18. package/dist/commands/core-pipeline-driver-process.js +68 -0
  19. package/dist/commands/core-pipeline-envelope-native.d.ts +4 -0
  20. package/dist/commands/core-pipeline-envelope-native.js +87 -0
  21. package/dist/commands/core-policy.js +3 -0
  22. package/dist/commands/core-worker-lint-native.d.ts +0 -1
  23. package/dist/commands/core-worker-lint-native.js +0 -1
  24. package/dist/commands/core-worker-share-native.js +2 -2
  25. package/dist/commands/core-worker-skills-command.d.ts +1 -1
  26. package/dist/commands/core-worker-skills-command.js +3 -20
  27. package/dist/commands/core.d.ts +5 -1
  28. package/dist/commands/core.js +8 -9
  29. package/dist/commands/integrations-core.d.ts +7 -1
  30. package/dist/commands/integrations-core.js +17 -4
  31. package/dist/commands/integrations.js +91 -22
  32. package/dist/commands/storage-gate.d.ts +4 -3
  33. package/dist/commands/storage-gate.js +4 -3
  34. package/dist/fast-scaffold.js +0 -2
  35. package/dist/lib/storage-feature-flag.d.ts +7 -7
  36. package/package.json +1 -1
  37. package/assets/scaffold/core/scripts/lint-shared-worker-skills.sh +0 -197
  38. package/assets/scaffold/core/scripts/share-worker-skill.sh +0 -178
  39. package/dist/commands/core-native-worker-skills-flag.d.ts +0 -4
  40. package/dist/commands/core-native-worker-skills-flag.js +0 -7
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env bash
2
+ # pipeline-envelope.sh - validate the HQ phase envelope / phase handoff shape,
3
+ # and normalize raw engine replies into a single JSON object.
4
+ #
5
+ # Usage:
6
+ # pipeline-envelope.sh validate [--kind envelope|handoff] <file|->
7
+ # pipeline-envelope.sh normalize <file|->
8
+ #
9
+ # validate: exit 0 when valid; otherwise exit 1 and print each problem to
10
+ # stderr as "missing field: <name>" or "wrong type: <name> (want <type>)".
11
+ # Unparseable JSON, empty input, a missing file, or a non-object top level
12
+ # always exit non-zero.
13
+ # normalize: extract one JSON object from a reply that may be wrapped in a
14
+ # ```json fence or surrounded by prose; print it compactly to stdout.
15
+ #
16
+ # Shapes are documented in .claude/skills/_shared/lane-dispatch-protocol.md §8.
17
+ # Portable to bash 3.2; parsing is done by node.
18
+
19
+ set -u
20
+
21
+ usage() {
22
+ echo "usage: pipeline-envelope.sh validate [--kind envelope|handoff] <file|->" >&2
23
+ echo " pipeline-envelope.sh normalize <file|->" >&2
24
+ exit 2
25
+ }
26
+
27
+ command -v node >/dev/null 2>&1 || { echo "pipeline-envelope: node required" >&2; exit 2; }
28
+
29
+ [ $# -ge 1 ] || usage
30
+ cmd="$1"; shift
31
+ kind=""
32
+ case "$cmd" in
33
+ validate)
34
+ if [ "${1:-}" = "--kind" ]; then
35
+ [ $# -ge 2 ] || usage
36
+ kind="$2"; shift 2
37
+ case "$kind" in envelope|handoff) ;; *) echo "pipeline-envelope: bad --kind: $kind" >&2; exit 2 ;; esac
38
+ fi
39
+ ;;
40
+ normalize) ;;
41
+ -h|--help) usage ;;
42
+ *) usage ;;
43
+ esac
44
+ [ $# -eq 1 ] || usage
45
+ src="$1"
46
+
47
+ # node reads its program from a heredoc, so stdin must be spooled first.
48
+ if [ "$src" = "-" ]; then
49
+ spool="$(mktemp "${TMPDIR:-/tmp}/pe-stdin.XXXXXX")" || exit 1
50
+ trap 'rm -f "$spool"' EXIT
51
+ cat > "$spool"
52
+ src="$spool"
53
+ fi
54
+
55
+ if [ ! -f "$src" ]; then
56
+ echo "pipeline-envelope: file not found: $src" >&2
57
+ exit 1
58
+ fi
59
+
60
+ PE_CMD="$cmd" PE_KIND="$kind" PE_SRC="$src" node - <<'JS'
61
+ 'use strict';
62
+ const fs = require('fs');
63
+ const cmd = process.env.PE_CMD; let kind = process.env.PE_KIND; const src = process.env.PE_SRC;
64
+ const err = (m) => process.stderr.write(m + '\n');
65
+ let text;
66
+ try {
67
+ text = new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(src));
68
+ if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
69
+ text = text.replace(/\r\n?/g, '\n');
70
+ } catch (e) {
71
+ err('pipeline-envelope: cannot read input: ' + (e && e.message ? e.message : e)); process.exit(1);
72
+ }
73
+ const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
74
+
75
+ // Python json.dumps default output: ", " and ": " separators.
76
+ function dumpStr(s, ascii) {
77
+ let o = JSON.stringify(s);
78
+ if (ascii) o = o.replace(/[\u0080-￿]/g, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
79
+ return o;
80
+ }
81
+ function dumpNum(n) {
82
+ if (!Number.isFinite(n)) return n > 0 ? 'Infinity' : n < 0 ? '-Infinity' : 'NaN';
83
+ if (Number.isInteger(n) && Math.abs(n) < 1e16) return String(n);
84
+ let s = String(n);
85
+ const m = /^(-?)(\d)(?:\.(\d+))?e([+-])(\d+)$/.exec(s);
86
+ if (m) s = m[1] + m[2] + (m[3] ? '.' + m[3] : '') + 'e' + m[4] + m[5].padStart(2, '0');
87
+ return s;
88
+ }
89
+ function dumps(v, ascii) {
90
+ if (v === null) return 'null';
91
+ if (v === true) return 'true';
92
+ if (v === false) return 'false';
93
+ if (typeof v === 'number') return dumpNum(v);
94
+ if (typeof v === 'string') return dumpStr(v, ascii);
95
+ if (Array.isArray(v)) return '[' + v.map((x) => dumps(x, ascii)).join(', ') + ']';
96
+ return '{' + Object.keys(v).map((k) => dumpStr(k, ascii) + ': ' + dumps(v[k], ascii)).join(', ') + '}';
97
+ }
98
+ // end index (exclusive) of the balanced {...} starting at j, string-aware; -1 when unbalanced
99
+ function balancedEnd(t, j) {
100
+ let depth = 0, inStr = false;
101
+ for (let i = j; i < t.length; i++) {
102
+ const c = t[i];
103
+ if (inStr) {
104
+ if (c === '\\') i++;
105
+ else if (c === '"') inStr = false;
106
+ continue;
107
+ }
108
+ if (c === '"') inStr = true;
109
+ else if (c === '{' || c === '[') depth++;
110
+ else if (c === '}' || c === ']') { depth--; if (depth === 0) return i + 1; if (depth < 0) return -1; }
111
+ }
112
+ return -1;
113
+ }
114
+
115
+ if (cmd === 'normalize') {
116
+ let cands = [];
117
+ // 1. fenced blocks first (```json ... ``` or ``` ... ```)
118
+ const fence = /```[A-Za-z0-9_-]*\s*\n([\s\S]*?)```/g;
119
+ let m;
120
+ while ((m = fence.exec(text)) !== null) {
121
+ try { const v = JSON.parse(m[1].trim()); if (isObj(v)) cands.push(v); } catch (e) { /* skip */ }
122
+ }
123
+ // 2. otherwise scan for top-level JSON objects in the prose
124
+ if (!cands.length) {
125
+ let i = 0;
126
+ while (i < text.length) {
127
+ const j = text.indexOf('{', i);
128
+ if (j < 0) break;
129
+ const end = balancedEnd(text, j);
130
+ let v, ok = false;
131
+ if (end > 0) { try { v = JSON.parse(text.slice(j, end)); ok = true; } catch (e) { ok = false; } }
132
+ if (ok) { if (isObj(v)) cands.push(v); i = end; } else i = j + 1;
133
+ }
134
+ }
135
+ if (!cands.length) { err('normalize: no JSON object found'); process.exit(1); }
136
+ if (cands.length > 1) {
137
+ // prefer the one carrying a known schema, if exactly one does
138
+ const tagged = cands.filter((c) => c.schema === 'hq-phase-handoff/v1' || c.schema === 'hq-phase-envelope/v1');
139
+ if (tagged.length !== 1) { err('normalize: ' + cands.length + ' JSON objects found, expected exactly one'); process.exit(1); }
140
+ cands = tagged;
141
+ }
142
+ process.stdout.write(dumps(cands[0], false) + '\n');
143
+ process.exit(0);
144
+ }
145
+
146
+ // ---- validate ----
147
+ if (!text.trim()) { err('invalid: empty input'); process.exit(1); }
148
+ let doc;
149
+ try { doc = JSON.parse(text); } catch (e) { err('invalid: unparseable JSON (' + e.message + ')'); process.exit(1); }
150
+ if (!isObj(doc)) {
151
+ const tn = doc === null ? 'NoneType' : Array.isArray(doc) ? 'list' : typeof doc === 'string' ? 'str'
152
+ : typeof doc === 'boolean' ? 'bool' : Number.isInteger(doc) ? 'int' : 'float';
153
+ err('invalid: top level must be a JSON object, got ' + tn); process.exit(1);
154
+ }
155
+ const has = (k) => Object.prototype.hasOwnProperty.call(doc, k);
156
+ const SCHEMAS = { envelope: 'hq-phase-envelope/v1', handoff: 'hq-phase-handoff/v1' };
157
+ if (!kind) {
158
+ for (const k of Object.keys(SCHEMAS)) if (doc.schema === SCHEMAS[k]) kind = k;
159
+ if (!kind) {
160
+ if (!has('schema')) err('missing field: schema');
161
+ else err('wrong type: schema (want ' + SCHEMAS.envelope + ' or ' + SCHEMAS.handoff + ')');
162
+ process.exit(1);
163
+ }
164
+ }
165
+ const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]00:?00)\n?$/;
166
+ const problems = [];
167
+ const isStr = (v) => typeof v === 'string';
168
+ const isBool = (v) => typeof v === 'boolean';
169
+ const strList = (v) => Array.isArray(v) && v.every(isStr);
170
+ function check(name, ok, want, required = true) {
171
+ if (!has(name)) { if (required) problems.push('missing field: ' + name); return; }
172
+ if (!ok(doc[name])) problems.push('wrong type: ' + name + ' (want ' + want + ')');
173
+ }
174
+ const sch = has('schema') ? doc.schema : null;
175
+ if (sch !== null && sch !== SCHEMAS[kind]) problems.push('wrong type: schema (want ' + SCHEMAS[kind] + ')');
176
+ else if (!has('schema')) problems.push('missing field: schema');
177
+
178
+ const nonEmpty = (v) => isStr(v) && v !== '';
179
+ if (kind === 'envelope') {
180
+ for (const f of ['story_id', 'phase', 'worker_id']) check(f, nonEmpty, 'non-empty string');
181
+ check('worktree', nonEmpty, 'string path');
182
+ check('incoming_handoff', (v) => v === null || nonEmpty(v), 'string path or null');
183
+ check('acceptance_criteria', strList, 'array of strings');
184
+ check('deadline', (v) => isStr(v) && ISO.test(v), 'ISO8601 UTC string');
185
+ check('fresh_call', isBool, 'boolean');
186
+ for (const f of ['project', 'engine', 'result_path', 'story_title', 'story_description', 'reopen_note', 'repo', 'branch',
187
+ 'prior_handoff']) check(f, isStr, 'string', false);
188
+ check('resumed_after_interrupt', isBool, 'boolean', false);
189
+ check('constraints', strList, 'array of strings', false);
190
+ } else {
191
+ for (const f of ['story_id', 'phase', 'worker_id']) check(f, nonEmpty, 'non-empty string');
192
+ check('status', (v) => v === 'passed' || v === 'failed' || v === 'blocked', 'one of passed|failed|blocked');
193
+ check('summary', isStr, 'string');
194
+ check('files_changed', strList, 'array of strings');
195
+ check('commits', strList, 'array of strings');
196
+ if (!has('back_pressure')) problems.push('missing field: back_pressure');
197
+ else if (!isObj(doc.back_pressure)) problems.push('wrong type: back_pressure (want object)');
198
+ else {
199
+ const bp = doc.back_pressure;
200
+ for (const g of ['tests', 'lint', 'typecheck', 'build']) {
201
+ if (!Object.prototype.hasOwnProperty.call(bp, g)) problems.push('missing field: back_pressure.' + g);
202
+ else if (!['pass', 'fail', 'skip'].includes(bp[g])) problems.push('wrong type: back_pressure.' + g + ' (want one of pass|fail|skip)');
203
+ }
204
+ }
205
+ check('context_for_next', isStr, 'string');
206
+ for (const f of ['engine', 'notes']) check(f, isStr, 'string', false);
207
+ }
208
+ if (problems.length) { for (const p of problems) err(p); process.exit(1); }
209
+ process.stdout.write('valid ' + kind + '\n');
210
+ process.exit(0);
211
+ JS
@@ -8626,6 +8626,40 @@ export declare const COMMAND_CATALOG: readonly [{
8626
8626
  }];
8627
8627
  readonly options: readonly [];
8628
8628
  readonly subcommands: readonly [];
8629
+ }, {
8630
+ readonly name: "conductor";
8631
+ readonly description: "Route and supervise pipeline stories";
8632
+ readonly aliases: readonly [];
8633
+ readonly hidden: false;
8634
+ readonly helpOption: false;
8635
+ readonly helpCommand: false;
8636
+ readonly usage: "[args...]";
8637
+ readonly helpText: readonly [];
8638
+ readonly arguments: readonly [{
8639
+ readonly name: "args";
8640
+ readonly required: false;
8641
+ readonly variadic: true;
8642
+ readonly description: "";
8643
+ }];
8644
+ readonly options: readonly [];
8645
+ readonly subcommands: readonly [];
8646
+ }, {
8647
+ readonly name: "driver";
8648
+ readonly description: "Run the detached pipeline queue driver";
8649
+ readonly aliases: readonly [];
8650
+ readonly hidden: false;
8651
+ readonly helpOption: false;
8652
+ readonly helpCommand: false;
8653
+ readonly usage: "[args...]";
8654
+ readonly helpText: readonly [];
8655
+ readonly arguments: readonly [{
8656
+ readonly name: "args";
8657
+ readonly required: false;
8658
+ readonly variadic: true;
8659
+ readonly description: "";
8660
+ }];
8661
+ readonly options: readonly [];
8662
+ readonly subcommands: readonly [];
8629
8663
  }];
8630
8664
  }, {
8631
8665
  readonly name: "resolve-company";
@@ -10482,6 +10482,46 @@ export const COMMAND_CATALOG = [
10482
10482
  ],
10483
10483
  "options": [],
10484
10484
  "subcommands": []
10485
+ },
10486
+ {
10487
+ "name": "conductor",
10488
+ "description": "Route and supervise pipeline stories",
10489
+ "aliases": [],
10490
+ "hidden": false,
10491
+ "helpOption": false,
10492
+ "helpCommand": false,
10493
+ "usage": "[args...]",
10494
+ "helpText": [],
10495
+ "arguments": [
10496
+ {
10497
+ "name": "args",
10498
+ "required": false,
10499
+ "variadic": true,
10500
+ "description": ""
10501
+ }
10502
+ ],
10503
+ "options": [],
10504
+ "subcommands": []
10505
+ },
10506
+ {
10507
+ "name": "driver",
10508
+ "description": "Run the detached pipeline queue driver",
10509
+ "aliases": [],
10510
+ "hidden": false,
10511
+ "helpOption": false,
10512
+ "helpCommand": false,
10513
+ "usage": "[args...]",
10514
+ "helpText": [],
10515
+ "arguments": [
10516
+ {
10517
+ "name": "args",
10518
+ "required": false,
10519
+ "variadic": true,
10520
+ "description": ""
10521
+ }
10522
+ ],
10523
+ "options": [],
10524
+ "subcommands": []
10485
10525
  }
10486
10526
  ]
10487
10527
  },
@@ -8,6 +8,11 @@
8
8
  */
9
9
  import * as fs from "node:fs";
10
10
  import { Command } from "commander";
11
+ /**
12
+ * A run without a report older than this no longer counts as a crash. A
13
+ * transient failure (a DNS blip, a brief outage) must not pin the choice.
14
+ */
15
+ export declare const CRASHED_RUN_MAX_AGE_MS: number;
11
16
  /**
12
17
  * Wall-clock cap on one sibling run. The supervisor kills the sibling and its
13
18
  * descendants at this point. It stays under the lock TTL so a capped run
@@ -59,11 +64,20 @@ export type WriteStampsOptions = {
59
64
  waitForRetry?: (delayMs: number) => void;
60
65
  };
61
66
  export declare function writeStamps(liveRoot: string, sessionId: string | undefined, options?: WriteStampsOptions): string[];
67
+ /**
68
+ * Backends whose most recent run died without producing a report.
69
+ *
70
+ * A version probe cannot catch a backend that starts cleanly and then fails at
71
+ * runtime — an expired login, a sandbox it cannot write in, a model it is not
72
+ * entitled to. The evidence for that is a finished run directory with no
73
+ * report.md in it, so prefer a different backend for the next checkpoint. Only
74
+ * the newest run per backend counts: one that has since succeeded is healthy.
75
+ */
76
+ export declare function crashedBackends(siblingRoot: string, now?: number): Set<string>;
62
77
  /**
63
78
  * Preference order for `--backend auto`: the caller's own backend first, so a
64
- * claude session keeps using claude and a codex session keeps using codex
65
- * (same-engine continuity), then the fallback priority claude -> codex -> grok
66
- * for whatever the caller is not. Deterministic and env-only, so it is unit
79
+ * claude session keeps using claude, then the fallback priority claude -> grok.
80
+ * Codex is never in this list: it runs only when explicitly requested. Deterministic and env-only, so it is unit
67
81
  * tested directly. `preferGrok` (the company's grok-backend flag) puts grok
68
82
  * ahead of that order and leaves the rest as the fallback.
69
83
  */
@@ -58,7 +58,19 @@ const SIBLING_SETTING = "HQ_CHECKPOINT_AGENT";
58
58
  const GATE_SETTING = "HQ_CHECKPOINT_GATE";
59
59
  const SIBLING_SETTING_OFF = new Set(["0", "false", "off", "no"]);
60
60
  const SIBLING_SETTING_ON = new Set(["1", "true", "on", "yes"]);
61
- const FALLBACK_BACKEND_ORDER = ["claude", "codex", "grok"];
61
+ /**
62
+ * Backends `--backend auto` may pick on its own. Codex is deliberately absent:
63
+ * it runs only when an operator names it (`--backend codex` or
64
+ * HQ_CHECKPOINT_BACKEND=codex).
65
+ */
66
+ const FALLBACK_BACKEND_ORDER = ["claude", "grok"];
67
+ /** Operator setting that replaces `--backend auto` with a named backend. */
68
+ const BACKEND_SETTING = "HQ_CHECKPOINT_BACKEND";
69
+ /**
70
+ * A run without a report older than this no longer counts as a crash. A
71
+ * transient failure (a DNS blip, a brief outage) must not pin the choice.
72
+ */
73
+ export const CRASHED_RUN_MAX_AGE_MS = 6 * 60 * 60 * 1000;
62
74
  /** A backend that cannot answer `--version` this fast is treated as broken. */
63
75
  const BACKEND_PROBE_TIMEOUT_MS = 10_000;
64
76
  /** A sibling still holding the lock after this long is treated as abandoned. */
@@ -626,7 +638,7 @@ function processAlive(pid) {
626
638
  * report.md in it, so prefer a different backend for the next checkpoint. Only
627
639
  * the newest run per backend counts: one that has since succeeded is healthy.
628
640
  */
629
- function crashedBackends(siblingRoot) {
641
+ export function crashedBackends(siblingRoot, now = Date.now()) {
630
642
  const crashed = new Set();
631
643
  let runDirs;
632
644
  try {
@@ -655,6 +667,14 @@ function crashedBackends(siblingRoot) {
655
667
  seen.add(backend);
656
668
  if (fs.existsSync(path.join(absolute, "report.md")))
657
669
  continue;
670
+ // Old failures expire so one transient crash cannot pin the choice.
671
+ try {
672
+ if (now - fs.statSync(path.join(absolute, "payload.json")).mtimeMs > CRASHED_RUN_MAX_AGE_MS)
673
+ continue;
674
+ }
675
+ catch {
676
+ continue;
677
+ }
658
678
  // A missing report only means a crash once the process itself is gone.
659
679
  let pid = Number.NaN;
660
680
  try {
@@ -680,13 +700,13 @@ function crashedBackends(siblingRoot) {
680
700
  */
681
701
  function callerBackend() {
682
702
  const runtime = checkpointGateRuntime();
683
- return runtime === "claude" || runtime === "codex" ? runtime : null;
703
+ // A codex caller does not pull the sibling onto codex: codex is opt-in only.
704
+ return runtime === "claude" ? runtime : null;
684
705
  }
685
706
  /**
686
707
  * Preference order for `--backend auto`: the caller's own backend first, so a
687
- * claude session keeps using claude and a codex session keeps using codex
688
- * (same-engine continuity), then the fallback priority claude -> codex -> grok
689
- * for whatever the caller is not. Deterministic and env-only, so it is unit
708
+ * claude session keeps using claude, then the fallback priority claude -> grok.
709
+ * Codex is never in this list: it runs only when explicitly requested. Deterministic and env-only, so it is unit
690
710
  * tested directly. `preferGrok` (the company's grok-backend flag) puts grok
691
711
  * ahead of that order and leaves the rest as the fallback.
692
712
  */
@@ -807,14 +827,34 @@ function assertKnownBackend(requested) {
807
827
  usage(`checkpoint: unknown backend: ${value}`);
808
828
  return value;
809
829
  }
830
+ /** HQ_CHECKPOINT_BACKEND from the environment, then the HQ settings file. */
831
+ function backendSetting(liveRoot) {
832
+ const fromEnv = process.env[BACKEND_SETTING]?.trim().toLowerCase();
833
+ if (fromEnv)
834
+ return fromEnv;
835
+ const lookup = settingsEnvValue(liveRoot, BACKEND_SETTING);
836
+ return lookup.kind === "value" ? lookup.value.trim().toLowerCase() || undefined : undefined;
837
+ }
810
838
  async function resolveBackend(requested, liveRoot, company) {
811
- const value = assertKnownBackend(requested);
839
+ let value = assertKnownBackend(requested);
840
+ if (value === "auto") {
841
+ const setting = backendSetting(liveRoot);
842
+ if (setting && setting !== "auto") {
843
+ if (BACKENDS.has(setting))
844
+ value = setting;
845
+ else
846
+ printError(`checkpoint: ignoring ${BACKEND_SETTING}=${setting} (expected auto, claude, codex, grok or none)`);
847
+ }
848
+ }
812
849
  // An explicitly named backend is honoured as given; only `auto` shops around.
813
850
  if (value !== "auto")
814
851
  return value;
815
852
  const available = autoBackendPreference(await companyPrefersGrok(liveRoot, company)).filter((name) => backendOnPath(name) && backendResponds(name));
816
- if (available.length === 0)
853
+ if (available.length === 0) {
854
+ printError("checkpoint: sibling skipped: no allowed backend available (claude, grok); " +
855
+ `codex runs only with --backend codex or ${BACKEND_SETTING}=codex`);
817
856
  return "none";
857
+ }
818
858
  const crashed = crashedBackends(path.join(liveRoot, "workspace", "checkpoints", "sibling"));
819
859
  // If every candidate failed last time, still try the preferred one: a stale
820
860
  // failure is a weaker signal than running no maintenance sibling at all.
@@ -6,11 +6,13 @@ import { isByteOrientedLocale } from "../lib/core-utils/byte-oriented-locale.js"
6
6
  export const DERIVE_TRIGGER_FACTS_ASSET = "core/scripts/derive-trigger-facts.sh";
7
7
  const TRANSCRIPT_TAIL_MAX_BYTES = 1_048_576;
8
8
  function effectiveEnvironment(options) {
9
+ const environment = { ...process.env, ...(options.env ?? {}) };
9
10
  return {
10
- ...process.env,
11
- ...(options.env ?? {}),
11
+ ...environment,
12
12
  HQ_ROOT: options.hqRoot,
13
- CLAUDE_PROJECT_DIR: options.hqRoot,
13
+ // HQ_ROOT locates session metadata; the shell still uses its project cwd
14
+ // independently for repo and branch facts.
15
+ CLAUDE_PROJECT_DIR: environment.CLAUDE_PROJECT_DIR || options.cwd,
14
16
  };
15
17
  }
16
18
  class ShellFallbackRequired extends Error {
@@ -457,6 +459,7 @@ function computeOutput(options) {
457
459
  // fact (plus any static session facts); it does not validate the event.
458
460
  const paired = withAssistantIntent && (event === "PreToolUse" || event === "UserPromptSubmit");
459
461
  const env = effectiveEnvironment(options);
462
+ const initialEnv = { ...process.env, ...(options.env ?? {}) };
460
463
  const jqAvailable = options.jqAvailable ?? commandOnPath("jq", env);
461
464
  let payload = {
462
465
  tool: "", command: "", runInBackground: "", toolResponse: "", prompt: "",
@@ -464,8 +467,8 @@ function computeOutput(options) {
464
467
  };
465
468
  if (jqAvailable)
466
469
  payload = parsePayload(options.stdin || "{}");
467
- const payloadCwd = payload.cwd || env.CLAUDE_PROJECT_DIR || options.hqRoot;
468
- const company = resolveCompanyFact(payload.cwd, payload.sessionId, options.hqRoot, options.hqRoot, env);
470
+ const payloadCwd = payload.cwd || env.CLAUDE_PROJECT_DIR || options.cwd;
471
+ const company = resolveCompanyFact(payload.cwd, payload.sessionId, options.hqRoot, options.cwd, env);
469
472
  const derive = (kind) => {
470
473
  const facts = ["always"];
471
474
  let branch = "";
@@ -508,7 +511,12 @@ function computeOutput(options) {
508
511
  }
509
512
  }
510
513
  else {
511
- if (payloadCwd.includes("repos/public/") || payloadCwd.includes("repos/private/"))
514
+ // Bash's PWD is slash-separated on Git Bash even though Node receives a
515
+ // Windows cwd with backslashes. But explicit payload/environment paths
516
+ // reach the shell unchanged, so normalize only the PWD fallback.
517
+ const usesPwdFallback = !payload.cwd && !initialEnv.CLAUDE_PROJECT_DIR;
518
+ const shellCwd = usesPwdFallback ? payloadCwd.replaceAll("\\", "/") : payloadCwd;
519
+ if (shellCwd.includes("repos/public/") || shellCwd.includes("repos/private/"))
512
520
  facts.push("repo");
513
521
  branch = options.gitBranch ?? readBranch(payloadCwd, env);
514
522
  }
@@ -607,6 +615,6 @@ export function runDeriveTriggerFactsFastResult(argv, stdin, cwd, hqRoot) {
607
615
  stderr: result.stderr ?? Buffer.alloc(0),
608
616
  };
609
617
  }
610
- return runDeriveTriggerFacts({ argv, stdin, cwd, hqRoot: hqRoot ?? cwd });
618
+ return runDeriveTriggerFacts({ argv, stdin, cwd, hqRoot: hqRoot ?? (process.env.HQ_ROOT?.trim() || cwd) });
611
619
  }
612
620
  //# sourceMappingURL=core-derive-trigger-facts-native.js.map
@@ -245,7 +245,9 @@ export function runMigratePolicyTriggers(args, deps) {
245
245
  if (bypass)
246
246
  return false;
247
247
  try {
248
- const last = fileSystem.readFileSync(stamp, "utf8");
248
+ // Bash command substitution strips trailing LF bytes before validating
249
+ // the decimal value. The shell writes one newline in every stamp.
250
+ const last = fileSystem.readFileSync(stamp, "utf8").replace(/\n+$/, "");
249
251
  if (!/^\d+$/.test(last)) {
250
252
  stderr += `migrate-policy-triggers: cooldown stamp is unreadable; running\n`;
251
253
  return false;
@@ -0,0 +1,5 @@
1
+ import type { Command } from "commander";
2
+ export declare const PIPELINE_CONDUCTOR_ASSET = "core/scripts/pipeline-conductor.sh";
3
+ export declare const PIPELINE_DRIVER_ASSET = "core/scripts/pipeline-driver.sh";
4
+ export declare function registerCorePipelineRuntimeCommands(core: Command): void;
5
+ //# sourceMappingURL=core-pipeline-conductor-command.d.ts.map