@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.
- package/CHANGELOG.md +18 -0
- package/assets/scaffold/core/scripts/pipeline-conductor.sh +2671 -0
- package/assets/scaffold/core/scripts/pipeline-driver.sh +820 -0
- package/assets/scaffold/core/scripts/pipeline-envelope.sh +211 -0
- package/dist/command-catalog.generated.d.ts +34 -0
- package/dist/command-catalog.generated.js +40 -0
- package/dist/commands/core-checkpoint-native.d.ts +17 -3
- package/dist/commands/core-checkpoint-native.js +48 -8
- package/dist/commands/core-derive-trigger-facts-native.js +15 -7
- package/dist/commands/core-migrate-policy-triggers-native.js +3 -1
- package/dist/commands/core-pipeline-conductor-command.d.ts +5 -0
- package/dist/commands/core-pipeline-conductor-command.js +138 -0
- package/dist/commands/core-pipeline-conductor-native.d.ts +7 -0
- package/dist/commands/core-pipeline-conductor-native.js +2950 -0
- package/dist/commands/core-pipeline-driver-native.d.ts +24 -0
- package/dist/commands/core-pipeline-driver-native.js +843 -0
- package/dist/commands/core-pipeline-driver-process.d.ts +4 -0
- package/dist/commands/core-pipeline-driver-process.js +68 -0
- package/dist/commands/core-pipeline-envelope-native.d.ts +4 -0
- package/dist/commands/core-pipeline-envelope-native.js +87 -0
- package/dist/commands/core-policy.js +3 -0
- package/dist/commands/core-worker-lint-native.d.ts +0 -1
- package/dist/commands/core-worker-lint-native.js +0 -1
- package/dist/commands/core-worker-share-native.js +2 -2
- package/dist/commands/core-worker-skills-command.d.ts +1 -1
- package/dist/commands/core-worker-skills-command.js +3 -20
- package/dist/commands/core.d.ts +5 -1
- package/dist/commands/core.js +8 -9
- package/dist/commands/integrations-core.d.ts +7 -1
- package/dist/commands/integrations-core.js +17 -4
- package/dist/commands/integrations.js +91 -22
- package/dist/commands/storage-gate.d.ts +4 -3
- package/dist/commands/storage-gate.js +4 -3
- package/dist/fast-scaffold.js +0 -2
- package/dist/lib/storage-feature-flag.d.ts +7 -7
- package/package.json +1 -1
- package/assets/scaffold/core/scripts/lint-shared-worker-skills.sh +0 -197
- package/assets/scaffold/core/scripts/share-worker-skill.sh +0 -178
- package/dist/commands/core-native-worker-skills-flag.d.ts +0 -4
- 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
|
|
65
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
688
|
-
*
|
|
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
|
-
|
|
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
|
-
...
|
|
11
|
-
...(options.env ?? {}),
|
|
11
|
+
...environment,
|
|
12
12
|
HQ_ROOT: options.hqRoot,
|
|
13
|
-
|
|
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.
|
|
468
|
-
const company = resolveCompanyFact(payload.cwd, payload.sessionId, options.hqRoot, options.
|
|
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
|
-
|
|
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
|
-
|
|
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
|