@indigoai-us/hq-cli 5.263.0 → 5.265.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 +24 -0
- package/dist/command-catalog.generated.d.ts +16 -0
- package/dist/command-catalog.generated.js +21 -0
- package/dist/commands/lanes-kpis.d.ts +11 -0
- package/dist/commands/lanes-kpis.js +34 -0
- package/dist/commands/lanes-watch.js +2 -1
- package/dist/commands/lanes.js +12 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +0 -11
- package/dist/lib/checkpoint-ontology/plan.js +0 -28
- package/dist/lib/lanes/config.d.ts +2 -0
- package/dist/lib/lanes/config.js +4 -0
- package/dist/lib/lanes/keep-alive.d.ts +2 -2
- package/dist/lib/lanes/keep-alive.js +9 -6
- package/dist/lib/lanes/kr-freshness.d.ts +76 -0
- package/dist/lib/lanes/kr-freshness.js +412 -0
- package/dist/lib/lanes/lane-company.d.ts +9 -0
- package/dist/lib/lanes/lane-company.js +24 -0
- package/dist/lib/lanes/reconcile.js +57 -6
- package/dist/lib/lanes/types.d.ts +20 -1
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.d.ts +2 -0
- package/dist/lib/lanes/watcher.js +70 -1
- package/package.json +1 -1
- package/dist/lib/checkpoint-ontology/hints.d.ts +0 -41
- package/dist/lib/checkpoint-ontology/hints.js +0 -146
|
@@ -49,6 +49,7 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, emitLane
|
|
|
49
49
|
import { envelopeQuestionId } from "./questions.js";
|
|
50
50
|
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
51
51
|
import { blockerFreshnessFingerprint, resolveBlockerFreshnessForLane, } from "./blocker-freshness.js";
|
|
52
|
+
import { krFreshnessFingerprint, resolveKrFreshnessForLane, } from "./kr-freshness.js";
|
|
52
53
|
import { MANAGER_STATUS_GATE_FLAG, managerStatusWasWritten, readStatusSnapshot, resolveManagerStatusTarget, managerStatusGateEnabledForLane, } from "./manager-status-gate.js";
|
|
53
54
|
import { queueLaneTerminalResume, queueManagerStatusGateResume } from "./keep-alive.js";
|
|
54
55
|
import { monitorSessionDir } from "../monitor.js";
|
|
@@ -863,6 +864,72 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
863
864
|
}
|
|
864
865
|
return updated;
|
|
865
866
|
}
|
|
867
|
+
async function settleKrFreshness(hqRoot, lane, manifestTurnId, at, deps, logDecision) {
|
|
868
|
+
if (manifestTurnId !== undefined && lane.status_gate !== undefined && lane.status_gate.turn_id !== manifestTurnId) {
|
|
869
|
+
logDecision({ event: "kr_freshness_baseline_mismatch", at, turn_id: lane.status_gate.turn_id });
|
|
870
|
+
return lane;
|
|
871
|
+
}
|
|
872
|
+
let errorLogged = false;
|
|
873
|
+
const resolution = await resolveKrFreshnessForLane(hqRoot, lane, {
|
|
874
|
+
readFlag: deps.readKrFreshnessFlag ?? readLanesRegistryFlag,
|
|
875
|
+
now: Date.parse(at),
|
|
876
|
+
onError: (error) => {
|
|
877
|
+
if (errorLogged)
|
|
878
|
+
return;
|
|
879
|
+
errorLogged = true;
|
|
880
|
+
const code = error instanceof LanesError ? error.error : "kr_freshness_unavailable";
|
|
881
|
+
process.stderr.write(`hq lanes watcher: KR freshness disabled for ${lane.lane_id}: ${code}\n`);
|
|
882
|
+
logDecision({ event: "kr_freshness_disabled", at, error: code });
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
if (!resolution.enabled || resolution.statusPath === undefined || resolution.error !== undefined)
|
|
886
|
+
return lane;
|
|
887
|
+
const stalled = resolution.kpis.filter((kpi) => kpi.verdict === "stalled");
|
|
888
|
+
if (stalled.length === 0) {
|
|
889
|
+
if (lane.kr_freshness_state === undefined)
|
|
890
|
+
return lane;
|
|
891
|
+
return retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => current.kr_freshness_state === undefined
|
|
892
|
+
? current
|
|
893
|
+
: { ...current, kr_freshness_state: undefined });
|
|
894
|
+
}
|
|
895
|
+
const fingerprint = krFreshnessFingerprint(resolution.stalledKeys);
|
|
896
|
+
const names = stalled.map((kpi) => kpi.name);
|
|
897
|
+
const turnId = manifestTurnId ?? lane.status_gate?.turn_id ?? `unknown-${at}`;
|
|
898
|
+
const resumeText = `These KPIs in ${resolution.statusPath} were not re-measured in the last ${resolution.windowHours}h: ${names.join(", ")}. Measure each one now, update value and measured_at, then end the turn.`;
|
|
899
|
+
return retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
|
|
900
|
+
const history = current.status_gate_history ?? [];
|
|
901
|
+
if (history.some((entry) => entry.event === "kr_stalled" && entry.turn_id === turnId))
|
|
902
|
+
return current;
|
|
903
|
+
const sameStalledSet = current.kr_freshness_state?.stalled_fingerprint === fingerprint;
|
|
904
|
+
let nextLane = current;
|
|
905
|
+
let resumeQueued = false;
|
|
906
|
+
let resumeSkippedReason;
|
|
907
|
+
if (!sameStalledSet) {
|
|
908
|
+
const mergeExisting = current.pending_resume !== undefined &&
|
|
909
|
+
current.state !== "blocked" && current.state !== "awaiting_input";
|
|
910
|
+
const resume = queueLaneTerminalResume(current, at, resumeText, { mergeExisting });
|
|
911
|
+
nextLane = resume.lane;
|
|
912
|
+
resumeQueued = resume.resumeQueued;
|
|
913
|
+
resumeSkippedReason = resume.resumeSkippedReason;
|
|
914
|
+
}
|
|
915
|
+
const entry = {
|
|
916
|
+
event: "kr_stalled",
|
|
917
|
+
turn_id: turnId,
|
|
918
|
+
path: resolution.statusPath,
|
|
919
|
+
at,
|
|
920
|
+
names,
|
|
921
|
+
count: stalled.length,
|
|
922
|
+
fingerprint,
|
|
923
|
+
...(resumeQueued ? { resume_queued: true } : {}),
|
|
924
|
+
...(resumeSkippedReason !== undefined ? { resume_skipped_reason: resumeSkippedReason } : {}),
|
|
925
|
+
};
|
|
926
|
+
return {
|
|
927
|
+
...nextLane,
|
|
928
|
+
kr_freshness_state: { stalled_fingerprint: fingerprint },
|
|
929
|
+
status_gate_history: appendCappedHistory(history, entry),
|
|
930
|
+
};
|
|
931
|
+
});
|
|
932
|
+
}
|
|
866
933
|
function resolveBlockerFreshnessSessionInbox(hqRoot, sessionId) {
|
|
867
934
|
let candidates;
|
|
868
935
|
try {
|
|
@@ -1452,11 +1519,13 @@ async function runWatcherLoopOwned(hqRoot, laneId, deps) {
|
|
|
1452
1519
|
if (terminalWrite.changed) {
|
|
1453
1520
|
updated = await settleManagerStatusGate(hqRoot, updated, manifest.turn_id, tick.emit.at, deps, logDecision);
|
|
1454
1521
|
updated = await settleBlockerFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, deps, mesh, logDecision);
|
|
1522
|
+
updated = await settleKrFreshness(hqRoot, updated, manifest.turn_id, tick.emit.at, deps, logDecision);
|
|
1455
1523
|
}
|
|
1456
1524
|
const managerStatusFollowupQueued = manifest.turn_id !== undefined &&
|
|
1457
1525
|
updated.status_gate_history?.some((entry) => entry.turn_id === manifest.turn_id &&
|
|
1458
1526
|
((entry.event === "status_missed" && entry.resume_queued === true) ||
|
|
1459
|
-
(entry.event === "blocker_stale" && entry.resume_queued === true)
|
|
1527
|
+
(entry.event === "blocker_stale" && entry.resume_queued === true) ||
|
|
1528
|
+
(entry.event === "kr_stalled" && entry.resume_queued === true))) === true;
|
|
1460
1529
|
if (terminalWrite.changed && deps.recoverQueuedMessages && !managerStatusFollowupQueued) {
|
|
1461
1530
|
try {
|
|
1462
1531
|
if (dropboxInboxHasMessages(hqRoot, laneId)) {
|
package/package.json
CHANGED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Existing entities an input names, for `checkpoint.ontology.entity-hints`.
|
|
3
|
-
*
|
|
4
|
-
* The model never sees the company's entities, so it can only link one when it
|
|
5
|
-
* happens to pick a name or alias the index holds. On the CO-39 fixtures it
|
|
6
|
-
* created `SpaceXAI` beside `xAI`, `Indigo desktop app` beside `indigo-nx`
|
|
7
|
-
* (alias `Indigo desktop repo`) and `hq-work-site` beside `Indigo Workspace`
|
|
8
|
-
* (workspace/checkpoint-ontology/analysis/co41-wrong-creates.json). Each
|
|
9
|
-
* input's plan entry lists the existing entities whose name the input text
|
|
10
|
-
* contains, so the model can use that name.
|
|
11
|
-
*/
|
|
12
|
-
import type { EntityIndex } from "./tree.js";
|
|
13
|
-
export declare const ENTITY_HINTS: {
|
|
14
|
-
/** Entities listed per input. */
|
|
15
|
-
readonly perInput: 25;
|
|
16
|
-
/** An alias match counts this fraction of a canonical-name match with the same words. */
|
|
17
|
-
readonly aliasWeight: 0.5;
|
|
18
|
-
/** Aliases shown per entity. */
|
|
19
|
-
readonly aliases: 3;
|
|
20
|
-
/** Longest name, in words, matched through the n-gram set. Longer names use a phrase search. */
|
|
21
|
-
readonly maxGram: 4;
|
|
22
|
-
};
|
|
23
|
-
export interface EntityHint {
|
|
24
|
-
type: string;
|
|
25
|
-
name: string;
|
|
26
|
-
aliases: string[];
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Inverse document frequency of each word over the entities' match names, so
|
|
30
|
-
* `Sentry` outranks `HQ Build` in a document that names both.
|
|
31
|
-
*/
|
|
32
|
-
export declare function wordWeights(index: EntityIndex): Map<string, number>;
|
|
33
|
-
/**
|
|
34
|
-
* Up to `ENTITY_HINTS.perInput` writable entities whose canonical name or
|
|
35
|
-
* qualifying alias appears in `text` as whole words within one sentence, table
|
|
36
|
-
* cell or markdown block, highest weight first (ties by path). A match scores the summed word weights, halved for an alias.
|
|
37
|
-
* Entities with the same type and canonical name are listed once. Only
|
|
38
|
-
* entities from `index`, which is the plan's own company.
|
|
39
|
-
*/
|
|
40
|
-
export declare function entityHints(index: EntityIndex, weights: Map<string, number>, text: string): EntityHint[];
|
|
41
|
-
//# sourceMappingURL=hints.d.ts.map
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Existing entities an input names, for `checkpoint.ontology.entity-hints`.
|
|
3
|
-
*
|
|
4
|
-
* The model never sees the company's entities, so it can only link one when it
|
|
5
|
-
* happens to pick a name or alias the index holds. On the CO-39 fixtures it
|
|
6
|
-
* created `SpaceXAI` beside `xAI`, `Indigo desktop app` beside `indigo-nx`
|
|
7
|
-
* (alias `Indigo desktop repo`) and `hq-work-site` beside `Indigo Workspace`
|
|
8
|
-
* (workspace/checkpoint-ontology/analysis/co41-wrong-creates.json). Each
|
|
9
|
-
* input's plan entry lists the existing entities whose name the input text
|
|
10
|
-
* contains, so the model can use that name.
|
|
11
|
-
*/
|
|
12
|
-
export const ENTITY_HINTS = {
|
|
13
|
-
/** Entities listed per input. */
|
|
14
|
-
perInput: 25,
|
|
15
|
-
/** An alias match counts this fraction of a canonical-name match with the same words. */
|
|
16
|
-
aliasWeight: 0.5,
|
|
17
|
-
/** Aliases shown per entity. */
|
|
18
|
-
aliases: 3,
|
|
19
|
-
/** Longest name, in words, matched through the n-gram set. Longer names use a phrase search. */
|
|
20
|
-
maxGram: 4,
|
|
21
|
-
};
|
|
22
|
-
const HINT_TYPES = new Set(["person", "project", "company", "concept"]);
|
|
23
|
-
/** Token that stands for a sentence, cell or block boundary. A name never matches across one it does not contain. */
|
|
24
|
-
const BOUNDARY = "\n";
|
|
25
|
-
const TOKEN = /[\p{L}\p{N}]+|[.!?;:](?=\s|$)|\|/gu;
|
|
26
|
-
/**
|
|
27
|
-
* A line that continues the previous one when the previous one is prose or a
|
|
28
|
-
* list item: hard-wrapped markdown keeps a name that wraps across the line
|
|
29
|
-
* break. Blank lines, headings, list items, table rows, quotes and fences start
|
|
30
|
-
* a new block.
|
|
31
|
-
*/
|
|
32
|
-
function continuesProse(line) {
|
|
33
|
-
return line.trim() !== "" && !/^\s*(#|[-*+]\s|\d+[.)]\s|\||>|```|~~~)/.test(line);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Lowercased words, with `BOUNDARY` between sentences (`.`, `!`, `?`, `;` or
|
|
37
|
-
* `:` before whitespace), table cells and markdown blocks. Names and text are
|
|
38
|
-
* split the same way, so a name that holds a boundary, such as `Tip 001:
|
|
39
|
-
* checkpoint`, still matches its own text.
|
|
40
|
-
*/
|
|
41
|
-
function tokens(text) {
|
|
42
|
-
const out = [];
|
|
43
|
-
const push = (token) => {
|
|
44
|
-
if (token !== BOUNDARY || (out.length > 0 && out[out.length - 1] !== BOUNDARY))
|
|
45
|
-
out.push(token);
|
|
46
|
-
};
|
|
47
|
-
let prevProse = null;
|
|
48
|
-
for (const line of text.toLowerCase().split("\n")) {
|
|
49
|
-
const prose = continuesProse(line);
|
|
50
|
-
if (prevProse !== null && !(prose && prevProse))
|
|
51
|
-
push(BOUNDARY);
|
|
52
|
-
for (const match of line.matchAll(TOKEN))
|
|
53
|
-
push(/^[\p{L}\p{N}]/u.test(match[0]) ? match[0] : BOUNDARY);
|
|
54
|
-
prevProse = prose || /^\s*([-*+]|\d+[.)])\s/.test(line);
|
|
55
|
-
}
|
|
56
|
-
while (out[0] === BOUNDARY)
|
|
57
|
-
out.shift();
|
|
58
|
-
while (out[out.length - 1] === BOUNDARY)
|
|
59
|
-
out.pop();
|
|
60
|
-
return out;
|
|
61
|
-
}
|
|
62
|
-
function words(name) {
|
|
63
|
-
return name.filter((token) => token !== BOUNDARY);
|
|
64
|
-
}
|
|
65
|
-
function matchNames(entity) {
|
|
66
|
-
const out = [];
|
|
67
|
-
const canonical = tokens(entity.canonical_name);
|
|
68
|
-
if (words(canonical).length > 0)
|
|
69
|
-
out.push({ tokens: canonical, weight: 1 });
|
|
70
|
-
for (const alias of entity.aliases) {
|
|
71
|
-
const aliasTokens = tokens(alias);
|
|
72
|
-
const count = words(aliasTokens).length;
|
|
73
|
-
if (count >= 2 || (count === 1 && /[-_./0-9]/.test(alias))) {
|
|
74
|
-
out.push({ tokens: aliasTokens, weight: ENTITY_HINTS.aliasWeight, alias });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return out;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Inverse document frequency of each word over the entities' match names, so
|
|
81
|
-
* `Sentry` outranks `HQ Build` in a document that names both.
|
|
82
|
-
*/
|
|
83
|
-
export function wordWeights(index) {
|
|
84
|
-
const counts = new Map();
|
|
85
|
-
let total = 0;
|
|
86
|
-
for (const entity of index.byRel.values()) {
|
|
87
|
-
if (!HINT_TYPES.has(entity.type))
|
|
88
|
-
continue;
|
|
89
|
-
total += 1;
|
|
90
|
-
for (const word of new Set(matchNames(entity).flatMap((name) => words(name.tokens))))
|
|
91
|
-
counts.set(word, (counts.get(word) ?? 0) + 1);
|
|
92
|
-
}
|
|
93
|
-
const weights = new Map();
|
|
94
|
-
for (const [word, count] of counts)
|
|
95
|
-
weights.set(word, Math.log((total + 1) / count));
|
|
96
|
-
return weights;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Up to `ENTITY_HINTS.perInput` writable entities whose canonical name or
|
|
100
|
-
* qualifying alias appears in `text` as whole words within one sentence, table
|
|
101
|
-
* cell or markdown block, highest weight first (ties by path). A match scores the summed word weights, halved for an alias.
|
|
102
|
-
* Entities with the same type and canonical name are listed once. Only
|
|
103
|
-
* entities from `index`, which is the plan's own company.
|
|
104
|
-
*/
|
|
105
|
-
export function entityHints(index, weights, text) {
|
|
106
|
-
const textTokens = tokens(text);
|
|
107
|
-
const grams = new Set();
|
|
108
|
-
for (let i = 0; i < textTokens.length; i += 1) {
|
|
109
|
-
for (let n = 1; n <= ENTITY_HINTS.maxGram && i + n <= textTokens.length; n += 1) {
|
|
110
|
-
grams.add(textTokens.slice(i, i + n).join(" "));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const padded = ` ${textTokens.join(" ")} `;
|
|
114
|
-
const best = new Map();
|
|
115
|
-
for (const entity of index.byRel.values()) {
|
|
116
|
-
if (!HINT_TYPES.has(entity.type))
|
|
117
|
-
continue;
|
|
118
|
-
let score = 0;
|
|
119
|
-
const matched = [];
|
|
120
|
-
for (const name of matchNames(entity)) {
|
|
121
|
-
const phrase = name.tokens.join(" ");
|
|
122
|
-
const found = name.tokens.length <= ENTITY_HINTS.maxGram ? grams.has(phrase) : padded.includes(` ${phrase} `);
|
|
123
|
-
if (!found)
|
|
124
|
-
continue;
|
|
125
|
-
if (name.alias !== undefined)
|
|
126
|
-
matched.push(name.alias);
|
|
127
|
-
score = Math.max(score, name.weight * words(name.tokens).reduce((sum, word) => sum + (weights.get(word) ?? 0), 0));
|
|
128
|
-
}
|
|
129
|
-
if (score <= 0)
|
|
130
|
-
continue;
|
|
131
|
-
const key = `${entity.type}\0${entity.canonical_name.toLowerCase()}`;
|
|
132
|
-
const held = best.get(key);
|
|
133
|
-
if (!held || score > held.score || (score === held.score && entity.rel < held.entity.rel))
|
|
134
|
-
best.set(key, { entity, score, matched });
|
|
135
|
-
}
|
|
136
|
-
return [...best.values()]
|
|
137
|
-
.sort((a, b) => b.score - a.score || (a.entity.rel < b.entity.rel ? -1 : a.entity.rel > b.entity.rel ? 1 : 0))
|
|
138
|
-
.slice(0, ENTITY_HINTS.perInput)
|
|
139
|
-
.map(({ entity, matched }) => ({
|
|
140
|
-
type: entity.type,
|
|
141
|
-
name: entity.canonical_name,
|
|
142
|
-
// Aliases the text used come first, so the model sees the name it read.
|
|
143
|
-
aliases: [...matched, ...entity.aliases.filter((alias) => !matched.includes(alias))].slice(0, ENTITY_HINTS.aliases),
|
|
144
|
-
}));
|
|
145
|
-
}
|
|
146
|
-
//# sourceMappingURL=hints.js.map
|