@indigoai-us/hq-cli 5.345.75 → 5.345.77
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 +48 -0
- package/assets/scaffold/core/scripts/rebuild-companies-index.sh +30 -37
- package/dist/command-catalog.generated.d.ts +83 -3
- package/dist/command-catalog.generated.js +100 -3
- package/dist/commands/access.d.ts +1 -0
- package/dist/commands/access.js +17 -5
- package/dist/commands/bot.d.ts +12 -0
- package/dist/commands/bot.js +54 -2
- package/dist/commands/core-native-family-flag.js +1 -1
- package/dist/commands/files.js +7 -2
- package/dist/commands/members.d.ts +42 -0
- package/dist/commands/members.js +151 -1
- package/dist/commands/secrets.js +1 -1
- package/dist/fast-scaffold.d.ts +1 -1
- package/dist/fast-scaffold.js +1 -0
- package/dist/lib/bot/api.d.ts +11 -0
- package/dist/lib/bot/api.js +2 -0
- package/dist/lib/bot/config.js +4 -2
- package/dist/lib/bot/inflight.d.ts +3 -0
- package/dist/lib/bot/probe.d.ts +57 -0
- package/dist/lib/bot/probe.js +100 -0
- package/dist/lib/bot/run.d.ts +4 -0
- package/dist/lib/bot/run.js +171 -37
- package/dist/lib/bot/runtime/codex.d.ts +67 -1
- package/dist/lib/bot/runtime/codex.js +197 -3
- package/dist/lib/bot/runtime/grok.js +2 -2
- package/dist/lib/bot/runtime/index.d.ts +8 -0
- package/dist/lib/bot/runtime/index.js +91 -5
- package/dist/lib/bot/runtime/messages-stream.js +1 -1
- package/dist/lib/bot/runtime/types.d.ts +33 -0
- package/dist/lib/bot/runtime/types.js +10 -0
- package/dist/lib/bot/runtime-failure.d.ts +113 -0
- package/dist/lib/bot/runtime-failure.js +180 -0
- package/dist/lib/bot/runtime-sign-in.js +2 -2
- package/dist/lib/bot/status.d.ts +21 -0
- package/dist/lib/index-render/companies.js +33 -16
- package/dist/lib/index-render/shared.d.ts +18 -0
- package/dist/lib/index-render/shared.js +24 -0
- package/dist/lib/lanes/monitor.js +30 -15
- package/package.json +1 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What went wrong with a bot's coding tool (Claude Code, Codex, Grok), in a
|
|
3
|
+
* class a person can act on.
|
|
4
|
+
*
|
|
5
|
+
* A failed model turn used to be reported by quoting the runtime's own error
|
|
6
|
+
* in the DM ("Sorry, I couldn't get started on my own (codex failed: The
|
|
7
|
+
* 'gpt-6-astra' model requires a newer version of Codex ...)"), and the
|
|
8
|
+
* fallback copy told the person to check the sign-in even when the cause was
|
|
9
|
+
* something else. Now every failure is classified here, the person gets one
|
|
10
|
+
* plain sentence plus one next step, and the raw text goes only to the bot
|
|
11
|
+
* log. For the four repairable classes the DM also carries a machine-readable
|
|
12
|
+
* `runtime-repair` block in its `richContent` so the desktop can render a
|
|
13
|
+
* repair card; the plain body stays the fallback.
|
|
14
|
+
*
|
|
15
|
+
* Matching is narrow on purpose: each class has its own known error shapes,
|
|
16
|
+
* and anything unrecognised is `unknown`, which never blames the sign-in.
|
|
17
|
+
*/
|
|
18
|
+
import { isRuntimeSignInFailure, runtimeDisplayName } from "./runtime-sign-in.js";
|
|
19
|
+
export const RUNTIME_REPAIR_ACTIONS = {
|
|
20
|
+
"signed-out": "signIn",
|
|
21
|
+
"cli-outdated": "update",
|
|
22
|
+
"model-unsupported": "switchModel",
|
|
23
|
+
transient: "tryAgain",
|
|
24
|
+
};
|
|
25
|
+
export function isRuntimeRepairClass(cls) {
|
|
26
|
+
return cls in RUNTIME_REPAIR_ACTIONS;
|
|
27
|
+
}
|
|
28
|
+
const NOT_INSTALLED = /\bis not installed or not on PATH\b|\bspawn \S+ ENOENT\b|\bcommand not found\b/i;
|
|
29
|
+
/**
|
|
30
|
+
* The tool says it is too old for the model. Codex: "The 'gpt-6-astra' model
|
|
31
|
+
* requires a newer version of Codex. Please upgrade to the latest app or CLI
|
|
32
|
+
* and try again." Claude Code words it "... requires a newer version of
|
|
33
|
+
* Claude Code".
|
|
34
|
+
*/
|
|
35
|
+
const VERSION_TOO_OLD = /\brequires a newer version of (?:codex|claude code|grok|the (?:app|cli))\b|\bplease upgrade to the latest (?:app or cli|version of (?:codex|claude code|grok))\b/i;
|
|
36
|
+
/** The failing model id inside a version-too-old message, when it names one. */
|
|
37
|
+
const VERSION_TOO_OLD_MODEL = /\bthe ['"`]([^'"`\s]{1,128})['"`] model requires a newer version of\b/i;
|
|
38
|
+
/**
|
|
39
|
+
* Claude Code: "There's an issue with the selected model" (its own wording for
|
|
40
|
+
* a model the account cannot use) and the API's 404 not_found_error whose
|
|
41
|
+
* message starts "model: <id>". Codex and the OpenAI API: model_not_found,
|
|
42
|
+
* "does not exist or you do not have access", "not supported when using
|
|
43
|
+
* Codex with a ChatGPT account".
|
|
44
|
+
*/
|
|
45
|
+
const MODEL_UNSUPPORTED = /there'?s an issue with the selected model|\bnot_found_error\b[^\n]{0,200}\bmodel:\s|\bmodel_not_found\b|\b(?:unknown|invalid|unsupported) model\b|\bthe (?:requested )?model ['"`]?[^'"`\s]{1,128}['"`]? (?:does not exist|is not supported|is not available|was not found|has been deprecated)\b|\bmodel ['"`][^'"`\s]{1,128}['"`] (?:does not exist|is not supported|is not available|not found)\b|\bdo(?:es)? not have access to (?:the )?model\b|\bmodel is not supported when using codex with a chatgpt account\b/i;
|
|
46
|
+
const TRANSIENT = /\brate[_ ]limit|\busage[_ ]limit|\byou've hit your usage limit\b|\btoo many requests\b|\b429\b|\boverloaded\b|\b(?:502|503|504|529)\b|\bservice unavailable\b|\bbad gateway\b|\bgateway time-?out\b|\binternal server error\b|\bECONNRESET\b|\bECONNREFUSED\b|\bETIMEDOUT\b|\bENOTFOUND\b|\bEAI_AGAIN\b|\bENETUNREACH\b|\bsocket hang up\b|\bnetwork (?:error|is unreachable)\b|\bstream (?:disconnected|error)\b|\bconnection (?:reset|refused|closed|error)\b|\btimed out\b|\bexceeded \d+ms \(HQ_BOT_TURN_TIMEOUT_MS\)/i;
|
|
47
|
+
/** Sort a runtime's failure text into one class. Order matters: most specific first. */
|
|
48
|
+
export function classifyRuntimeFailure(text) {
|
|
49
|
+
const t = (text ?? "").trim();
|
|
50
|
+
if (!t)
|
|
51
|
+
return "unknown";
|
|
52
|
+
if (NOT_INSTALLED.test(t))
|
|
53
|
+
return "not-installed";
|
|
54
|
+
if (VERSION_TOO_OLD.test(t))
|
|
55
|
+
return "cli-outdated";
|
|
56
|
+
if (isRuntimeSignInFailure(t))
|
|
57
|
+
return "signed-out";
|
|
58
|
+
if (MODEL_UNSUPPORTED.test(t))
|
|
59
|
+
return "model-unsupported";
|
|
60
|
+
if (TRANSIENT.test(t))
|
|
61
|
+
return "transient";
|
|
62
|
+
return "unknown";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Class of a failed turn's error. A RuntimeError's excerpt (the runtime's own
|
|
66
|
+
* reported error: failureReason, turn.failed, stderr) is the better evidence;
|
|
67
|
+
* its message (what HQ saw, e.g. "codex is not installed or not on PATH")
|
|
68
|
+
* decides when the excerpt says nothing known. An excerpt that is only a tail
|
|
69
|
+
* of the runtime's ordinary output (`excerptIsOutputTail`) is never read: the
|
|
70
|
+
* model may have printed "command not found" or a 503 itself.
|
|
71
|
+
*/
|
|
72
|
+
export function classifyRuntimeError(err) {
|
|
73
|
+
if (err && typeof err === "object" && "stderrExcerpt" in err) {
|
|
74
|
+
const e = err;
|
|
75
|
+
const message = typeof e.message === "string" ? e.message : "";
|
|
76
|
+
if (e.excerptIsOutputTail === true)
|
|
77
|
+
return classifyRuntimeFailure(message);
|
|
78
|
+
const fromExcerpt = classifyRuntimeFailure(typeof e.stderrExcerpt === "string" ? e.stderrExcerpt : "");
|
|
79
|
+
if (fromExcerpt !== "unknown")
|
|
80
|
+
return fromExcerpt;
|
|
81
|
+
return classifyRuntimeFailure(message);
|
|
82
|
+
}
|
|
83
|
+
return classifyRuntimeFailure(err instanceof Error ? err.message : String(err ?? ""));
|
|
84
|
+
}
|
|
85
|
+
export function isRuntimeVersionTooOld(text) {
|
|
86
|
+
return VERSION_TOO_OLD.test((text ?? "").trim());
|
|
87
|
+
}
|
|
88
|
+
/** "gpt-6-astra" from "The 'gpt-6-astra' model requires a newer version of Codex ...". */
|
|
89
|
+
export function versionTooOldModel(text) {
|
|
90
|
+
return VERSION_TOO_OLD_MODEL.exec(text ?? "")?.[1];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The plain reason, as a sentence fragment without a closing period
|
|
94
|
+
* ("my Codex app is out of date"). Used in job run dispositions.
|
|
95
|
+
*/
|
|
96
|
+
export function runtimeFailureReason(cls, runtime) {
|
|
97
|
+
const tool = runtimeDisplayName(runtime);
|
|
98
|
+
switch (cls) {
|
|
99
|
+
case "signed-out":
|
|
100
|
+
return `${tool} needs you to sign in again on this computer`;
|
|
101
|
+
case "cli-outdated":
|
|
102
|
+
return `my ${tool} app is out of date`;
|
|
103
|
+
case "model-unsupported":
|
|
104
|
+
return `the model I'm set to use isn't available in ${tool} here`;
|
|
105
|
+
case "transient":
|
|
106
|
+
return `${tool} is busy or out of reach right now`;
|
|
107
|
+
case "not-installed":
|
|
108
|
+
return `${tool} isn't installed on this computer`;
|
|
109
|
+
default:
|
|
110
|
+
return `${tool} stopped with an error`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** The one next step, as a full sentence. */
|
|
114
|
+
export function runtimeFailureNextStep(cls, runtime, context = "message") {
|
|
115
|
+
const tool = runtimeDisplayName(runtime);
|
|
116
|
+
switch (cls) {
|
|
117
|
+
case "signed-out":
|
|
118
|
+
return `Use "Sign in to ${tool}" in HQ, then send me any message.`;
|
|
119
|
+
case "cli-outdated":
|
|
120
|
+
return `Update ${tool}, then send me any message.`;
|
|
121
|
+
case "model-unsupported":
|
|
122
|
+
return `Choose a different model in my settings, then send me any message.`;
|
|
123
|
+
case "transient":
|
|
124
|
+
return context === "kickoff"
|
|
125
|
+
? `Send me any message in a few minutes and I'll get started.`
|
|
126
|
+
: `Send your message again in a few minutes.`;
|
|
127
|
+
case "not-installed":
|
|
128
|
+
return `Install ${tool} from HQ, then send me any message.`;
|
|
129
|
+
default:
|
|
130
|
+
return context === "kickoff"
|
|
131
|
+
? `Send me any message and I'll try again. If it keeps happening, restart me from HQ.`
|
|
132
|
+
: `Send your message again. If it keeps happening, restart me from HQ.`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function capitalize(s) {
|
|
136
|
+
return s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
137
|
+
}
|
|
138
|
+
/** "My Codex app is out of date. Update Codex, then send me any message." */
|
|
139
|
+
export function runtimeFailureText(cls, runtime, context = "message") {
|
|
140
|
+
return `${capitalize(runtimeFailureReason(cls, runtime))}. ${runtimeFailureNextStep(cls, runtime, context)}`;
|
|
141
|
+
}
|
|
142
|
+
/** A failure after the model started (and maybe posted part of the answer). */
|
|
143
|
+
export function runtimeDidNotFinishText(cls, runtime, progressPosted) {
|
|
144
|
+
return `I didn't finish that. ${runtimeFailureText(cls, runtime)}${progressPosted ? " The messages above show how far I got." : ""}`;
|
|
145
|
+
}
|
|
146
|
+
export function runtimeRepairBlock(args) {
|
|
147
|
+
return {
|
|
148
|
+
v: 1,
|
|
149
|
+
kind: "runtime-repair",
|
|
150
|
+
class: args.cls,
|
|
151
|
+
runtime: args.runtime,
|
|
152
|
+
action: RUNTIME_REPAIR_ACTIONS[args.cls],
|
|
153
|
+
botName: args.botName,
|
|
154
|
+
...(args.retryOf ? { retryOf: args.retryOf } : {}),
|
|
155
|
+
...(args.suggestedModel ? { suggestedModel: args.suggestedModel.slice(0, 128) } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** The plain body plus, for a repairable class, the repair block. */
|
|
159
|
+
export function runtimeRepairNotice(args) {
|
|
160
|
+
const text = args.text ?? runtimeFailureText(args.cls, args.runtime, args.context);
|
|
161
|
+
if (!isRuntimeRepairClass(args.cls))
|
|
162
|
+
return { cls: args.cls, text };
|
|
163
|
+
return {
|
|
164
|
+
cls: args.cls,
|
|
165
|
+
text,
|
|
166
|
+
richContent: {
|
|
167
|
+
v: 1,
|
|
168
|
+
blocks: [
|
|
169
|
+
runtimeRepairBlock({
|
|
170
|
+
cls: args.cls,
|
|
171
|
+
runtime: args.runtime,
|
|
172
|
+
botName: args.botName,
|
|
173
|
+
...(args.retryOf ? { retryOf: args.retryOf } : {}),
|
|
174
|
+
...(args.suggestedModel ? { suggestedModel: args.suggestedModel } : {}),
|
|
175
|
+
}),
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=runtime-failure.js.map
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
/** Time between the bot's own retries while its sign-in is broken. */
|
|
12
12
|
export const SIGN_IN_RETRY_MS = 60_000;
|
|
13
|
-
const SIGN_IN_FAILURE = /failed to authenticate|authentication[_ ]failed|not logged in|not signed in|please (?:run \/login|log ?in|sign ?in)|(?:oauth|access|refresh) token (?:has been |was )?(?:expired|revoked|invalid)|oauth session expired|invalid api key|\b401\b|unauthori[sz]ed|login (?:required|expired)|sign-?in (?:required|expired)/i;
|
|
13
|
+
const SIGN_IN_FAILURE = /failed to authenticate|authentication[_ ]failed|not logged in|not signed in|please (?:run \/login|log ?in|sign ?in)|(?:oauth|access|refresh) token (?:has been |has |was )?(?:expired|revoked|invalid)|oauth session expired|invalid api key|\b401\b|unauthori[sz]ed|login (?:required|expired)|sign-?in (?:required|expired)/i;
|
|
14
14
|
/** Usage and rate limits are a different problem; a new sign-in does not fix them. */
|
|
15
15
|
const NOT_SIGN_IN = /usage[_ ]limit|rate[_ ]limit|quota|too many requests|credits/i;
|
|
16
16
|
export function isRuntimeSignInFailure(text) {
|
|
@@ -26,6 +26,6 @@ export function runtimeDisplayName(runtime) {
|
|
|
26
26
|
export function signInNeededText(runtime, kickoff = false) {
|
|
27
27
|
const tool = runtimeDisplayName(runtime);
|
|
28
28
|
return (`I can't work right now: ${tool} needs you to sign in again on this computer. ` +
|
|
29
|
-
`Use "Sign in to ${tool}" in HQ, and I'll ${kickoff ? "get started" : "answer this"} as soon as it works again
|
|
29
|
+
`Use "Sign in to ${tool}" in HQ, and I'll ${kickoff ? "get started" : "answer this"} as soon as it works again. No need to send it twice.`);
|
|
30
30
|
}
|
|
31
31
|
//# sourceMappingURL=runtime-sign-in.js.map
|
package/dist/lib/bot/status.d.ts
CHANGED
|
@@ -44,6 +44,27 @@ export interface BotStatusFile {
|
|
|
44
44
|
* reason); cleared by the next turn that works. `null` = known to work.
|
|
45
45
|
*/
|
|
46
46
|
runtimeSignIn?: RuntimeSignInIssue | null;
|
|
47
|
+
/**
|
|
48
|
+
* Set when the installed coding tool is too old for the model it was asked
|
|
49
|
+
* to use and the last turn ran on `to` instead (`from` is "" when the model
|
|
50
|
+
* came from the tool's own config). `null` once a turn runs on the model it
|
|
51
|
+
* was asked for again. Updating the tool clears it.
|
|
52
|
+
*/
|
|
53
|
+
runtimeModelFallback?: {
|
|
54
|
+
from: string;
|
|
55
|
+
to: string;
|
|
56
|
+
at: string;
|
|
57
|
+
} | null;
|
|
58
|
+
/**
|
|
59
|
+
* Set while the bot holds its model turns after FAILURE_LIMIT failures in a
|
|
60
|
+
* window (it used to exit instead, and a bot that exited could neither load
|
|
61
|
+
* an updated hq-cli nor answer the next message). `null` once the hold is
|
|
62
|
+
* over; the next message then runs a fresh turn.
|
|
63
|
+
*/
|
|
64
|
+
modelHold?: {
|
|
65
|
+
until: string;
|
|
66
|
+
reason: string;
|
|
67
|
+
} | null;
|
|
47
68
|
/** hq-cli version this process loaded (a bot keeps it until it restarts). */
|
|
48
69
|
cliVersion?: string;
|
|
49
70
|
updatedAt: string;
|
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { basename, countImmediate,
|
|
3
|
+
import { basename, countImmediate, exists, immediateEntries, isHidden, log, prdProjectStatus, readJson, sanitize, titleize, truncate, writeIfChanged } from "./shared.js";
|
|
4
4
|
export class CompanyIndexUserError extends Error {
|
|
5
5
|
constructor(message) {
|
|
6
6
|
super(message);
|
|
7
7
|
this.name = "CompanyIndexUserError";
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* One-line summary for a top-level company directory.
|
|
12
|
+
*
|
|
13
|
+
* Output must be identical on every machine that holds the same synced tree,
|
|
14
|
+
* because company INDEX.md files are synced. So nothing here reads local-only
|
|
15
|
+
* state: `repos/` (never synced), `data/` (partly synced), whether
|
|
16
|
+
* `knowledge/` is an embedded git checkout, or symlink targets. Counts use
|
|
17
|
+
* lstat so a symlinked child is never counted as a directory or file.
|
|
18
|
+
*/
|
|
10
19
|
function description(root, company, name) {
|
|
11
20
|
const base = `companies/${company}/${name}`;
|
|
12
|
-
const
|
|
21
|
+
const realDirectory = (n, f) => !isHidden(n) && fs.lstatSync(f).isDirectory();
|
|
13
22
|
switch (name) {
|
|
14
|
-
case "data": return
|
|
15
|
-
case "knowledge": return
|
|
16
|
-
case "settings": return `${countImmediate(root, base,
|
|
23
|
+
case "data": return "Company data";
|
|
24
|
+
case "knowledge": return `Company knowledge (${countImmediate(root, base, realDirectory)} subdirs)`;
|
|
25
|
+
case "settings": return `${countImmediate(root, base, realDirectory)} integration(s)`;
|
|
17
26
|
case "policies": return `${countImmediate(root, base, (n) => n.endsWith(".md"))} policy file(s)`;
|
|
18
27
|
// Match `find -type d`: count actual directories, not symlinks to directories.
|
|
19
|
-
case "projects": return `${countImmediate(root, base, (n, f) => !n.startsWith("_") &&
|
|
20
|
-
case "workers": return `${countImmediate(root, base,
|
|
21
|
-
case "repos": return
|
|
28
|
+
case "projects": return `${countImmediate(root, base, (n, f) => !n.startsWith("_") && realDirectory(n, f))} project(s)`;
|
|
29
|
+
case "workers": return `${countImmediate(root, base, realDirectory)} worker(s)`;
|
|
30
|
+
case "repos": return "Local repo links (not synced)";
|
|
22
31
|
case "registry": return `Resource registry (${countImmediate(root, `${base}/resources`, (n) => n.endsWith(".yaml"))} resource(s))`;
|
|
23
|
-
case "scripts": return `${countImmediate(root, base, (_n, f) => fs.
|
|
32
|
+
case "scripts": return `${countImmediate(root, base, (_n, f) => fs.lstatSync(f).isFile())} script(s)`;
|
|
24
33
|
default: return "—";
|
|
25
34
|
}
|
|
26
35
|
}
|
|
@@ -56,6 +65,7 @@ export function renderCompanies(context, args = []) {
|
|
|
56
65
|
}
|
|
57
66
|
}
|
|
58
67
|
const written = [];
|
|
68
|
+
const unchanged = [];
|
|
59
69
|
let scopedDirectories;
|
|
60
70
|
if (selectedCompany === undefined) {
|
|
61
71
|
scopedDirectories = immediateEntries(context.root, "companies", "dir");
|
|
@@ -87,7 +97,9 @@ export function renderCompanies(context, args = []) {
|
|
|
87
97
|
const company = basename(directory);
|
|
88
98
|
if (company.startsWith("_") || isHidden(company))
|
|
89
99
|
continue;
|
|
90
|
-
|
|
100
|
+
// No date line: the file must only change when its content changes. The
|
|
101
|
+
// `> Auto-generated.` marker stays, since hq-cloud detects generated INDEX.md by it.
|
|
102
|
+
const lines = [`# ${titleize(company)}`, "", "> Auto-generated.", "", "| Name | Description |", "|------|-------------|"];
|
|
91
103
|
for (const name of ["data", "knowledge", "policies", "projects", "registry", "repos", "scripts", "settings", "workers"]) {
|
|
92
104
|
if (exists(context.root, `companies/${company}/${name}`))
|
|
93
105
|
lines.push(`| \`${name}/\` | ${sanitize(description(context.root, company, name))} |`);
|
|
@@ -104,18 +116,23 @@ export function renderCompanies(context, args = []) {
|
|
|
104
116
|
const parsed = readJson(prd);
|
|
105
117
|
let text = typeof parsed?.description === "string" ? parsed.description : "";
|
|
106
118
|
text = truncate(sanitize(text), 110) || "—";
|
|
107
|
-
lines.push(`| \`${project}\` | ${
|
|
119
|
+
lines.push(`| \`${project}\` | ${prdProjectStatus(prd, "active")} | ${text} |`);
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
else
|
|
111
123
|
lines.push("_No projects directory._");
|
|
112
124
|
lines.push("", "## Deployments", "", "_See `companies/manifest.yaml` for deployment targets._", "");
|
|
113
125
|
const output = `companies/${company}/INDEX.md`;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
if (writeIfChanged(context, output, lines.join("\n"))) {
|
|
127
|
+
written.push(output);
|
|
128
|
+
log(context, `rebuild-companies-index: wrote ${output}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
unchanged.push(output);
|
|
132
|
+
log(context, `rebuild-companies-index: unchanged ${output}`);
|
|
133
|
+
}
|
|
117
134
|
}
|
|
118
|
-
log(context, `rebuild-companies-index: regenerated ${written.length} company INDEX.md file(s)`);
|
|
119
|
-
return { written };
|
|
135
|
+
log(context, `rebuild-companies-index: regenerated ${written.length + unchanged.length} company INDEX.md file(s)`);
|
|
136
|
+
return { written, unchanged };
|
|
120
137
|
}
|
|
121
138
|
//# sourceMappingURL=companies.js.map
|
|
@@ -10,6 +10,8 @@ export type RenderContext = {
|
|
|
10
10
|
};
|
|
11
11
|
export type RenderResult = {
|
|
12
12
|
written: string[];
|
|
13
|
+
/** Relative paths whose rendered content matched the file on disk, so no write happened. */
|
|
14
|
+
unchanged?: string[];
|
|
13
15
|
};
|
|
14
16
|
export declare function rel(root: string, file: string): string;
|
|
15
17
|
export declare function at(root: string, relative: string): string;
|
|
@@ -32,6 +34,15 @@ export declare function atomicWrite(file: string, content: string | Buffer, opti
|
|
|
32
34
|
mode?: number;
|
|
33
35
|
}): void;
|
|
34
36
|
export declare function write(context: RenderContext, relative: string, content: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Write only when the rendered bytes differ from the file on disk. Returns
|
|
39
|
+
* true when the file was written, false when it was already identical.
|
|
40
|
+
*
|
|
41
|
+
* The comparison is exact rather than `generatedFingerprint`: that fingerprint
|
|
42
|
+
* drops keep fences and trailing newlines, which would hide real differences.
|
|
43
|
+
* Callers that want a skip must render deterministic content (no dates).
|
|
44
|
+
*/
|
|
45
|
+
export declare function writeIfChanged(context: RenderContext, relative: string, content: string): boolean;
|
|
35
46
|
/** Drop keep fences, the generated stamp, and the rolling date so two generated bodies can be compared. */
|
|
36
47
|
export declare function generatedFingerprint(content: string): string;
|
|
37
48
|
/** Insert a content stamp after the Auto-generated line. Idempotent. */
|
|
@@ -42,6 +53,13 @@ export declare function looksAutoGenerated(content: string): boolean;
|
|
|
42
53
|
export declare function generatedStamp(content: string): string | undefined;
|
|
43
54
|
export declare function log(context: RenderContext, message: string): void;
|
|
44
55
|
export declare function projectStatus(root: string, project: string, prdPath: string, fallback: string, expectedStatePrdPath?: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Project status from prd.json alone: "complete" when every story passes,
|
|
58
|
+
* otherwise `fallback`. Unlike `projectStatus`, this never reads the
|
|
59
|
+
* machine-local `workspace/orchestrator/<project>/state.json`, so synced
|
|
60
|
+
* output renders the same on every machine.
|
|
61
|
+
*/
|
|
62
|
+
export declare function prdProjectStatus(prdPath: string, fallback: string): string;
|
|
45
63
|
export declare function basename(file: string): string;
|
|
46
64
|
export declare function isHidden(name: string): boolean;
|
|
47
65
|
export declare function mtime(file: string): number;
|
|
@@ -100,6 +100,21 @@ export function write(context, relative, content) {
|
|
|
100
100
|
atomicWrite(at(context.root, relative), content);
|
|
101
101
|
return relative;
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Write only when the rendered bytes differ from the file on disk. Returns
|
|
105
|
+
* true when the file was written, false when it was already identical.
|
|
106
|
+
*
|
|
107
|
+
* The comparison is exact rather than `generatedFingerprint`: that fingerprint
|
|
108
|
+
* drops keep fences and trailing newlines, which would hide real differences.
|
|
109
|
+
* Callers that want a skip must render deterministic content (no dates).
|
|
110
|
+
*/
|
|
111
|
+
export function writeIfChanged(context, relative, content) {
|
|
112
|
+
const file = at(context.root, relative);
|
|
113
|
+
if (readText(file) === content)
|
|
114
|
+
return false;
|
|
115
|
+
atomicWrite(file, content);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
103
118
|
const GENERATED_DATE_LINE = /^> Auto-generated\. Updated: \d{4}-\d{2}-\d{2}$/m;
|
|
104
119
|
const GENERATED_STAMP_LINE = /^<!-- hq-generated-sha256: [a-f0-9]{64} -->\n?/m;
|
|
105
120
|
function keepFencePattern() {
|
|
@@ -149,6 +164,15 @@ export function projectStatus(root, project, prdPath, fallback, expectedStatePrd
|
|
|
149
164
|
return "active";
|
|
150
165
|
return raw;
|
|
151
166
|
}
|
|
167
|
+
return prdProjectStatus(prdPath, fallback);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Project status from prd.json alone: "complete" when every story passes,
|
|
171
|
+
* otherwise `fallback`. Unlike `projectStatus`, this never reads the
|
|
172
|
+
* machine-local `workspace/orchestrator/<project>/state.json`, so synced
|
|
173
|
+
* output renders the same on every machine.
|
|
174
|
+
*/
|
|
175
|
+
export function prdProjectStatus(prdPath, fallback) {
|
|
152
176
|
const prd = readJson(prdPath);
|
|
153
177
|
if (prd && Array.isArray(prd.userStories) && prd.userStories.length > 0 && prd.userStories.every((story) => Boolean(story.passes)))
|
|
154
178
|
return "complete";
|
|
@@ -347,26 +347,41 @@ function defaultProcessCommands() {
|
|
|
347
347
|
return [];
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
|
+
const WATCH_RUNTIME_EXECUTABLES = ["bun", "deno", "node", "nodejs", "ts-node", "tsx"];
|
|
351
|
+
// The hq executable path may contain spaces (the desktop toolchain installs
|
|
352
|
+
// under "~/Library/Application Support/Indigo HQ/..."), so the ps line cannot
|
|
353
|
+
// be split on whitespace to find it. Match the fixed "lanes watch <ids>" suffix
|
|
354
|
+
// on the raw line, then validate whatever precedes it as an hq invocation.
|
|
355
|
+
// A path form (absolute or ./, ../) is required so an unrelated leading
|
|
356
|
+
// command such as "vim /tmp/hq" is not absorbed into the path.
|
|
357
|
+
function isHqExecutablePath(candidate) {
|
|
358
|
+
if (candidate === "hq")
|
|
359
|
+
return true;
|
|
360
|
+
if (!/^\.{0,2}\//u.test(candidate) || path.basename(candidate) !== "hq") {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
return !candidate.split(" ").some((piece) => piece.startsWith("-"));
|
|
364
|
+
}
|
|
365
|
+
function isHqInvocationPrefix(prefix) {
|
|
366
|
+
if (isHqExecutablePath(prefix))
|
|
367
|
+
return true;
|
|
368
|
+
const runtimeEnd = prefix.indexOf(" ");
|
|
369
|
+
if (runtimeEnd === -1)
|
|
370
|
+
return false;
|
|
371
|
+
const runtime = path.basename(prefix.slice(0, runtimeEnd));
|
|
372
|
+
return (WATCH_RUNTIME_EXECUTABLES.includes(runtime) &&
|
|
373
|
+
isHqExecutablePath(prefix.slice(runtimeEnd + 1)));
|
|
374
|
+
}
|
|
350
375
|
function processHasExactCommand(processCommands, command) {
|
|
351
376
|
const laneIds = parseCanonicalWatchCommand(command);
|
|
352
377
|
if (!laneIds)
|
|
353
378
|
return false;
|
|
354
|
-
const expectedSuffix =
|
|
355
|
-
return processCommands.some((
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
if (suffixStart < 1)
|
|
359
|
-
return false;
|
|
360
|
-
if (!expectedSuffix.every((token, index) => tokens[suffixStart + index] === token)) {
|
|
361
|
-
return false;
|
|
362
|
-
}
|
|
363
|
-
const hqToken = tokens[suffixStart - 1];
|
|
364
|
-
if (path.basename(hqToken) !== "hq")
|
|
379
|
+
const expectedSuffix = ` lanes watch ${laneIds.join(" ")}`;
|
|
380
|
+
return processCommands.some((rawLine) => {
|
|
381
|
+
const line = rawLine.trim().replace(/\s+/gu, " ");
|
|
382
|
+
if (!line.endsWith(expectedSuffix))
|
|
365
383
|
return false;
|
|
366
|
-
|
|
367
|
-
return (prefix.length === 0 ||
|
|
368
|
-
(prefix.length === 1 &&
|
|
369
|
-
["bun", "deno", "node", "nodejs", "ts-node", "tsx"].includes(path.basename(prefix[0]))));
|
|
384
|
+
return isHqInvocationPrefix(line.slice(0, -expectedSuffix.length));
|
|
370
385
|
});
|
|
371
386
|
}
|
|
372
387
|
export function readLiveMonitorEvidence(sessionId, options = {}) {
|