@indigoai-us/hq-cli 5.345.123 → 5.345.124
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 +14 -0
- package/assets/scaffold/core/scripts/work-mesh-project-registration-offer.sh +77 -0
- package/assets/scaffold/core/scripts/workflow-gate.sh +247 -0
- package/dist/command-catalog.generated.d.ts +239 -0
- package/dist/command-catalog.generated.js +291 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.d.ts +7 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.js +147 -0
- package/dist/commands/core-bucket-c-workflow-gate-fast.d.ts +3 -0
- package/dist/commands/core-bucket-c-workflow-gate-fast.js +17 -0
- package/dist/commands/core-bucket-c-workflow-gate-native.d.ts +7 -0
- package/dist/commands/core-bucket-c-workflow-gate-native.js +250 -0
- package/dist/commands/core.js +12 -0
- package/dist/commands/mesh.js +457 -16
- package/dist/fast-scaffold.js +26 -0
- package/dist/lib/lanes/quota-notice.d.ts +24 -0
- package/dist/lib/lanes/quota-notice.js +61 -0
- package/dist/lib/lanes/quota.d.ts +44 -8
- package/dist/lib/lanes/quota.js +187 -45
- package/dist/lib/lanes/reconcile.js +17 -1
- package/dist/lib/lanes/watcher.js +13 -1
- package/dist/lib/mesh/api.d.ts +2 -0
- package/dist/lib/mesh/api.js +3 -0
- package/dist/lib/mesh/client.d.ts +6 -1
- package/dist/lib/mesh/client.js +18 -5
- package/dist/lib/mesh/team-coordination.d.ts +106 -0
- package/dist/lib/mesh/team-coordination.js +297 -0
- package/dist/lib/work-context/auto-bind.d.ts +75 -0
- package/dist/lib/work-context/auto-bind.js +245 -0
- package/package.json +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
const ok = (stdout = "") => ({ code: 0, stdout, stderr: "" });
|
|
4
|
+
const fail = (message, code = 1) => ({ code, stdout: "", stderr: `${message}\n` });
|
|
5
|
+
const validId = /^[a-z0-9._-]+$/;
|
|
6
|
+
const USAGE = `workflow-gate.sh — inspect and answer human gates opened by codex-workflow.mjs gate().
|
|
7
|
+
|
|
8
|
+
A gate is a self-contained pending question file written by a paused workflow
|
|
9
|
+
run; writing a matching answered/<id>.json resumes that run in place. Answers
|
|
10
|
+
are durable: a re-launched run returns them instantly instead of re-asking
|
|
11
|
+
the human, so never delete answered/ files while their project is live.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
workflow-gate.sh list
|
|
15
|
+
workflow-gate.sh show <id>
|
|
16
|
+
workflow-gate.sh answer <id> <choice|N> [--notes "..."] [--by name] [--force] [--freeform]
|
|
17
|
+
<choice|N> an option label, or a 1-based option index
|
|
18
|
+
--notes free-text note stored alongside the choice
|
|
19
|
+
--by who answered (recorded as answered_by)
|
|
20
|
+
--force pre-answer a gate that has not opened yet, or overwrite an
|
|
21
|
+
existing answer
|
|
22
|
+
--freeform accept a choice that is not one of the gate's options
|
|
23
|
+
workflow-gate.sh wait-pending [--timeout secs]
|
|
24
|
+
Blocks until any pending gate exists (exit 0) or the timeout elapses
|
|
25
|
+
(exit 1). Monitor-friendly wake condition for watching sessions.
|
|
26
|
+
workflow-gate.sh watch-run <run-dir> [--timeout secs]
|
|
27
|
+
Run-scoped wake condition for the session that launched a workflow.
|
|
28
|
+
Blocks until a gate belonging to THAT run opens (prints its id, exit 0),
|
|
29
|
+
the run finishes (exit 3), or --timeout elapses (exit 1). Match is on
|
|
30
|
+
the run_dir every gate carries, which is the --run-dir the launching
|
|
31
|
+
session chose — no id parsing, no env coupling, no startup race.
|
|
32
|
+
workflow-gate.sh clear <id>
|
|
33
|
+
Remove both the pending and answered files for a gate id.
|
|
34
|
+
|
|
35
|
+
Env: HQ_WORKFLOW_GATES_DIR overrides the gates root (default
|
|
36
|
+
<hq-root>/workspace/gates); legacy CODEX_WORKFLOW_GATES_DIR is honored.
|
|
37
|
+
`;
|
|
38
|
+
function gateId(id) {
|
|
39
|
+
return !validId.test(id) ? fail(`workflow-gate: invalid gate id "${id}" — ids are lowercase [a-z0-9._-]`) : undefined;
|
|
40
|
+
}
|
|
41
|
+
function readJson(file) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function listFiles(dir) {
|
|
50
|
+
try {
|
|
51
|
+
return fs.readdirSync(dir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => path.join(dir, entry));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function parseTimeout(args) {
|
|
58
|
+
let timeout;
|
|
59
|
+
for (let index = 0; index < args.length;) {
|
|
60
|
+
if (args[index] !== "--timeout")
|
|
61
|
+
return { error: `workflow-gate: unknown flag: ${args[index]}` };
|
|
62
|
+
if (index + 1 >= args.length)
|
|
63
|
+
return { error: "workflow-gate: --timeout needs a value" };
|
|
64
|
+
const raw = args[index + 1];
|
|
65
|
+
if (!/^\d+$/.test(raw))
|
|
66
|
+
return { error: "workflow-gate: --timeout must be a whole number of seconds" };
|
|
67
|
+
timeout = Number(raw);
|
|
68
|
+
index += 2;
|
|
69
|
+
}
|
|
70
|
+
return { ...(timeout === undefined ? {} : { timeout }) };
|
|
71
|
+
}
|
|
72
|
+
const waitSecond = () => new Promise((resolve) => setTimeout(resolve, 1000));
|
|
73
|
+
export async function runWorkflowGate(args, root, env = process.env, now = Date.now, sleep = waitSecond) {
|
|
74
|
+
const gatesDir = env.HQ_WORKFLOW_GATES_DIR || env.CODEX_WORKFLOW_GATES_DIR || path.join(root, "workspace/gates");
|
|
75
|
+
const pending = path.join(gatesDir, "pending");
|
|
76
|
+
const answered = path.join(gatesDir, "answered");
|
|
77
|
+
const [command, ...rest] = args;
|
|
78
|
+
if (!command)
|
|
79
|
+
return { code: 1, stdout: `${USAGE}\n`, stderr: "" };
|
|
80
|
+
if (command === "-h" || command === "--help" || command === "help")
|
|
81
|
+
return ok(`${USAGE}\n`);
|
|
82
|
+
if (command === "list") {
|
|
83
|
+
const files = listFiles(pending);
|
|
84
|
+
if (!files.length)
|
|
85
|
+
return ok("no pending gates\n");
|
|
86
|
+
return ok(files.map((file) => {
|
|
87
|
+
const gate = readJson(file);
|
|
88
|
+
if (!gate || typeof gate !== "object")
|
|
89
|
+
return `<unreadable> ${file}`;
|
|
90
|
+
const created = Date.parse(String(gate.created_at || 0));
|
|
91
|
+
const ageMin = Math.max(0, Math.round((now() - created) / 60000));
|
|
92
|
+
const options = Array.isArray(gate.options) ? gate.options.map((option, index) => `${index + 1}=${String(option?.label)}`).join(" ") : "";
|
|
93
|
+
return `${String(gate.id)} (${ageMin}m) ${String(gate.question)}${options ? ` [${options}]` : ""}`;
|
|
94
|
+
}).join("\n") + "\n");
|
|
95
|
+
}
|
|
96
|
+
if (command === "show") {
|
|
97
|
+
const id = rest[0] ?? "";
|
|
98
|
+
if (!id)
|
|
99
|
+
return fail("workflow-gate: usage: show <id>");
|
|
100
|
+
const invalid = gateId(id);
|
|
101
|
+
if (invalid)
|
|
102
|
+
return invalid;
|
|
103
|
+
const file = [path.join(pending, `${id}.json`), path.join(answered, `${id}.json`)].find((candidate) => {
|
|
104
|
+
try {
|
|
105
|
+
return fs.statSync(candidate).isFile();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (!file)
|
|
112
|
+
return fail(`workflow-gate: no gate "${id}" in pending or answered`);
|
|
113
|
+
try {
|
|
114
|
+
return ok(fs.readFileSync(file, "utf8"));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return fail(`workflow-gate: no gate "${id}" in pending or answered`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (command === "answer") {
|
|
121
|
+
const [id = "", rawChoice = "", ...flags] = rest;
|
|
122
|
+
if (!id || !rawChoice)
|
|
123
|
+
return fail("workflow-gate: usage: answer <id> <choice|N> [--notes ...] [--by name] [--force] [--freeform]");
|
|
124
|
+
const invalid = gateId(id);
|
|
125
|
+
if (invalid)
|
|
126
|
+
return invalid;
|
|
127
|
+
let notes = "";
|
|
128
|
+
let by = "";
|
|
129
|
+
let force = false;
|
|
130
|
+
let freeform = false;
|
|
131
|
+
for (let index = 0; index < flags.length;) {
|
|
132
|
+
const flag = flags[index];
|
|
133
|
+
if (flag === "--force") {
|
|
134
|
+
force = true;
|
|
135
|
+
index += 1;
|
|
136
|
+
}
|
|
137
|
+
else if (flag === "--freeform") {
|
|
138
|
+
freeform = true;
|
|
139
|
+
index += 1;
|
|
140
|
+
}
|
|
141
|
+
else if (flag === "--notes" || flag === "--by") {
|
|
142
|
+
if (index + 1 >= flags.length)
|
|
143
|
+
return fail(`workflow-gate: ${flag} needs a value`);
|
|
144
|
+
if (flag === "--notes")
|
|
145
|
+
notes = flags[index + 1];
|
|
146
|
+
else
|
|
147
|
+
by = flags[index + 1];
|
|
148
|
+
index += 2;
|
|
149
|
+
}
|
|
150
|
+
else
|
|
151
|
+
return fail(`workflow-gate: unknown flag: ${flag}`);
|
|
152
|
+
}
|
|
153
|
+
const pendingFile = path.join(pending, `${id}.json`);
|
|
154
|
+
const answerFile = path.join(answered, `${id}.json`);
|
|
155
|
+
const gate = readJson(pendingFile);
|
|
156
|
+
if (!gate && !force) {
|
|
157
|
+
const ids = listFiles(pending).map((file) => path.basename(file, ".json"));
|
|
158
|
+
return fail(`workflow-gate: no pending gate "${id}"${ids.length ? ` — pending: ${ids.join(", ")}` : " — nothing is pending"} (use --force to pre-answer)`);
|
|
159
|
+
}
|
|
160
|
+
let choice = rawChoice;
|
|
161
|
+
const options = gate && Array.isArray(gate.options) ? gate.options : [];
|
|
162
|
+
if (options.length) {
|
|
163
|
+
if (/^\d+$/.test(rawChoice)) {
|
|
164
|
+
const number = Number(rawChoice);
|
|
165
|
+
if (number >= 1 && number <= options.length)
|
|
166
|
+
choice = String(options[number - 1].label);
|
|
167
|
+
else
|
|
168
|
+
return fail(`workflow-gate: index ${number} is out of range 1..${options.length} for "${id}"`);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const hit = options.find((option) => String(option.label).toLowerCase() === rawChoice.toLowerCase());
|
|
172
|
+
if (hit)
|
|
173
|
+
choice = String(hit.label);
|
|
174
|
+
else if (!freeform)
|
|
175
|
+
return fail(`workflow-gate: "${rawChoice}" is not an option for "${id}" — options: ${options.map((option) => option.label).join(", ")} (use --freeform to record it anyway)`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (fs.existsSync(answerFile) && !force)
|
|
179
|
+
return fail(`workflow-gate: gate "${id}" is already answered (${answerFile}) — use --force to overwrite`);
|
|
180
|
+
try {
|
|
181
|
+
fs.mkdirSync(answered, { recursive: true });
|
|
182
|
+
const temporary = `${answerFile}.tmp-${process.pid}`;
|
|
183
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ id, choice, ...(notes ? { notes } : {}), ...(by ? { answered_by: by } : {}), answered_at: new Date(now()).toISOString() }, null, 2)}\n`);
|
|
184
|
+
fs.renameSync(temporary, answerFile);
|
|
185
|
+
try {
|
|
186
|
+
fs.rmSync(pendingFile, { force: true });
|
|
187
|
+
}
|
|
188
|
+
catch { /* shell is best-effort here */ }
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
return fail(`workflow-gate: ${String(error.message)}`);
|
|
192
|
+
}
|
|
193
|
+
return ok(`answered ${id} -> ${choice}\n`);
|
|
194
|
+
}
|
|
195
|
+
if (command === "wait-pending" || command === "watch-run") {
|
|
196
|
+
let runDir = "";
|
|
197
|
+
let optionArgs = rest;
|
|
198
|
+
if (command === "watch-run") {
|
|
199
|
+
runDir = rest[0] ?? "";
|
|
200
|
+
if (!runDir || runDir.startsWith("-"))
|
|
201
|
+
return fail("workflow-gate: usage: watch-run <run-dir> [--timeout secs]");
|
|
202
|
+
optionArgs = rest.slice(1);
|
|
203
|
+
}
|
|
204
|
+
const parsed = parseTimeout(optionArgs);
|
|
205
|
+
if (parsed.error)
|
|
206
|
+
return fail(parsed.error);
|
|
207
|
+
let elapsed = 0;
|
|
208
|
+
for (;;) {
|
|
209
|
+
const files = listFiles(pending);
|
|
210
|
+
if (command === "wait-pending" && files.length)
|
|
211
|
+
return ok();
|
|
212
|
+
if (command === "watch-run") {
|
|
213
|
+
const wanted = path.resolve(runDir);
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
const gate = readJson(file);
|
|
216
|
+
if (gate?.run_dir && path.resolve(String(gate.run_dir)) === wanted)
|
|
217
|
+
return ok(`${String(gate.id || path.basename(file, ".json"))}\n`);
|
|
218
|
+
}
|
|
219
|
+
const journal = path.join(runDir, "journal.jsonl");
|
|
220
|
+
try {
|
|
221
|
+
if (fs.readFileSync(journal, "utf8").includes('"event":"run-done"'))
|
|
222
|
+
return { code: 3, stdout: "", stderr: "" };
|
|
223
|
+
}
|
|
224
|
+
catch { /* not finished */ }
|
|
225
|
+
}
|
|
226
|
+
if (parsed.timeout !== undefined && elapsed >= parsed.timeout)
|
|
227
|
+
return { code: 1, stdout: "", stderr: "" };
|
|
228
|
+
await sleep();
|
|
229
|
+
elapsed += 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (command === "clear") {
|
|
233
|
+
const id = rest[0] ?? "";
|
|
234
|
+
if (!id)
|
|
235
|
+
return fail("workflow-gate: usage: clear <id>");
|
|
236
|
+
const invalid = gateId(id);
|
|
237
|
+
if (invalid)
|
|
238
|
+
return invalid;
|
|
239
|
+
try {
|
|
240
|
+
fs.rmSync(path.join(pending, `${id}.json`), { force: true });
|
|
241
|
+
fs.rmSync(path.join(answered, `${id}.json`), { force: true });
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return fail(`workflow-gate: ${String(error.message)}`);
|
|
245
|
+
}
|
|
246
|
+
return ok(`cleared ${id}\n`);
|
|
247
|
+
}
|
|
248
|
+
return fail(`workflow-gate: unknown command: ${command} (list | show | answer | wait-pending | watch-run | clear)`);
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=core-bucket-c-workflow-gate-native.js.map
|
package/dist/commands/core.js
CHANGED
|
@@ -248,6 +248,18 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
248
248
|
root: "live",
|
|
249
249
|
summary: "Check whether a company-scoped skill is installed",
|
|
250
250
|
},
|
|
251
|
+
{
|
|
252
|
+
name: "work-mesh-project-registration-offer",
|
|
253
|
+
asset: "core/scripts/work-mesh-project-registration-offer.sh",
|
|
254
|
+
root: "live",
|
|
255
|
+
summary: "Check and record the work-mesh project registration offer",
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "workflow-gate",
|
|
259
|
+
asset: "core/scripts/workflow-gate.sh",
|
|
260
|
+
root: "live",
|
|
261
|
+
summary: "Inspect, answer, and wait for workflow gates",
|
|
262
|
+
},
|
|
251
263
|
// Session/harness plumbing relocated out of the HQ tree so a single published
|
|
252
264
|
// CLI owns it. Both are `root: "live"`: they read and write the live HQ tree
|
|
253
265
|
// (workspace/sessions, companies/, workspace/orchestrator/hook-state), so the
|