@indigoai-us/hq-cli 5.316.0 → 5.318.0
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 +9 -0
- package/assets/scaffold/core/scripts/hq-session.sh +3 -3
- package/assets/scaffold/core/scripts/session-journal.sh +301 -0
- package/assets/scaffold/core/scripts/session-project.sh +800 -0
- package/assets/scaffold/core/scripts/session-title-config.sh +76 -0
- package/assets/scaffold/core/scripts/session-title.sh +305 -0
- package/dist/command-catalog.generated.d.ts +52 -0
- package/dist/command-catalog.generated.js +64 -0
- package/dist/commands/core-hq-session-native.js +3 -2
- package/dist/commands/core-session-helper-runtime.d.ts +10 -0
- package/dist/commands/core-session-helper-runtime.js +7 -0
- package/dist/commands/core-session-journal-native.d.ts +4 -0
- package/dist/commands/core-session-journal-native.js +327 -0
- package/dist/commands/core-session-project-native.d.ts +3 -0
- package/dist/commands/core-session-project-native.js +855 -0
- package/dist/commands/core-session-title-config-native.d.ts +4 -0
- package/dist/commands/core-session-title-config-native.js +77 -0
- package/dist/commands/core-session-title-native.d.ts +3 -0
- package/dist/commands/core-session-title-native.js +306 -0
- package/dist/commands/core.js +61 -0
- package/dist/fast-scaffold.js +16 -0
- package/dist/lib/lanes/autopilot.d.ts +7 -0
- package/dist/lib/lanes/autopilot.js +22 -2
- package/dist/utils/version-gate.js +8 -7
- package/package.json +1 -1
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type SessionHelperRuntimeOverrides } from "./core-session-helper-runtime.js";
|
|
2
|
+
/** Port of the flat grep/sed settings helper; the hook still parses its own YAML. */
|
|
3
|
+
export declare function runSessionTitleConfigFast(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, dependencies?: SessionHelperRuntimeOverrides): number;
|
|
4
|
+
//# sourceMappingURL=core-session-title-config-native.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { resolveSessionHelperRuntime } from "./core-session-helper-runtime.js";
|
|
4
|
+
const C_SPACE = " \\t\\r\\n\\v\\f";
|
|
5
|
+
function yamlScalar(file, key) {
|
|
6
|
+
let text;
|
|
7
|
+
try {
|
|
8
|
+
if (!fs.statSync(file).isFile())
|
|
9
|
+
return undefined;
|
|
10
|
+
text = fs.readFileSync(file, "utf8");
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const line = text.split(/\r?\n/).find((item) => new RegExp(`^${key}:[${C_SPACE}]*`).test(item));
|
|
16
|
+
if (line === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
const value = line.slice(line.indexOf(":") + 1)
|
|
19
|
+
.replace(new RegExp(`[${C_SPACE}]*#.*$`), "")
|
|
20
|
+
.replace(new RegExp(`^[${C_SPACE}]+`), "")
|
|
21
|
+
.replace(new RegExp(`[${C_SPACE}]+$`), "")
|
|
22
|
+
.replace(/^['"]/, "")
|
|
23
|
+
.replace(/['"]$/, "");
|
|
24
|
+
return value || undefined;
|
|
25
|
+
}
|
|
26
|
+
function resolveSettings(root, env) {
|
|
27
|
+
const settings = {
|
|
28
|
+
enabled: "true",
|
|
29
|
+
mode: "full",
|
|
30
|
+
desktopAutoname: "ignore-first",
|
|
31
|
+
};
|
|
32
|
+
for (const file of [
|
|
33
|
+
path.join(root, "core/settings/session-title.yaml"),
|
|
34
|
+
path.join(root, "personal/settings/session-title.yaml"),
|
|
35
|
+
]) {
|
|
36
|
+
settings.enabled = yamlScalar(file, "enabled") || settings.enabled;
|
|
37
|
+
settings.mode = yamlScalar(file, "mode") || settings.mode;
|
|
38
|
+
settings.desktopAutoname = yamlScalar(file, "desktop_autoname") || settings.desktopAutoname;
|
|
39
|
+
}
|
|
40
|
+
settings.enabled = ["0", "false", "FALSE", "False", "off", "OFF", "no", "NO"].includes(settings.enabled)
|
|
41
|
+
? "false" : "true";
|
|
42
|
+
settings.mode = ["auto", "AUTO", "hook", "deterministic"].includes(settings.mode) ? "auto" : "full";
|
|
43
|
+
settings.desktopAutoname = ["respect", "RESPECT", "off", "OFF", "false", "FALSE", "no", "NO"].includes(settings.desktopAutoname)
|
|
44
|
+
? "respect" : "ignore-first";
|
|
45
|
+
switch (env.HQ_SESSION_TITLE ?? "") {
|
|
46
|
+
case "0":
|
|
47
|
+
case "false":
|
|
48
|
+
case "FALSE":
|
|
49
|
+
case "off":
|
|
50
|
+
case "OFF":
|
|
51
|
+
case "no":
|
|
52
|
+
case "NO":
|
|
53
|
+
settings.enabled = "false";
|
|
54
|
+
break;
|
|
55
|
+
case "auto":
|
|
56
|
+
case "AUTO":
|
|
57
|
+
settings.mode = "auto";
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if ((env.HQ_DISABLED_HOOKS ?? "").includes("session-title"))
|
|
61
|
+
settings.enabled = "false";
|
|
62
|
+
return settings;
|
|
63
|
+
}
|
|
64
|
+
/** Port of the flat grep/sed settings helper; the hook still parses its own YAML. */
|
|
65
|
+
export function runSessionTitleConfigFast(args, _stdin, cwd, hqRoot, dependencies = {}) {
|
|
66
|
+
let root = hqRoot ?? cwd;
|
|
67
|
+
for (let i = 0; i < args.length; i++) {
|
|
68
|
+
if (args[i] === "--root") {
|
|
69
|
+
root = args[i + 1] ?? root;
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const settings = resolveSettings(root, resolveSessionHelperRuntime(dependencies).env);
|
|
74
|
+
process.stdout.write(`enabled=${settings.enabled}\nmode=${settings.mode}\ndesktop_autoname=${settings.desktopAutoname}\n`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=core-session-title-config-native.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type SessionHelperRuntimeOverrides } from "./core-session-helper-runtime.js";
|
|
2
|
+
export declare function runSessionTitleFast(args: readonly string[], _stdin: string, processCwd: string, root: string | undefined, dependencies?: SessionHelperRuntimeOverrides): number;
|
|
3
|
+
//# sourceMappingURL=core-session-title-native.d.ts.map
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { resolveSessionHelperRuntime } from "./core-session-helper-runtime.js";
|
|
4
|
+
// The legacy helper's compute body is kept as in-process TypeScript.
|
|
5
|
+
export function runSessionTitleFast(args, _stdin, processCwd, root, dependencies = {}) {
|
|
6
|
+
const { now } = resolveSessionHelperRuntime(dependencies);
|
|
7
|
+
let sessionId = "default";
|
|
8
|
+
let commandArg = "";
|
|
9
|
+
let sessionCwd = "";
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
switch (args[i]) {
|
|
12
|
+
case "--session-id":
|
|
13
|
+
sessionId = args[i + 1] ?? "default";
|
|
14
|
+
i++;
|
|
15
|
+
break;
|
|
16
|
+
case "--command":
|
|
17
|
+
commandArg = args[i + 1] ?? "";
|
|
18
|
+
i++;
|
|
19
|
+
break;
|
|
20
|
+
case "--cwd":
|
|
21
|
+
sessionCwd = args[i + 1] ?? "";
|
|
22
|
+
i++;
|
|
23
|
+
break;
|
|
24
|
+
default: break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const resolvedRoot = root ?? processCwd;
|
|
28
|
+
const hq = resolvedRoot;
|
|
29
|
+
const sid = sessionId;
|
|
30
|
+
let command = (commandArg || "").trim().replace(/^\/+/, "");
|
|
31
|
+
const key = sid.replace(/[^A-Za-z0-9._-]/g, "_") || "default";
|
|
32
|
+
const state = path.join(hq, ".claude", "state");
|
|
33
|
+
const firstLine = (p) => {
|
|
34
|
+
try {
|
|
35
|
+
for (let line of fs.readFileSync(p, "utf8").split(/\r?\n/)) {
|
|
36
|
+
line = line.trim();
|
|
37
|
+
if (line)
|
|
38
|
+
return line;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch { /* the manifest is optional */ }
|
|
42
|
+
return "";
|
|
43
|
+
};
|
|
44
|
+
// --- resolve active project path (session-scoped, then global fallback) ---
|
|
45
|
+
// Session-scoped ONLY. There used to be a fallback to the machine-global
|
|
46
|
+
// .claude/state/active-session-project here; it made every session without its
|
|
47
|
+
// own entry inherit whatever project was last active anywhere on the box (142
|
|
48
|
+
// unrelated sessions all named "learn-policies-only · chat"). Session-scoped
|
|
49
|
+
// state must fail to empty, never to a shared global.
|
|
50
|
+
const projPath = firstLine(path.join(state, "auto-session-project-" + key));
|
|
51
|
+
let company = "";
|
|
52
|
+
let project = "";
|
|
53
|
+
if (projPath) {
|
|
54
|
+
let rel = projPath;
|
|
55
|
+
try {
|
|
56
|
+
const resolved = fs.realpathSync(projPath);
|
|
57
|
+
const hqResolved = fs.realpathSync(hq);
|
|
58
|
+
const r = path.relative(hqResolved, resolved);
|
|
59
|
+
if (r === "" || r.startsWith("..") || path.isAbsolute(r))
|
|
60
|
+
throw new Error("outside hq");
|
|
61
|
+
rel = r;
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
rel = projPath.split(hq.replace(/\/+$/, "") + "/").join("");
|
|
65
|
+
}
|
|
66
|
+
const parts = rel.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
67
|
+
if (parts.length >= 2 && parts[0] === "companies") {
|
|
68
|
+
company = parts[1];
|
|
69
|
+
project = parts[parts.length - 1];
|
|
70
|
+
}
|
|
71
|
+
else if (parts.length && parts[0] === "personal") {
|
|
72
|
+
company = "personal"; // rendered as the "ME" org token
|
|
73
|
+
project = parts[parts.length - 1];
|
|
74
|
+
}
|
|
75
|
+
else if (parts.length) {
|
|
76
|
+
project = parts[parts.length - 1];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// --- company fallbacks when no project resolved ---
|
|
80
|
+
if (!company && command === "hqwork")
|
|
81
|
+
company = "hq-core";
|
|
82
|
+
if (!company && !project) {
|
|
83
|
+
const manifest = path.join(hq, "companies", "manifest.yaml");
|
|
84
|
+
const slugs = [];
|
|
85
|
+
try {
|
|
86
|
+
let inCompanies = false;
|
|
87
|
+
for (const line of fs.readFileSync(manifest, "utf8").split(/\r?\n/)) {
|
|
88
|
+
if (!line.trim() || line.trimStart().startsWith("#"))
|
|
89
|
+
continue;
|
|
90
|
+
if (inCompanies && /^[^\s#]/.test(line))
|
|
91
|
+
break;
|
|
92
|
+
if (/^companies:\s*$/.test(line)) {
|
|
93
|
+
inCompanies = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (inCompanies) {
|
|
97
|
+
const m = line.match(/^\x20{2}([a-z][a-z0-9_-]*):\s*$/);
|
|
98
|
+
if (m && m[1] !== "_template")
|
|
99
|
+
slugs.push(m[1]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch { /* orchestrator state is optional */ }
|
|
104
|
+
if (slugs.length === 1)
|
|
105
|
+
company = slugs[0];
|
|
106
|
+
}
|
|
107
|
+
// --- status emoji from the orchestrator ---
|
|
108
|
+
let emoji = "";
|
|
109
|
+
const orch = path.join(hq, "workspace", "orchestrator", "state.json");
|
|
110
|
+
if (project) {
|
|
111
|
+
try {
|
|
112
|
+
const data = JSON.parse(fs.readFileSync(orch, "utf8"));
|
|
113
|
+
for (const p of data.projects || []) {
|
|
114
|
+
const nm = p.name || "";
|
|
115
|
+
const prd = String(p.prdPath || "").replace(/\\/g, "/");
|
|
116
|
+
const expectedProjectPath = company === "personal"
|
|
117
|
+
? `personal/projects/${project}/`
|
|
118
|
+
: company ? `companies/${company}/projects/${project}/` : "";
|
|
119
|
+
const matchesProject = prd
|
|
120
|
+
? expectedProjectPath
|
|
121
|
+
? prd.replace(/^\/+/, "").includes(expectedProjectPath)
|
|
122
|
+
: nm === project || prd.includes("/" + project + "/")
|
|
123
|
+
: nm === project;
|
|
124
|
+
if (matchesProject) {
|
|
125
|
+
const st = p.state || "";
|
|
126
|
+
if (st === "IN_PROGRESS") {
|
|
127
|
+
emoji = "▶️";
|
|
128
|
+
}
|
|
129
|
+
else if (st === "COMPLETED") {
|
|
130
|
+
const ts = p.updatedAt || p.updated_at || "";
|
|
131
|
+
const t = new Date(ts);
|
|
132
|
+
if (!isNaN(t.getTime()) && (now().getTime() - t.getTime()) / 1000 < 86400) {
|
|
133
|
+
emoji = "✅"; // only recent completions
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch { /* orchestrator state is optional */ }
|
|
141
|
+
}
|
|
142
|
+
// --- mode ---
|
|
143
|
+
// The emoji encodes what the session IS DOING, not what domain it belongs to.
|
|
144
|
+
// State is what a human scans a sidebar for ("does this need me?"); domain is
|
|
145
|
+
// already implied by the subject line. Keyed on the active slash command,
|
|
146
|
+
// which the wrapper hook already tracks turn to turn.
|
|
147
|
+
//
|
|
148
|
+
// Exactly ONE glyph, never two — a second glyph beside the first reads as a
|
|
149
|
+
// badge pair and makes the left edge busier without making it more scannable.
|
|
150
|
+
// The single slot shows the most USEFUL thing about the session right now,
|
|
151
|
+
// picked by the precedence ladder in hq-session-title-grammar: work that needs
|
|
152
|
+
// the user or has terminated outranks the session kind, which outranks the
|
|
153
|
+
// workflow stage, which outranks the craft.
|
|
154
|
+
//
|
|
155
|
+
// This script only ever emits a stage or kind glyph. It never emits 🙋 (needs
|
|
156
|
+
// the user) — it cannot know that — and never a craft glyph, since its only
|
|
157
|
+
// subject is a directory slug and any keyword match against that slug is
|
|
158
|
+
// redundant with the slug by construction. Both are the model's job.
|
|
159
|
+
//
|
|
160
|
+
// A handoff has two moments and they are different rows in a sidebar:
|
|
161
|
+
// 📝 the handoff is being written (the session is wrapping up) and 📤 it is
|
|
162
|
+
// ready (the session is closed; resume from the thread). This script emits 📝
|
|
163
|
+
// while /handoff runs; only the model can know when it finished, so it sets 📤.
|
|
164
|
+
// Both are distinct from ✅ "the work shipped" and 🤝 "a person owns it now".
|
|
165
|
+
const MODES = [
|
|
166
|
+
[/^(brainstorm|idea|dream|discover|strategize)$/, "💭"],
|
|
167
|
+
[/^(plan|prd|deep-plan|storyboard|architect|review-plan|codebase-design)$/, "📐"],
|
|
168
|
+
[/^(run-project|execute-task|ship|land|land-batch|run-pipeline|orchestrate|tdd|deploy)$/, "⚡"],
|
|
169
|
+
[/^(review|code-review|security-review)$/, "👀"],
|
|
170
|
+
[/^(quality-gate|smoke|verify|diagnose|investigate)$/, "🧪"],
|
|
171
|
+
[/^(handoff|checkpoint-handoff|retro)$/, "📝"],
|
|
172
|
+
[/^(delegate|new-hire|new-agent|promote)$/, "🤝"],
|
|
173
|
+
[/^(dm|hq-slack|meeting-notes|work-broadcast|signals|hq-share)$/, "💬"],
|
|
174
|
+
[/^(schedule|job|loop)$/, "🔁"],
|
|
175
|
+
];
|
|
176
|
+
const modeFor = (word) => {
|
|
177
|
+
for (const [re, ic] of MODES)
|
|
178
|
+
if (re.test(word))
|
|
179
|
+
return ic;
|
|
180
|
+
return "";
|
|
181
|
+
};
|
|
182
|
+
if (!command)
|
|
183
|
+
command = "chat";
|
|
184
|
+
// Precedence: a finished or running project outranks the command word — "it
|
|
185
|
+
// shipped" and "it is executing right now" beat "you typed /plan an hour ago".
|
|
186
|
+
// `emoji` is set above from orchestrator state (✅ recent completion, ▶️ running).
|
|
187
|
+
const mode = emoji === "▶️" ? "⚡" : (emoji || modeFor(command));
|
|
188
|
+
// --- company ---
|
|
189
|
+
// Always the first text token. Long slugs get an explicit short form from the
|
|
190
|
+
// settings `aliases:` block rather than a mid-word truncation — a single-word
|
|
191
|
+
// slug sliced at 8 characters is unreadable.
|
|
192
|
+
// Built-ins only. Company short forms are USER data — a release-shipped file
|
|
193
|
+
// must never carry tenant slugs (enforced by the slug-scan / denylist-scan CI
|
|
194
|
+
// gates). Users map their own long slugs in personal/settings/session-title.yaml:
|
|
195
|
+
//
|
|
196
|
+
// aliases:
|
|
197
|
+
// some-long-company-slug: SHORT
|
|
198
|
+
//
|
|
199
|
+
const ALIASES = { personal: "ME", "hq-core": "HQ" };
|
|
200
|
+
// Merge user aliases from the settings files, lowest precedence first.
|
|
201
|
+
const readAliases = (file) => {
|
|
202
|
+
let text = "";
|
|
203
|
+
try {
|
|
204
|
+
text = fs.readFileSync(file, "utf8");
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
let inBlock = false;
|
|
210
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
211
|
+
const line = raw.replace(/#.*$/, "").replace(/\s+$/, "");
|
|
212
|
+
if (!line.trim())
|
|
213
|
+
continue;
|
|
214
|
+
if (/^aliases:\s*$/.test(line)) {
|
|
215
|
+
inBlock = true;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!/^\s/.test(line)) {
|
|
219
|
+
inBlock = false;
|
|
220
|
+
continue;
|
|
221
|
+
} // any new top-level key ends it
|
|
222
|
+
if (!inBlock)
|
|
223
|
+
continue;
|
|
224
|
+
const m = line.match(/^\s+["']?([A-Za-z0-9._-]+)["']?:\s*["']?([^"'\s]+)["']?\s*$/);
|
|
225
|
+
if (m)
|
|
226
|
+
ALIASES[m[1]] = m[2];
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
readAliases(path.join(hq, "core", "settings", "session-title.yaml"));
|
|
230
|
+
readAliases(path.join(hq, "personal", "settings", "session-title.yaml"));
|
|
231
|
+
let org = "";
|
|
232
|
+
if (company) {
|
|
233
|
+
org = ALIASES[company];
|
|
234
|
+
if (!org) {
|
|
235
|
+
const head = company.split("-")[0].toUpperCase();
|
|
236
|
+
org = head.length <= 8 ? head : head.slice(0, 8);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// --- compose ---
|
|
240
|
+
// Grammar: {glyph} {COMPANY} · {Product} · {subject}
|
|
241
|
+
//
|
|
242
|
+
// Every rendered glyph must mean something. When the mode is unknown the title
|
|
243
|
+
// simply starts with the company — a placeholder icon (the old "🟦") is worse
|
|
244
|
+
// than nothing, because it trains the eye to ignore the emoji column.
|
|
245
|
+
//
|
|
246
|
+
// The hook can only ever put a directory slug in the subject slot. A session
|
|
247
|
+
// that knows what it is actually about should overwrite this via
|
|
248
|
+
// set_session_title, keeping the same grammar.
|
|
249
|
+
// --- product / repo ---
|
|
250
|
+
// The repo or product the session is working in, rendered between the company
|
|
251
|
+
// and the subject so identity survives a narrow sidebar's truncation. Derived
|
|
252
|
+
// from the session cwd when it sits inside repos/; otherwise left to the model.
|
|
253
|
+
//
|
|
254
|
+
// A leading company prefix is stripped, because the company token already said
|
|
255
|
+
// it: repos/public/hq-work under company HQ renders as "Work", not "Hq-work".
|
|
256
|
+
let product = "";
|
|
257
|
+
const cwd = sessionCwd;
|
|
258
|
+
if (cwd) {
|
|
259
|
+
const rel = cwd.split(/[\\/]/).filter(Boolean);
|
|
260
|
+
const i = rel.lastIndexOf("repos");
|
|
261
|
+
if (i !== -1 && rel.length > i + 1) {
|
|
262
|
+
// repos/<public|private>/<repo>/... or repos/<repo>/...
|
|
263
|
+
let name = rel[i + 1];
|
|
264
|
+
if ((name === "public" || name === "private") && rel.length > i + 2)
|
|
265
|
+
name = rel[i + 2];
|
|
266
|
+
if (name) {
|
|
267
|
+
// "hq-work" -> ["HQ", "Work"]. Two-letter tokens are acronyms, not words.
|
|
268
|
+
let words = name.split("-").filter(Boolean).map((w) => w.length <= 2 ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1));
|
|
269
|
+
// Drop a leading word the company token already said: under company HQ,
|
|
270
|
+
// "hq-work" is "Work", not "HQ Work".
|
|
271
|
+
if (org && words.length > 1 && words[0].toUpperCase() === org.toUpperCase()) {
|
|
272
|
+
words = words.slice(1);
|
|
273
|
+
}
|
|
274
|
+
product = words.join(" ");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const subject = project || "";
|
|
279
|
+
// --- stub suppression -------------------------------------------------------
|
|
280
|
+
// With no project AND no repo/product, every remaining ingredient is
|
|
281
|
+
// information-free: a bare command word ("chat", "startwork") or a lone org
|
|
282
|
+
// token ("HQ"). Emitting one of those is actively harmful — it is not merely
|
|
283
|
+
// useless, it OVERWRITES the host's written summary ("HQ core skill cloning")
|
|
284
|
+
// with a word that distinguishes nothing, and because the stub never changes,
|
|
285
|
+
// the wrapper's change-only cadence then goes silent for the rest of the
|
|
286
|
+
// session. Sessions were sitting on 1281 prompts still titled "chat".
|
|
287
|
+
//
|
|
288
|
+
// Printing nothing makes the wrapper exit before it emits or records anything,
|
|
289
|
+
// so the host's own title stands. The moment a project or repo does resolve,
|
|
290
|
+
// the normal path takes the title back.
|
|
291
|
+
if (!project && !product)
|
|
292
|
+
return 0;
|
|
293
|
+
const parts = [org, product, subject].filter(Boolean);
|
|
294
|
+
if (!parts.length)
|
|
295
|
+
parts.push(command);
|
|
296
|
+
const compose = (ps) => (mode ? mode + " " : "") + ps.join(" · ");
|
|
297
|
+
const MAX = 56;
|
|
298
|
+
let title = compose(parts);
|
|
299
|
+
if (title.length > MAX && parts.length > 1)
|
|
300
|
+
title = compose([parts[parts.length - 1]]);
|
|
301
|
+
if (title.length > MAX)
|
|
302
|
+
title = title.slice(0, MAX - 1).replace(/\s+$/, "") + "…";
|
|
303
|
+
process.stdout.write(title + "\n");
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
//# sourceMappingURL=core-session-title-native.js.map
|
package/dist/commands/core.js
CHANGED
|
@@ -66,6 +66,7 @@ import { timeoutGuardCommand } from "../lib/core-utils/timeout-guard.js";
|
|
|
66
66
|
import { readSentryReportStdin, reportSentryEvent } from "../lib/core-utils/sentry-report.js";
|
|
67
67
|
import { CLEAN_HANDOFF_ASSET, RESUME_LOCK_ASSET, VERIFY_DELIVERABLES_ASSET, WORKTREE_GC_ASSET, runCleanWorktreeReconcileHandoff, runResumeThreadLock, runVerifyStoryDeliverables, runWorktreeGc, } from "../lib/core-utils/worktree-resume.js";
|
|
68
68
|
import { coreNativeHandoffEnabled } from "./core-native-handoff-flag.js";
|
|
69
|
+
import { coreNativeHookHelpersEnabled } from "./core-native-hook-helpers-flag.js";
|
|
69
70
|
import { coreCompanyIndexScopeEnabled } from "./core-company-index-scope-flag.js";
|
|
70
71
|
import { runWorkerSkillCommand } from "./core-worker-skills-command.js";
|
|
71
72
|
import { describeCoreCommand, registeredCoreCommandListing, } from "./core-command-catalog.js";
|
|
@@ -224,6 +225,10 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
224
225
|
* direct `hq core` command.
|
|
225
226
|
*/
|
|
226
227
|
export const SCAFFOLD_ONLY_ASSETS = [
|
|
228
|
+
{ asset: "core/scripts/session-journal.sh", root: "live", summary: "Rollback companion for native session journal" },
|
|
229
|
+
{ asset: "core/scripts/session-project.sh", root: "live", summary: "Rollback companion for native session project" },
|
|
230
|
+
{ asset: "core/scripts/session-title.sh", root: "live", summary: "Rollback companion for native session title" },
|
|
231
|
+
{ asset: "core/scripts/session-title-config.sh", root: "live", summary: "Rollback companion for native session-title settings" },
|
|
227
232
|
{ asset: "core/scripts/resolve-company.sh", root: "live", summary: "Rollback companion for native resolve-company" },
|
|
228
233
|
{ asset: "core/scripts/jobs-schedule-parse.sh", root: "live", summary: "Rollback companion for native jobs schedule-parse" },
|
|
229
234
|
{ asset: "core/scripts/jobs-validate.sh", root: "live", summary: "Rollback companion for native jobs validate" },
|
|
@@ -392,6 +397,62 @@ export function registerCoreCommands(program) {
|
|
|
392
397
|
registerCoreWorkMeshBindTrustedNative(core);
|
|
393
398
|
registerCoreHookHelperNativeCommands(core);
|
|
394
399
|
registerCoreHookStateHelpers(core);
|
|
400
|
+
const sessionHelperCommands = [
|
|
401
|
+
{
|
|
402
|
+
name: "session-journal", asset: "core/scripts/session-journal.sh", root: "live",
|
|
403
|
+
summary: "Write and read session journal entries",
|
|
404
|
+
run: async (args, _stdin, root) => (await import("./core-session-journal-native.js"))
|
|
405
|
+
.runSessionJournalFast(args, "", process.cwd(), root),
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: "session-project", asset: "core/scripts/session-project.sh", root: "live",
|
|
409
|
+
summary: "Find and update native session projects", readsStdin: true,
|
|
410
|
+
run: async (args, stdin, root) => (await import("./core-session-project-native.js"))
|
|
411
|
+
.runSessionProjectFast(args, stdin, process.cwd(), root),
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "session-title", asset: "core/scripts/session-title.sh", root: "live",
|
|
415
|
+
summary: "Compute the current session title",
|
|
416
|
+
run: async (args, _stdin, root) => (await import("./core-session-title-native.js"))
|
|
417
|
+
.runSessionTitleFast(args, "", process.cwd(), root),
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: "session-title-config", asset: "core/scripts/session-title-config.sh", root: "live",
|
|
421
|
+
summary: "Resolve session title settings",
|
|
422
|
+
run: async (args, _stdin, root) => (await import("./core-session-title-config-native.js"))
|
|
423
|
+
.runSessionTitleConfigFast(args, "", process.cwd(), root),
|
|
424
|
+
},
|
|
425
|
+
];
|
|
426
|
+
for (const entry of sessionHelperCommands) {
|
|
427
|
+
const command = core
|
|
428
|
+
.command(entry.name)
|
|
429
|
+
.description(entry.summary)
|
|
430
|
+
.allowUnknownOption()
|
|
431
|
+
.allowExcessArguments()
|
|
432
|
+
.helpOption(false)
|
|
433
|
+
.argument("[args...]")
|
|
434
|
+
.action(async (args = [], _opts, cmd) => {
|
|
435
|
+
const scope = core.opts();
|
|
436
|
+
const operands = cmd.args.length > 0 ? cmd.args : args;
|
|
437
|
+
if (!await coreNativeHookHelpersEnabled()) {
|
|
438
|
+
runEntry(entry, scope, operands);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const { cwd, hqRoot } = resolveEntryRoot(entry, scope);
|
|
442
|
+
const shouldReadStdin = entry.readsStdin && operands[0] === "ingest-plan";
|
|
443
|
+
let stdin = "";
|
|
444
|
+
if (shouldReadStdin) {
|
|
445
|
+
const chunks = [];
|
|
446
|
+
for await (const chunk of process.stdin)
|
|
447
|
+
chunks.push(chunk);
|
|
448
|
+
stdin = Buffer.concat(chunks).toString("utf8");
|
|
449
|
+
}
|
|
450
|
+
const code = await entry.run(operands, stdin, hqRoot ?? cwd);
|
|
451
|
+
if (code !== 0)
|
|
452
|
+
process.exit(code);
|
|
453
|
+
});
|
|
454
|
+
describeCoreCommand(command, { execution: "native", asset: entry.asset, root: entry.root });
|
|
455
|
+
}
|
|
395
456
|
// Wave 2 handoff/worktree commands retain their byte-identical shell assets
|
|
396
457
|
// as a default-off rollback path for one release. Keep the gate helper small
|
|
397
458
|
// so sibling Wave 2 work can reuse it without editing this command table.
|
package/dist/fast-scaffold.js
CHANGED
|
@@ -201,6 +201,22 @@ export const FAST_SCAFFOLD_SCRIPTS = [
|
|
|
201
201
|
shellCwd: "live",
|
|
202
202
|
shellHqRoot: "live",
|
|
203
203
|
},
|
|
204
|
+
...["session-journal", "session-project", "session-title", "session-title-config"].map((name) => ({
|
|
205
|
+
name,
|
|
206
|
+
kind: "native",
|
|
207
|
+
modulePath: `./commands/core-${name}-native.js`,
|
|
208
|
+
exportName: name === "session-journal" ? "runSessionJournalFast"
|
|
209
|
+
: name === "session-project" ? "runSessionProjectFast"
|
|
210
|
+
: name === "session-title" ? "runSessionTitleFast"
|
|
211
|
+
: "runSessionTitleConfigFast",
|
|
212
|
+
flagKey: "hq-core-native-fast-route",
|
|
213
|
+
familyFlagKey: "core-native-hook-helpers",
|
|
214
|
+
asset: `core/scripts/${name}.sh`,
|
|
215
|
+
root: "live",
|
|
216
|
+
shellCwd: "live",
|
|
217
|
+
shellHqRoot: "live",
|
|
218
|
+
...(name === "session-project" ? { readsStdin: true, readsStdinFor: (args) => args[0] === "ingest-plan" } : {}),
|
|
219
|
+
})),
|
|
204
220
|
...[
|
|
205
221
|
{ name: "eval-trigger", modulePath: "./commands/core-eval-trigger-native.js", exportName: "runEvalTriggerFast", asset: "core/scripts/eval-trigger.sh", root: "cwd", shellCwd: "caller", shellHqRoot: "none", readsStdinFor: (args) => args[0] === "--check" },
|
|
206
222
|
{ name: "migrate-policy-triggers", modulePath: "./commands/core-migrate-policy-triggers-native.js", exportName: "runMigratePolicyTriggersFast", asset: "core/scripts/migrate-policy-triggers.sh", root: "live", shellCwd: "caller", shellHqRoot: "live" },
|
|
@@ -83,6 +83,13 @@ export interface AutopilotPassResult {
|
|
|
83
83
|
skipped: Array<{
|
|
84
84
|
lead_lane: string;
|
|
85
85
|
reason: string;
|
|
86
|
+
status?: undefined;
|
|
87
|
+
fix_command?: undefined;
|
|
88
|
+
} | {
|
|
89
|
+
lead_lane: string;
|
|
90
|
+
reason: "lead_lane_missing";
|
|
91
|
+
status: "stale";
|
|
92
|
+
fix_command: string;
|
|
86
93
|
} | {
|
|
87
94
|
lane_id: string;
|
|
88
95
|
error: string;
|
|
@@ -35,6 +35,13 @@ function isOpenProjectStory(story) {
|
|
|
35
35
|
function isRecord(value) {
|
|
36
36
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
37
37
|
}
|
|
38
|
+
function shellQuote(value) {
|
|
39
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
40
|
+
}
|
|
41
|
+
function staleLeadFixCommand(laneId) {
|
|
42
|
+
const registryTemp = `${LEAD_REGISTRY_REL}.tmp`;
|
|
43
|
+
return `jq --arg lane ${shellQuote(laneId)} '(.juniors) |= map(select(.lane != $lane))' ${shellQuote(LEAD_REGISTRY_REL)} > ${shellQuote(registryTemp)} && mv ${shellQuote(registryTemp)} ${shellQuote(LEAD_REGISTRY_REL)}`;
|
|
44
|
+
}
|
|
38
45
|
function readJsonFile(filePath, code) {
|
|
39
46
|
let text;
|
|
40
47
|
try {
|
|
@@ -755,9 +762,23 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
755
762
|
const stateFile = path.join(hqRoot, "workspace", "lane-briefs", "autopilot-state.json");
|
|
756
763
|
const state = readState(stateFile);
|
|
757
764
|
for (const registered of leads) {
|
|
765
|
+
const registeredLocalHost = registered.host === "local" || registered.host === input.hostname;
|
|
758
766
|
const lead = byId.get(registered.lane) ?? lanes.find((candidate) => candidate.lane_id.startsWith(`${registered.lane}_`));
|
|
759
767
|
if (!lead) {
|
|
760
|
-
|
|
768
|
+
// A row for another host has no lane record here; that host's pass owns it.
|
|
769
|
+
const registryTargetsThisHost = registeredLocalHost
|
|
770
|
+
? effectiveOutpostId === null
|
|
771
|
+
: input.outpostId === registered.host;
|
|
772
|
+
if (!registryTargetsThisHost) {
|
|
773
|
+
result.skipped.push({ lead_lane: registered.lane, reason: "lead_host_mismatch" });
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
result.skipped.push({
|
|
777
|
+
lead_lane: registered.lane,
|
|
778
|
+
reason: "lead_lane_missing",
|
|
779
|
+
status: "stale",
|
|
780
|
+
fix_command: staleLeadFixCommand(registered.lane),
|
|
781
|
+
});
|
|
761
782
|
continue;
|
|
762
783
|
}
|
|
763
784
|
const company = resolveLaneCompany(hqRoot, lead).company;
|
|
@@ -765,7 +786,6 @@ async function runLeadAutopilotPassUnlocked(input) {
|
|
|
765
786
|
result.skipped.push({ lead_lane: lead.lane_id, reason: "lead_company_unresolved" });
|
|
766
787
|
continue;
|
|
767
788
|
}
|
|
768
|
-
const registeredLocalHost = registered.host === "local" || registered.host === input.hostname;
|
|
769
789
|
const hostMatchesRegistry = registeredLocalHost
|
|
770
790
|
? lead.host.kind === "local"
|
|
771
791
|
: lead.host.kind === "outpost" && lead.host.id === registered.host && input.outpostId === registered.host;
|
|
@@ -1382,7 +1382,9 @@ function attemptRequiredUpdate(decision, deps, install) {
|
|
|
1382
1382
|
const command = decision.updateCommand;
|
|
1383
1383
|
const isManagedOutsideNpm = install.manager !== "npm";
|
|
1384
1384
|
const prefix = install.prefix;
|
|
1385
|
-
|
|
1385
|
+
// Any npm global install with a known prefix must build away from the live package.
|
|
1386
|
+
// An interrupted in-place npm install can leave hq-cli itself un-runnable.
|
|
1387
|
+
const stagedNpmPrefix = Boolean(!isManagedOutsideNpm && prefix);
|
|
1386
1388
|
const runner = deps.runner ?? runUpdateCommand;
|
|
1387
1389
|
// Resolve the concrete install argv once so the sudo fallback below can re-run
|
|
1388
1390
|
// the EXACT same command under elevation.
|
|
@@ -1422,7 +1424,7 @@ function attemptRequiredUpdate(decision, deps, install) {
|
|
|
1422
1424
|
else if (prefix) {
|
|
1423
1425
|
console.error(chalk.dim(` Installing into npm prefix: ${prefix}`));
|
|
1424
1426
|
if (stagedNpmPrefix) {
|
|
1425
|
-
console.error(chalk.dim(" Building
|
|
1427
|
+
console.error(chalk.dim(" Building outside the active npm prefix before activation."));
|
|
1426
1428
|
const installStaged = deps.installNpmPackageStaged;
|
|
1427
1429
|
if (!installStaged) {
|
|
1428
1430
|
result = { ok: false, detail: "staged npm installer was not loaded" };
|
|
@@ -1556,11 +1558,10 @@ export async function enforceVersionGate(onUpdateRecommended, options = {}) {
|
|
|
1556
1558
|
// neither downstream path repeats it.
|
|
1557
1559
|
const install = resolveRunningInstall();
|
|
1558
1560
|
if (decision.updateRequired) {
|
|
1559
|
-
// Load the staged installer only
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
1562
|
-
|
|
1563
|
-
const installNpmPackageStaged = install.manager === "npm" && install.prefix && /\s/.test(install.prefix)
|
|
1561
|
+
// Load the staged installer only when an npm prefix is resolved. The version
|
|
1562
|
+
// gate is imported during every CLI startup, so keep the filesystem-heavy
|
|
1563
|
+
// module off commands that never need a required update.
|
|
1564
|
+
const installNpmPackageStaged = install.manager === "npm" && install.prefix
|
|
1564
1565
|
? (await import("./staged-npm-install.js")).installNpmPackageStaged
|
|
1565
1566
|
: undefined;
|
|
1566
1567
|
// Exits the process, EXCEPT when another updater holds the shared update
|