@indigoai-us/hq-cli 5.345.105 → 5.345.107
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 +23 -0
- package/assets/scaffold/core/scripts/hq-task.mjs +259 -0
- package/assets/scaffold/core/scripts/hq-task.sh +31 -0
- package/assets/scaffold/core/scripts/pipeline-worker-table.sh +285 -0
- package/dist/command-catalog.generated.d.ts +135 -5
- package/dist/command-catalog.generated.js +161 -5
- package/dist/commands/auth.js +42 -0
- package/dist/commands/core-hq-task-fast.d.ts +4 -0
- package/dist/commands/core-hq-task-fast.js +11 -0
- package/dist/commands/core-hq-task-native.d.ts +20 -0
- package/dist/commands/core-hq-task-native.js +275 -0
- package/dist/commands/core-native-bucket-c-flag.d.ts +3 -0
- package/dist/commands/core-native-bucket-c-flag.js +6 -0
- package/dist/commands/core-native-family-flag.d.ts +0 -2
- package/dist/commands/core-native-family-flag.js +0 -4
- package/dist/commands/core-native-pipeline-flag.d.ts +3 -0
- package/dist/commands/core-native-pipeline-flag.js +6 -0
- package/dist/commands/core-ontology-native.js +24 -30
- package/dist/commands/core-pipeline-workers-command.d.ts +3 -0
- package/dist/commands/core-pipeline-workers-command.js +37 -0
- package/dist/commands/core-pipeline-workers-native.d.ts +13 -0
- package/dist/commands/core-pipeline-workers-native.js +361 -0
- package/dist/commands/core.js +12 -10
- package/dist/commands/lanes.js +32 -7
- package/dist/commands/logout.d.ts +6 -1
- package/dist/commands/logout.js +18 -2
- package/dist/commands/outposts.d.ts +5 -0
- package/dist/commands/outposts.js +5 -0
- package/dist/fast-scaffold.d.ts +14 -1
- package/dist/fast-scaffold.js +149 -5
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +56937 -64550
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +15 -1
- package/dist/lib/daemon/remote-control-pools.d.ts +2 -2
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.d.ts +33 -0
- package/dist/lib/lanes/remote-control-lane.js +276 -0
- package/dist/lib/lanes/spawn.d.ts +5 -0
- package/dist/lib/lanes/spawn.js +65 -14
- package/dist/lib/lanes/store.js +7 -0
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.js +8 -1
- package/dist/utils/cognito-config.d.ts +5 -0
- package/dist/utils/cognito-config.js +14 -0
- package/dist/utils/cognito-revoke.d.ts +33 -0
- package/dist/utils/cognito-revoke.js +42 -0
- package/dist/utils/cognito-session.d.ts +12 -4
- package/dist/utils/cognito-session.js +42 -9
- package/dist/utils/token-keychain.d.ts +96 -0
- package/dist/utils/token-keychain.js +218 -0
- package/package.json +2 -2
- package/assets/scaffold/core/scripts/ontology-audience-grant.sh +0 -81
- package/assets/scaffold/core/scripts/ontology-brief.mjs +0 -93
- package/assets/scaffold/core/scripts/ontology-candidate.sh +0 -100
- package/assets/scaffold/core/scripts/ontology-garden.mjs +0 -327
- package/assets/scaffold/core/scripts/ontology-source-worklist.sh +0 -226
- package/assets/scaffold/core/scripts/source-yaml-validate.sh +0 -36
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.107] — 2026-10-13
|
|
6
|
+
- Company Outposts: `hq outposts provision --company <slug>`, `hq outposts status --company <slug>` and `hq outposts jobs register` are available (hq-cloud 6.18.66). Only company admins can run them, the price and paying company are shown before anything is created, and nothing is provisioned without `--yes`.
|
|
7
|
+
- Agent processes on one box share one cached Cognito sign-in again instead of each signing in separately (hq-cloud 6.18.66 fix).
|
|
8
|
+
- Add native `hq core hq-task` behind the `core-native-bucket-c` family gate, with its pinned shell rollback assets.
|
|
9
|
+
|
|
10
|
+
- Remove the six bundled ontology shell fallbacks and pin their 189 shell cases as native differential fixtures.
|
|
11
|
+
|
|
12
|
+
- On macOS, hq now reads your HQ sign-in from the macOS Keychain copy kept
|
|
13
|
+
by the desktop app, and uses whichever of the Keychain and
|
|
14
|
+
`~/.hq/cognito-tokens.json` is newer. The first time, hq says that macOS
|
|
15
|
+
will ask once for access; choose Always Allow. If you deny it, hq keeps
|
|
16
|
+
using the file and won't ask again; run `hq auth keychain --retry` to
|
|
17
|
+
allow it later. Other platforms still use the file only.
|
|
18
|
+
- `hq lanes create --remote-control` now opens a Claude lane through Remote Control using account A for the remote session and a ready daemon pool from account B, C, or D for inference. Remote Control lanes disable `AskUserQuestion` and resume the same Claude session with `hq lanes resume`.
|
|
19
|
+
## [5.345.106] — 2026-10-12
|
|
20
|
+
- Add `hq core pipeline workers` as a native TypeScript worker-table command, with a bundled shell companion controlled by the `core-native-pipeline` flag.
|
|
21
|
+
|
|
22
|
+
- On Windows, `hq core` converts Git Bash paths for commands that declare path operands. Direct calls use the same path rules as the former script wrappers.
|
|
23
|
+
- `hq logout` now ends your session on the HQ server as well as on this
|
|
24
|
+
device, so a copied token file stops working. If the server can't be
|
|
25
|
+
reached, logout still signs you out locally and says the old session will
|
|
26
|
+
expire on its own.
|
|
27
|
+
|
|
5
28
|
## [5.345.105] — 2026-10-12
|
|
6
29
|
- Session-start health no longer files leftover company journals that this
|
|
7
30
|
repair pass did not refresh, including older hooks that only read
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hq-task.mjs — the global task list on an HQ board.
|
|
3
|
+
//
|
|
4
|
+
// Tasks live in `tasks[]` on a board file, not inside a project. Most tasks
|
|
5
|
+
// never belong to a project, and a `prd.json` is the wrong shape for one: it
|
|
6
|
+
// carries branchName, e2eTests, files and dependsOn, none of which mean
|
|
7
|
+
// anything for "renew the registration".
|
|
8
|
+
//
|
|
9
|
+
// personal/board.json the owner's own board (default)
|
|
10
|
+
// companies/<slug>/board.json a company board (--company <slug>)
|
|
11
|
+
//
|
|
12
|
+
// See core/knowledge/public/hq-core/goals-and-tasks-board.md for the concept.
|
|
13
|
+
//
|
|
14
|
+
// Node rather than python3: HQ's script layer must run on Windows machines
|
|
15
|
+
// where python3 is absent or is a Store-alias stub that fails every call
|
|
16
|
+
// (core/scripts/tests/hooks-no-python.test.sh).
|
|
17
|
+
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import process from "node:process";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const STATUSES = ["open", "blocked", "done"];
|
|
24
|
+
|
|
25
|
+
const USAGE = `hq-task — the global task list on an HQ board
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
hq-task.sh list [--company <slug>] [--all] [--goal <objective-id>]
|
|
29
|
+
hq-task.sh add [--company <slug>] --title "<short title>"
|
|
30
|
+
[--description "<context>"]
|
|
31
|
+
[--criteria "<one done-criterion>"]...
|
|
32
|
+
[--contact "Name <email>|role"]...
|
|
33
|
+
[--goal <objective-id>] [--priority <1-3>]
|
|
34
|
+
hq-task.sh done [--company <slug>] --id T-002
|
|
35
|
+
hq-task.sh block [--company <slug>] --id T-002 --reason "<what is in the way>"
|
|
36
|
+
hq-task.sh reopen [--company <slug>] --id T-002
|
|
37
|
+
hq-task.sh goals [--company <slug>]
|
|
38
|
+
|
|
39
|
+
Default board: personal/board.json. With --company: companies/<slug>/board.json.`;
|
|
40
|
+
|
|
41
|
+
function hqRoot() {
|
|
42
|
+
if (process.env.HQ_ROOT) return process.env.HQ_ROOT;
|
|
43
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function die(message) {
|
|
47
|
+
process.stderr.write(`${message}\n`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseArgs(argv) {
|
|
52
|
+
const repeatable = new Set(["criteria", "contact"]);
|
|
53
|
+
const bare = new Set(["all"]);
|
|
54
|
+
const flags = {};
|
|
55
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
56
|
+
const token = argv[i];
|
|
57
|
+
if (!token.startsWith("--")) die(`Unexpected argument: ${token}\n\n${USAGE}`);
|
|
58
|
+
const key = token.slice(2);
|
|
59
|
+
if (bare.has(key)) {
|
|
60
|
+
flags[key] = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const value = argv[i + 1];
|
|
64
|
+
if (value === undefined || value.startsWith("--")) {
|
|
65
|
+
die(`Flag --${key} needs a value.\n\n${USAGE}`);
|
|
66
|
+
}
|
|
67
|
+
i += 1;
|
|
68
|
+
if (repeatable.has(key)) (flags[key] ||= []).push(value);
|
|
69
|
+
else flags[key] = value;
|
|
70
|
+
}
|
|
71
|
+
return flags;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The owner's own scope is the `personal/` overlay at the HQ root, NOT
|
|
75
|
+
// `companies/personal/`. The personal vault's allowlist syncs `personal/`
|
|
76
|
+
// paths, so a board kept there is readable by an agent running under the
|
|
77
|
+
// owner's identity; the reserved company scope is absent from that vault.
|
|
78
|
+
function boardPath(flags) {
|
|
79
|
+
const root = hqRoot();
|
|
80
|
+
return flags.company
|
|
81
|
+
? path.join(root, "companies", flags.company, "board.json")
|
|
82
|
+
: path.join(root, "personal", "board.json");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function load(flags) {
|
|
86
|
+
const file = boardPath(flags);
|
|
87
|
+
if (!fs.existsSync(file)) die(`No board at ${file}`);
|
|
88
|
+
const board = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
89
|
+
board.tasks ||= [];
|
|
90
|
+
return { file, board };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function save(file, board) {
|
|
94
|
+
board.updated_at = new Date().toISOString().replace(/\.\d+Z$/, "Z");
|
|
95
|
+
fs.writeFileSync(file, `${JSON.stringify(board, null, 2)}\n`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function nextId(tasks) {
|
|
99
|
+
let highest = 0;
|
|
100
|
+
for (const task of tasks) {
|
|
101
|
+
const match = /^T-(\d+)$/.exec(task.id || "");
|
|
102
|
+
if (match) highest = Math.max(highest, Number(match[1]));
|
|
103
|
+
}
|
|
104
|
+
return `T-${String(highest + 1).padStart(3, "0")}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Accept "Name <email>|role" with the email and role both optional.
|
|
108
|
+
function parseContact(raw) {
|
|
109
|
+
let rest = raw;
|
|
110
|
+
let role = null;
|
|
111
|
+
const bar = rest.indexOf("|");
|
|
112
|
+
if (bar !== -1) {
|
|
113
|
+
role = rest.slice(bar + 1).trim();
|
|
114
|
+
rest = rest.slice(0, bar).trim();
|
|
115
|
+
}
|
|
116
|
+
let email = null;
|
|
117
|
+
const angle = /<([^>]+)>/.exec(rest);
|
|
118
|
+
if (angle) {
|
|
119
|
+
email = angle[1].trim();
|
|
120
|
+
rest = rest.slice(0, angle.index).trim();
|
|
121
|
+
}
|
|
122
|
+
const contact = { name: rest };
|
|
123
|
+
if (email) contact.email = email;
|
|
124
|
+
if (role) contact.role = role;
|
|
125
|
+
return contact;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const today = () => new Date().toISOString().slice(0, 10);
|
|
129
|
+
|
|
130
|
+
function findTask(board, id) {
|
|
131
|
+
const task = board.tasks.find((t) => t.id === id);
|
|
132
|
+
if (!task) die(`No task ${id} on that board.`);
|
|
133
|
+
return task;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function goalTitle(board, id) {
|
|
137
|
+
const objective = (board.objectives || []).find((o) => o.id === id);
|
|
138
|
+
return objective ? objective.title : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const MARK = { open: " ", blocked: "!", done: "x" };
|
|
142
|
+
|
|
143
|
+
function cmdList(flags) {
|
|
144
|
+
const { board } = load(flags);
|
|
145
|
+
let tasks = board.tasks.filter((t) => flags.all || t.status !== "done");
|
|
146
|
+
if (flags.goal) tasks = tasks.filter((t) => t.objective_id === flags.goal);
|
|
147
|
+
if (tasks.length === 0) {
|
|
148
|
+
process.stdout.write("No open tasks.\n");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
process.stdout.write(
|
|
152
|
+
`${flags.company || "personal"} — ${flags.all ? "all tasks" : "open tasks"} (${tasks.length})\n\n`,
|
|
153
|
+
);
|
|
154
|
+
tasks.sort(
|
|
155
|
+
(a, b) =>
|
|
156
|
+
(a.priority ?? 99) - (b.priority ?? 99) ||
|
|
157
|
+
String(a.id).localeCompare(String(b.id)),
|
|
158
|
+
);
|
|
159
|
+
for (const task of tasks) {
|
|
160
|
+
process.stdout.write(
|
|
161
|
+
` [${MARK[task.status] ?? " "}] ${task.id} ${task.title || ""}\n`,
|
|
162
|
+
);
|
|
163
|
+
const goal = task.objective_id && goalTitle(board, task.objective_id);
|
|
164
|
+
if (goal) process.stdout.write(` goal: ${goal}\n`);
|
|
165
|
+
if (task.status === "blocked" && task.blockedReason) {
|
|
166
|
+
process.stdout.write(` blocked: ${task.blockedReason}\n`);
|
|
167
|
+
}
|
|
168
|
+
for (const contact of task.contacts || []) {
|
|
169
|
+
const trail = [contact.email, contact.phone].filter(Boolean).join(" · ");
|
|
170
|
+
process.stdout.write(
|
|
171
|
+
` ${contact.name}${trail ? ` — ${trail}` : ""}\n`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function cmdGoals(flags) {
|
|
178
|
+
const { board } = load(flags);
|
|
179
|
+
const objectives = board.objectives || [];
|
|
180
|
+
if (objectives.length === 0) {
|
|
181
|
+
process.stdout.write("No goals on that board.\n");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
for (const objective of objectives) {
|
|
185
|
+
const open = board.tasks.filter(
|
|
186
|
+
(t) => t.objective_id === objective.id && t.status !== "done",
|
|
187
|
+
).length;
|
|
188
|
+
process.stdout.write(
|
|
189
|
+
`${objective.id} ${objective.title} (${open} open task${open === 1 ? "" : "s"})\n`,
|
|
190
|
+
);
|
|
191
|
+
for (const kr of objective.key_results || []) {
|
|
192
|
+
process.stdout.write(
|
|
193
|
+
` ${kr.title} — ${kr.current ?? 0}/${kr.target} ${kr.unit || ""}\n`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function cmdAdd(flags) {
|
|
200
|
+
if (!flags.title) die(`add needs --title.\n\n${USAGE}`);
|
|
201
|
+
const { file, board } = load(flags);
|
|
202
|
+
if (flags.goal && !goalTitle(board, flags.goal)) {
|
|
203
|
+
die(`No goal ${flags.goal} on that board. Run 'hq-task.sh goals' to list them.`);
|
|
204
|
+
}
|
|
205
|
+
const task = {
|
|
206
|
+
id: nextId(board.tasks),
|
|
207
|
+
title: flags.title,
|
|
208
|
+
description: flags.description || flags.title,
|
|
209
|
+
status: "open",
|
|
210
|
+
priority: flags.priority ? Number(flags.priority) : 2,
|
|
211
|
+
objective_id: flags.goal || null,
|
|
212
|
+
criteria: flags.criteria || [`${flags.title} — completed.`],
|
|
213
|
+
contacts: (flags.contact || []).map(parseContact),
|
|
214
|
+
createdAt: today(),
|
|
215
|
+
};
|
|
216
|
+
board.tasks.push(task);
|
|
217
|
+
save(file, board);
|
|
218
|
+
process.stdout.write(`Added ${task.id}: ${task.title}\n`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function setStatus(flags, status, extra = {}) {
|
|
222
|
+
const { file, board } = load(flags);
|
|
223
|
+
if (!flags.id) die(`${status} needs --id.\n\n${USAGE}`);
|
|
224
|
+
const task = findTask(board, flags.id);
|
|
225
|
+
task.status = status;
|
|
226
|
+
if (status === "done") task.closedAt = today();
|
|
227
|
+
else delete task.closedAt;
|
|
228
|
+
if (status === "blocked") task.blockedReason = extra.reason || "";
|
|
229
|
+
else delete task.blockedReason;
|
|
230
|
+
save(file, board);
|
|
231
|
+
process.stdout.write(`${task.id} → ${status}: ${task.title}\n`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const COMMANDS = {
|
|
235
|
+
list: cmdList,
|
|
236
|
+
goals: cmdGoals,
|
|
237
|
+
add: cmdAdd,
|
|
238
|
+
done: (f) => setStatus(f, "done"),
|
|
239
|
+
reopen: (f) => setStatus(f, "open"),
|
|
240
|
+
block: (f) => {
|
|
241
|
+
if (!f.reason) die(`block needs --reason.\n\n${USAGE}`);
|
|
242
|
+
setStatus(f, "blocked", { reason: f.reason });
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
function main() {
|
|
247
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
248
|
+
if (!command || command === "--help" || command === "-h") {
|
|
249
|
+
process.stdout.write(`${USAGE}\n`);
|
|
250
|
+
process.exit(command ? 0 : 1);
|
|
251
|
+
}
|
|
252
|
+
const handler = COMMANDS[command];
|
|
253
|
+
if (!handler) die(`Unknown command: ${command}\n\n${USAGE}`);
|
|
254
|
+
handler(parseArgs(rest));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
main();
|
|
258
|
+
|
|
259
|
+
export { STATUSES };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hq-task.sh — add, list, and close tasks on a standing task-bucket project.
|
|
3
|
+
#
|
|
4
|
+
# A task bucket is an ordinary HQ project whose prd.json carries
|
|
5
|
+
# metadata.kind == "task_board". Its user stories are errands rather than
|
|
6
|
+
# software features, so they carry no branch, repo path, or test harness.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# hq-task.sh list [--company <slug>] [--project <name>] [--all]
|
|
10
|
+
# hq-task.sh add [--company <slug>] [--project <name>] \
|
|
11
|
+
# --title "<short title>" [--description "<context>"] \
|
|
12
|
+
# [--criteria "<one done-criterion>"]... \
|
|
13
|
+
# [--priority <n>] [--contact "Name <email>|role"]...
|
|
14
|
+
# hq-task.sh done [--company <slug>] [--project <name>] --id US-002
|
|
15
|
+
# hq-task.sh reopen [--company <slug>] [--project <name>] --id US-002
|
|
16
|
+
#
|
|
17
|
+
# Defaults: --company personal --project life-admin.
|
|
18
|
+
#
|
|
19
|
+
# See core/knowledge/public/hq-core/goals-and-tasks-board.md for the concept.
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
24
|
+
export HQ_ROOT
|
|
25
|
+
|
|
26
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
27
|
+
echo "hq-task: node is required but was not found on PATH." >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
exec node "$SCRIPT_DIR/hq-task.mjs" "$@"
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# hq-core: public
|
|
3
|
+
# pipeline-worker-table.sh — the per-worker engine/model table behind
|
|
4
|
+
# /run-project --pipeline.
|
|
5
|
+
#
|
|
6
|
+
# Each worker lane runs on its own engine and model. This script proposes that
|
|
7
|
+
# table from a prd.json and, once the owner has confirmed it, turns it into the
|
|
8
|
+
# launcher exports each lane starts with. It never reads an engine or model
|
|
9
|
+
# default from a settings file (orchestrator.yaml, settings.json, ...): the only
|
|
10
|
+
# seed is each worker's own worker.yaml, and anything that cannot be read off
|
|
11
|
+
# it is left empty and flagged for the owner to answer.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# pipeline-worker-table.sh [--workers-root <dir>]... [--overlay <file>] <prd.json>
|
|
15
|
+
# pipeline-worker-table.sh [--workers-root <dir>]... [--overlay <file>] --confirmed <table.tsv> <prd.json>
|
|
16
|
+
#
|
|
17
|
+
# PROPOSE MODE (no --confirmed) prints tab-separated lines:
|
|
18
|
+
#
|
|
19
|
+
# row <worker> <engine> <model> <effort> <status> <stories> <note>
|
|
20
|
+
# hint <worker> <story> <model>
|
|
21
|
+
# unclassified <story>
|
|
22
|
+
#
|
|
23
|
+
# status is `ok` or `needs-answer`. The lane set is the union of every
|
|
24
|
+
# story's worker sequence: one `row` per distinct worker that appears in any
|
|
25
|
+
# sequence (architect and qa-tester included), in first-seen order. A `hint`
|
|
26
|
+
# line is a story's model_hint, shown for the owner; a confirmed table pin wins
|
|
27
|
+
# over it (see CONFIRMED MODE).
|
|
28
|
+
#
|
|
29
|
+
# WORKER SEQUENCE (story -> workers, the full phase sequence in order), first
|
|
30
|
+
# match wins; pipeline-conductor.sh classify reads the same three sources:
|
|
31
|
+
# 1. --overlay <file> (the run's <state>/overlay.json, or the preflight plan):
|
|
32
|
+
# {id: {"worker_sequence": [...]}}, {id: [...]}, or
|
|
33
|
+
# {"ordered_stories": [{"id": .., "worker_sequence": [...]}]}.
|
|
34
|
+
# 2. worker_preference — a string or an array of worker ids, used as given.
|
|
35
|
+
# 3. Fallback, only when both are empty: the keyword table from
|
|
36
|
+
# .claude/skills/execute-task/SKILL.md step 3, matched case-insensitively
|
|
37
|
+
# against title + description + labels:
|
|
38
|
+
# schema_change (database|migration|schema|prisma|sql) -> database-dev
|
|
39
|
+
# ui_component (component|page|form|button|react|ui) -> frontend-dev
|
|
40
|
+
# api_development (endpoint|api|rest|graphql|route|service) -> backend-dev
|
|
41
|
+
# giving architect, the matched implementers (backend-dev also for a
|
|
42
|
+
# schema change), then qa-tester.
|
|
43
|
+
# A story matching none is printed as `unclassified`; no worker is guessed.
|
|
44
|
+
#
|
|
45
|
+
# UNKNOWN WORKERS: a worker with no worker.yaml under the worker roots gets an
|
|
46
|
+
# empty, needs-answer row noted "no worker.yaml", and a line on stderr naming
|
|
47
|
+
# the id, the roots searched and every known id. pipeline-conductor.sh classify
|
|
48
|
+
# refuses such a worker before launch.
|
|
49
|
+
#
|
|
50
|
+
# ENGINE SEED (from worker.yaml `execution:`):
|
|
51
|
+
# model only -> engine claude, model = execution.model
|
|
52
|
+
# codex_model only -> engine codex, model = execution.codex_model,
|
|
53
|
+
# effort = `--reasoning <x>` from execution.codex_flags
|
|
54
|
+
# both set -> ambiguous: engine and model empty, needs-answer, the
|
|
55
|
+
# candidates listed in the note column
|
|
56
|
+
# neither / no file -> engine and model empty, needs-answer
|
|
57
|
+
# worker.yaml has no grok field, so grok is never seeded; the owner chooses it
|
|
58
|
+
# in the confirmed table.
|
|
59
|
+
#
|
|
60
|
+
# CONFIRMED MODE: --confirmed <file> holds one lane per line,
|
|
61
|
+
# <worker> <engine> <model> <effort> (tabs or spaces; # comments allowed;
|
|
62
|
+
# effort may be omitted). engine must be claude, codex, or grok and model must
|
|
63
|
+
# be non-empty, or the script exits 2. Output per lane, using the pins in
|
|
64
|
+
# core/scripts/workflow-runner.mjs, plus HQ_CONDUCT_ENGINE=<engine> for
|
|
65
|
+
# every lane (a loop lane runs pipeline phase envelopes on that engine):
|
|
66
|
+
# claude -> HQ_WORKFLOW_CLAUDE_PLAN_MODEL, HQ_WORKFLOW_CLAUDE_EXEC_MODEL,
|
|
67
|
+
# HQ_WORKFLOW_CLAUDE_EFFORT
|
|
68
|
+
# codex|grok -> HQ_WORKFLOW_MODEL, HQ_WORKFLOW_EFFORT
|
|
69
|
+
# The effort export is omitted when effort is empty (runner default applies).
|
|
70
|
+
# A story's model_hint never overrides the lane: the lane's table pin wins.
|
|
71
|
+
# Each hinted story on a confirmed lane gets a `# lane <w> story <id>
|
|
72
|
+
# (model_hint)` block whose exports are commented out and that says "hint
|
|
73
|
+
# ignored, table pins <model>". A bare alias (opus, sonnet, haiku, fable) is
|
|
74
|
+
# never printed as a model.
|
|
75
|
+
#
|
|
76
|
+
# Worker roots: --workers-root (repeatable) or PIPELINE_WORKERS_ROOTS
|
|
77
|
+
# (colon-separated). Default: <hq>/core/workers, <hq>/companies/*/workers,
|
|
78
|
+
# <hq>/personal/workers. A worker is the directory named <id> holding worker.yaml.
|
|
79
|
+
#
|
|
80
|
+
# POSIX sh (dash-clean); JSON work is done by jq.
|
|
81
|
+
|
|
82
|
+
set -eu
|
|
83
|
+
|
|
84
|
+
HQ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
85
|
+
NL='
|
|
86
|
+
'
|
|
87
|
+
TAB="$(printf '\t')"
|
|
88
|
+
|
|
89
|
+
die() { echo "pipeline-worker-table: $1" >&2; exit "${2:-1}"; }
|
|
90
|
+
|
|
91
|
+
ROOTS=""
|
|
92
|
+
CONFIRMED=""
|
|
93
|
+
OVERLAY=""
|
|
94
|
+
PRD=""
|
|
95
|
+
while [ $# -gt 0 ]; do
|
|
96
|
+
case "$1" in
|
|
97
|
+
--workers-root) [ $# -ge 2 ] || die "--workers-root needs a dir"; ROOTS="${ROOTS:+$ROOTS$NL}$2"; shift 2 ;;
|
|
98
|
+
--confirmed) [ $# -ge 2 ] || die "--confirmed needs a file"; CONFIRMED="$2"; shift 2 ;;
|
|
99
|
+
--overlay) [ $# -ge 2 ] || die "--overlay needs a file"; OVERLAY="$2"; shift 2 ;;
|
|
100
|
+
-h|--help) sed -n '3,/^# POSIX sh/p' "$0"; exit 0 ;;
|
|
101
|
+
-*) die "unknown flag: $1" ;;
|
|
102
|
+
*) [ -z "$PRD" ] || die "only one prd.json"; PRD="$1"; shift ;;
|
|
103
|
+
esac
|
|
104
|
+
done
|
|
105
|
+
[ -n "$PRD" ] || die "usage: pipeline-worker-table.sh [--workers-root <dir>] [--overlay <file>] [--confirmed <file>] <prd.json>"
|
|
106
|
+
[ -f "$PRD" ] || die "no such prd: $PRD"
|
|
107
|
+
command -v jq >/dev/null 2>&1 || die "jq is required"
|
|
108
|
+
jq -e '.userStories | type == "array"' "$PRD" >/dev/null 2>&1 || die "not a prd (no userStories array): $PRD"
|
|
109
|
+
if [ -n "$OVERLAY" ]; then
|
|
110
|
+
[ -f "$OVERLAY" ] || die "no such overlay: $OVERLAY"
|
|
111
|
+
jq -e 'type == "object"' "$OVERLAY" >/dev/null 2>&1 || die "overlay is not a JSON object: $OVERLAY"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if [ -z "$ROOTS" ]; then
|
|
115
|
+
if [ -n "${PIPELINE_WORKERS_ROOTS:-}" ]; then
|
|
116
|
+
ROOTS="$(printf '%s\n' "$PIPELINE_WORKERS_ROOTS" | tr ':' '\n')"
|
|
117
|
+
else
|
|
118
|
+
ROOTS="$HQ_ROOT/core/workers"
|
|
119
|
+
for d in "$HQ_ROOT"/companies/*/workers; do [ -d "$d" ] && ROOTS="$ROOTS$NL$d"; done
|
|
120
|
+
ROOTS="$ROOTS$NL$HQ_ROOT/personal/workers"
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# SEQS: one line per story, "<id><TAB><w1> <w2> ..." (empty worker list when
|
|
125
|
+
# unclassified), in prd order. The one place the sequence is derived.
|
|
126
|
+
SEQS="$(jq -r --slurpfile ov "${OVERLAY:-/dev/null}" '
|
|
127
|
+
def seq: if type == "array" then [.[] | select(type == "string" and length > 0)]
|
|
128
|
+
elif type == "string" then [split(",")[] | gsub("^\\s+|\\s+$"; "") | select(length > 0)]
|
|
129
|
+
elif type == "object" then (.worker_sequence // [] | seq) else [] end;
|
|
130
|
+
($ov[0] // {}) as $o
|
|
131
|
+
| (if ($o.ordered_stories | type) == "array"
|
|
132
|
+
then [$o.ordered_stories[] | select(type == "object" and (.id | type) == "string") | {key: .id, value: (.worker_sequence // [] | seq)}] | from_entries
|
|
133
|
+
else ($o | with_entries(.value |= seq)) end) as $m
|
|
134
|
+
| .userStories[]
|
|
135
|
+
| . as $s
|
|
136
|
+
| ([$s.title // "", $s.description // "", (($s.labels // []) | join(" "))] | join(" ") | ascii_downcase) as $t
|
|
137
|
+
| ($m[$s.id // ""] // []) as $ovs
|
|
138
|
+
| (($s.worker_preference // []) | seq) as $pref
|
|
139
|
+
| (if ($ovs | length) > 0 then $ovs
|
|
140
|
+
elif ($pref | length) > 0 then $pref
|
|
141
|
+
else
|
|
142
|
+
($t | test("\\b(database|migration|schema|prisma|sql)\\b")) as $schema
|
|
143
|
+
| ($t | test("\\b(component|page|form|button|react|ui)\\b")) as $ui
|
|
144
|
+
| ($t | test("\\b(endpoint|api|rest|graphql|route|service)\\b")) as $api
|
|
145
|
+
| if ($schema or $ui or $api) | not then []
|
|
146
|
+
else ["architect"] + (if $schema then ["database-dev"] else [] end)
|
|
147
|
+
+ (if ($api or $schema) then ["backend-dev"] else [] end)
|
|
148
|
+
+ (if $ui then ["frontend-dev"] else [] end) + ["qa-tester"] end
|
|
149
|
+
end) as $w
|
|
150
|
+
| "\($s.id // "")\t\($w | join(" "))"' "$PRD")"
|
|
151
|
+
|
|
152
|
+
# story_has <story id> <worker> -> 0 when the worker is in that story's sequence
|
|
153
|
+
story_has() {
|
|
154
|
+
printf '%s\n' "$SEQS" | while IFS="$TAB" read -r sid ws; do
|
|
155
|
+
[ "$sid" = "$1" ] || continue
|
|
156
|
+
case " $ws " in *" $2 "*) echo yes ;; esac
|
|
157
|
+
done | grep -q yes
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
find_worker_yaml() {
|
|
161
|
+
printf '%s\n' "$ROOTS" | while IFS= read -r r; do
|
|
162
|
+
[ -n "$r" ] && [ -d "$r" ] || continue
|
|
163
|
+
hit="$(find "$r" -type f -name worker.yaml -path "*/$1/worker.yaml" 2>/dev/null | head -n 1)"
|
|
164
|
+
if [ -n "$hit" ]; then echo "$hit"; break; fi
|
|
165
|
+
done
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
known_workers() {
|
|
169
|
+
printf '%s\n' "$ROOTS" | while IFS= read -r r; do
|
|
170
|
+
[ -n "$r" ] && [ -d "$r" ] || continue
|
|
171
|
+
find "$r" -type f -name worker.yaml 2>/dev/null
|
|
172
|
+
done | while IFS= read -r y; do basename "$(dirname "$y")"; done | sort -u | tr '\n' ' ' | sed 's/ $//'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# exec_field <worker.yaml> <key> -> value of execution.<key>, quotes and
|
|
176
|
+
# trailing comments stripped; empty when absent.
|
|
177
|
+
exec_field() {
|
|
178
|
+
awk -v key="$2" '
|
|
179
|
+
/^[^[:space:]#]/ { inexec = ($0 ~ /^execution:[[:space:]]*$/); next }
|
|
180
|
+
inexec && $0 ~ "^[[:space:]]+" key ":" {
|
|
181
|
+
v = $0; sub("^[[:space:]]+" key ":[[:space:]]*", "", v)
|
|
182
|
+
sub(/[[:space:]]+#.*$/, "", v); sub(/[[:space:]]+$/, "", v)
|
|
183
|
+
gsub(/^["'\'']|["'\'']$/, "", v); print v; exit
|
|
184
|
+
}' "$1"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
N="$(jq '.userStories | length' "$PRD")"
|
|
188
|
+
|
|
189
|
+
# ---- confirmed mode -------------------------------------------------------
|
|
190
|
+
# sq <value> -> value as one single-quoted shell word (embedded ' escaped), so
|
|
191
|
+
# a hostile model/effort string in the table or prd cannot break out of the
|
|
192
|
+
# export line when it is eval'd or sourced.
|
|
193
|
+
sq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
|
|
194
|
+
|
|
195
|
+
emit_exports() { # engine model effort [prefix]
|
|
196
|
+
printf '%sexport HQ_CONDUCT_ENGINE=%s\n' "${4:-}" "$(sq "$1")"
|
|
197
|
+
case "$1" in
|
|
198
|
+
claude)
|
|
199
|
+
printf '%sexport HQ_WORKFLOW_CLAUDE_PLAN_MODEL=%s\n' "${4:-}" "$(sq "$2")"
|
|
200
|
+
printf '%sexport HQ_WORKFLOW_CLAUDE_EXEC_MODEL=%s\n' "${4:-}" "$(sq "$2")"
|
|
201
|
+
[ -z "$3" ] || printf '%sexport HQ_WORKFLOW_CLAUDE_EFFORT=%s\n' "${4:-}" "$(sq "$3")" ;;
|
|
202
|
+
codex|grok)
|
|
203
|
+
printf '%sexport HQ_WORKFLOW_MODEL=%s\n' "${4:-}" "$(sq "$2")"
|
|
204
|
+
[ -z "$3" ] || printf '%sexport HQ_WORKFLOW_EFFORT=%s\n' "${4:-}" "$(sq "$3")" ;;
|
|
205
|
+
esac
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
is_bare_alias() { case "$1" in opus|sonnet|haiku|fable|Opus|Sonnet|Haiku|Fable) return 0 ;; esac; return 1; }
|
|
209
|
+
|
|
210
|
+
if [ -n "$CONFIRMED" ]; then
|
|
211
|
+
[ -f "$CONFIRMED" ] || die "no such confirmed table: $CONFIRMED"
|
|
212
|
+
while read -r w engine model effort _rest || [ -n "${w:-}" ]; do
|
|
213
|
+
case "$w" in ''|'#'*|worker) continue ;; esac
|
|
214
|
+
case "${engine:-}" in claude|codex|grok) ;; *) die "lane $w: engine must be claude, codex, or grok (got '${engine:-}')" 2 ;; esac
|
|
215
|
+
[ -n "${model:-}" ] || die "lane $w: model is empty — answer it before launch" 2
|
|
216
|
+
echo "# lane $w ($engine)"
|
|
217
|
+
emit_exports "$engine" "$model" "${effort:-}"
|
|
218
|
+
i=0
|
|
219
|
+
while [ "$i" -lt "$N" ]; do
|
|
220
|
+
hint="$(jq -r --argjson i "$i" '.userStories[$i].model_hint // ""' "$PRD")"
|
|
221
|
+
sid="$(jq -r --argjson i "$i" '.userStories[$i].id' "$PRD")"
|
|
222
|
+
if [ -n "$hint" ] && story_has "$sid" "$w"; then
|
|
223
|
+
if is_bare_alias "$hint"; then
|
|
224
|
+
echo "# lane $w story $sid (model_hint) bare alias ignored, table pins $model"
|
|
225
|
+
else
|
|
226
|
+
echo "# lane $w story $sid (model_hint) hint ignored, table pins $model"
|
|
227
|
+
emit_exports "$engine" "$hint" "${effort:-}" "# "
|
|
228
|
+
fi
|
|
229
|
+
fi
|
|
230
|
+
i=$((i + 1))
|
|
231
|
+
done
|
|
232
|
+
w=""; engine=""; model=""; effort=""
|
|
233
|
+
done <"$CONFIRMED"
|
|
234
|
+
exit 0
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
# ---- propose mode ---------------------------------------------------------
|
|
238
|
+
# WORKERS: distinct workers, first-seen order, one per line.
|
|
239
|
+
# STORIES_OF <worker>: the story ids whose sequence holds it, space-joined.
|
|
240
|
+
WORKERS="$(printf '%s\n' "$SEQS" | while IFS="$TAB" read -r sid ws; do
|
|
241
|
+
for w in $ws; do echo "$w"; done
|
|
242
|
+
done | awk '!seen[$0]++')"
|
|
243
|
+
|
|
244
|
+
printf '%s\n' "$SEQS" | while IFS="$TAB" read -r sid ws; do
|
|
245
|
+
[ -n "$ws" ] || printf 'unclassified\t%s\n' "$sid"
|
|
246
|
+
done
|
|
247
|
+
|
|
248
|
+
stories_of() {
|
|
249
|
+
printf '%s\n' "$SEQS" | while IFS="$TAB" read -r sid ws; do
|
|
250
|
+
case " $ws " in *" $1 "*) printf '%s\n' "$sid" ;; esac
|
|
251
|
+
done | tr '\n' ' ' | sed 's/ $//'
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
KNOWN=""
|
|
255
|
+
printf '%s\n' "$WORKERS" | while IFS= read -r w; do
|
|
256
|
+
[ -n "$w" ] || continue
|
|
257
|
+
engine=""; model=""; effort=""; status="needs-answer"; note=""
|
|
258
|
+
y="$(find_worker_yaml "$w")"
|
|
259
|
+
if [ -z "$y" ]; then
|
|
260
|
+
note="no worker.yaml"
|
|
261
|
+
[ -n "$KNOWN" ] || KNOWN="$(known_workers)"
|
|
262
|
+
echo "pipeline-worker-table: unknown worker $w: no worker.yaml under $(printf '%s\n' "$ROOTS" | tr '\n' ' ' | sed 's/ $//'); known: ${KNOWN:-none}" >&2
|
|
263
|
+
else
|
|
264
|
+
cm="$(exec_field "$y" model)"; xm="$(exec_field "$y" codex_model)"
|
|
265
|
+
if [ -n "$cm" ] && [ -n "$xm" ]; then
|
|
266
|
+
note="ambiguous: claude:$cm codex:$xm"
|
|
267
|
+
elif [ -n "$cm" ]; then
|
|
268
|
+
engine=claude; model="$cm"; status=ok
|
|
269
|
+
elif [ -n "$xm" ]; then
|
|
270
|
+
engine=codex; model="$xm"; status=ok
|
|
271
|
+
effort="$(exec_field "$y" codex_flags | sed -n 's/.*--reasoning[= ]\([A-Za-z]*\).*/\1/p')"
|
|
272
|
+
else
|
|
273
|
+
note="no execution.model or execution.codex_model"
|
|
274
|
+
fi
|
|
275
|
+
fi
|
|
276
|
+
printf 'row\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$w" "$engine" "$model" "$effort" "$status" "$(stories_of "$w")" "$note"
|
|
277
|
+
done
|
|
278
|
+
|
|
279
|
+
# hint lines: each hinted story in prd order, one per worker in its sequence
|
|
280
|
+
jq -r '.userStories[] | select((.model_hint // "") != "") | "\(.id)\t\(.model_hint)"' "$PRD" |
|
|
281
|
+
while IFS="$TAB" read -r sid hint; do
|
|
282
|
+
ws="$(printf '%s\n' "$SEQS" | while IFS="$TAB" read -r s2 w2; do if [ "$s2" = "$sid" ]; then printf "%s\n" "$w2"; fi; done)"
|
|
283
|
+
for w in $ws; do printf 'hint\t%s\t%s\t%s\n' "$w" "$sid" "$hint"; done
|
|
284
|
+
done
|
|
285
|
+
exit 0
|