@indigoai-us/hq-cli 5.291.0 → 5.293.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 +53 -8
- package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
- package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
- package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
- package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
- package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
- package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
- package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
- package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
- package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
- package/dist/command-catalog.generated.d.ts +109 -1
- package/dist/command-catalog.generated.js +136 -1
- package/dist/commands/core-delegate-w1b.d.ts +8 -0
- package/dist/commands/core-delegate-w1b.js +1049 -0
- package/dist/commands/core-delegate.d.ts +28 -0
- package/dist/commands/core-delegate.js +15 -13
- package/dist/commands/core-handoff.js +152 -23
- package/dist/commands/core-jobs-native.d.ts +15 -0
- package/dist/commands/core-jobs-native.js +709 -2
- package/dist/commands/core-ontology.d.ts +22 -4
- package/dist/commands/core-ontology.js +90 -8
- package/dist/commands/core.js +10 -0
- package/dist/commands/lanes-move.js +54 -1
- package/dist/commands/lanes-questions.js +4 -0
- package/dist/commands/lanes.d.ts +2 -0
- package/dist/commands/lanes.js +119 -5
- package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
- package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
- package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
- package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
- package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
- package/dist/lib/core-utils/sentry-report.js +2 -2
- package/dist/lib/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +2 -0
- package/dist/lib/daemon/run.d.ts +11 -0
- package/dist/lib/daemon/run.js +17 -1
- package/dist/lib/lanes/autopilot.d.ts +13 -1
- package/dist/lib/lanes/autopilot.js +271 -28
- package/dist/lib/lanes/grants.d.ts +1 -1
- package/dist/lib/lanes/grants.js +7 -5
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +5 -0
- package/dist/lib/lanes/keep-alive.js +9 -4
- package/dist/lib/lanes/questions.d.ts +2 -1
- package/dist/lib/lanes/questions.js +2 -0
- package/dist/lib/lanes/reconcile.js +186 -9
- package/dist/lib/lanes/spawn.js +4 -0
- package/dist/lib/lanes/store.js +21 -1
- package/dist/lib/lanes/types.d.ts +13 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -1
- package/dist/utils/scaffold-sibling-references.js +4 -4
- package/dist/utils/self-update.d.ts +13 -0
- package/dist/utils/self-update.js +33 -7
- package/dist/utils/staged-install.d.ts +34 -0
- package/dist/utils/staged-install.js +296 -0
- package/dist/utils/version-gate.d.ts +15 -0
- package/dist/utils/version-gate.js +46 -2
- package/package.json +1 -1
- package/scripts/mesh-daemon-if-stale.mjs +4 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq core ontology drop-aliases`: remove reviewed wrong aliases from entity
|
|
3
|
+
* files (CO-74). An alias that names a different entity joins two unrelated
|
|
4
|
+
* entities into one duplicate group; a person lists each `{ entity, alias }`
|
|
5
|
+
* to remove, and nothing else changes. The dry run writes nothing; `apply`
|
|
6
|
+
* writes the entity files through the multi-writer outbox so sync pushes them.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { entitiesRoot, safeWriteFile } from "./company.js";
|
|
11
|
+
import { patchEntityFile } from "./entity-file.js";
|
|
12
|
+
import { isWritableEntityType } from "./identity.js";
|
|
13
|
+
import { findDuplicateGroups, recordOutbox } from "./multi-writer.js";
|
|
14
|
+
import { buildEntityIndex } from "./tree.js";
|
|
15
|
+
export const DROP_ALIASES_FLAG = "checkpoint.ontology.drop-aliases";
|
|
16
|
+
export function dropAliases(options) {
|
|
17
|
+
const { hqRoot, company, mode, drops, now } = options;
|
|
18
|
+
const index = buildEntityIndex(company.dir);
|
|
19
|
+
const report = {
|
|
20
|
+
company: company.slug,
|
|
21
|
+
mode,
|
|
22
|
+
duplicate_groups_before: findDuplicateGroups(index).length,
|
|
23
|
+
duplicate_groups_after: 0,
|
|
24
|
+
planned: {},
|
|
25
|
+
written: [],
|
|
26
|
+
skipped: [],
|
|
27
|
+
errors: [],
|
|
28
|
+
};
|
|
29
|
+
const byEntity = new Map();
|
|
30
|
+
for (const drop of drops) {
|
|
31
|
+
const entity = index.byRel.get(drop.entity);
|
|
32
|
+
if (!entity) {
|
|
33
|
+
report.skipped.push({ ...drop, reason: "no_entity" });
|
|
34
|
+
}
|
|
35
|
+
else if (!isWritableEntityType(entity.type)) {
|
|
36
|
+
report.skipped.push({ ...drop, reason: "type_not_writable" });
|
|
37
|
+
}
|
|
38
|
+
else if (!entity.aliases.includes(drop.alias)) {
|
|
39
|
+
report.skipped.push({ ...drop, reason: "alias_absent" });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
byEntity.set(drop.entity, [...(byEntity.get(drop.entity) ?? []), drop]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const writes = new Map();
|
|
46
|
+
for (const [rel, list] of [...byEntity].sort(([a], [b]) => a.localeCompare(b))) {
|
|
47
|
+
const content = fs.readFileSync(path.join(company.dir, rel), "utf8");
|
|
48
|
+
const patched = patchEntityFile(content, { removeAliases: list.map((drop) => drop.alias), lastUpdated: now.toISOString() });
|
|
49
|
+
if (!patched.ok) {
|
|
50
|
+
for (const drop of list)
|
|
51
|
+
report.skipped.push({ ...drop, reason: "patch_failed", detail: patched.reason });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
report.planned[rel] = [...new Set(list.map((drop) => drop.alias))];
|
|
55
|
+
writes.set(rel, patched.content);
|
|
56
|
+
}
|
|
57
|
+
if (mode === "apply") {
|
|
58
|
+
for (const [rel, content] of writes) {
|
|
59
|
+
try {
|
|
60
|
+
// Outbox first: sync pushes an entity file only when its bytes match.
|
|
61
|
+
recordOutbox(hqRoot, company, [{ rel, content }], now);
|
|
62
|
+
safeWriteFile(hqRoot, [entitiesRoot(company)], path.join(company.dir, rel), content);
|
|
63
|
+
report.written.push(rel);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
report.errors.push({ path: rel, error: error.name });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
report.duplicate_groups_after = findDuplicateGroups(buildEntityIndex(company.dir)).length;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
report.duplicate_groups_after = report.duplicate_groups_before;
|
|
73
|
+
}
|
|
74
|
+
return report;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=drop-aliases.js.map
|
|
@@ -47,6 +47,8 @@ export declare function renderNewEntity(entity: NewEntity): string;
|
|
|
47
47
|
export declare const OWNED_KEYS: ReadonlySet<string>;
|
|
48
48
|
export interface EntityPatch {
|
|
49
49
|
addAliases?: string[];
|
|
50
|
+
/** Exact alias strings to remove (reviewed wrong aliases). Applied before `addAliases`. */
|
|
51
|
+
removeAliases?: string[];
|
|
50
52
|
signalCount?: number;
|
|
51
53
|
lastUpdated: string;
|
|
52
54
|
/** The caller changed the body, so `last_updated` moves even with no key change. */
|
|
@@ -61,9 +63,10 @@ export type PatchResult = {
|
|
|
61
63
|
reason: string;
|
|
62
64
|
};
|
|
63
65
|
/**
|
|
64
|
-
* Patch an existing, valid entity file. Aliases are
|
|
65
|
-
* `
|
|
66
|
-
* change, the file is returned
|
|
66
|
+
* Patch an existing, valid entity file. Aliases are appended, and removed
|
|
67
|
+
* only when `removeAliases` names them exactly; `signal_count` never goes
|
|
68
|
+
* down. When nothing but `last_updated` would change, the file is returned
|
|
69
|
+
* unchanged so a rerun writes nothing.
|
|
67
70
|
*/
|
|
68
71
|
export declare function patchEntityFile(content: string, patch: EntityPatch): PatchResult;
|
|
69
72
|
/** A namespaced record in a source system, as the cloud schema's `external_refs` holds it. */
|
|
@@ -144,9 +144,10 @@ function appendAliases(lines, all, added) {
|
|
|
144
144
|
return [...lines.slice(0, end), ...added.map((alias) => `${indent}- ${quote(alias)}`), ...lines.slice(end)];
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
|
-
* Patch an existing, valid entity file. Aliases are
|
|
148
|
-
* `
|
|
149
|
-
* change, the file is returned
|
|
147
|
+
* Patch an existing, valid entity file. Aliases are appended, and removed
|
|
148
|
+
* only when `removeAliases` names them exactly; `signal_count` never goes
|
|
149
|
+
* down. When nothing but `last_updated` would change, the file is returned
|
|
150
|
+
* unchanged so a rerun writes nothing.
|
|
150
151
|
*/
|
|
151
152
|
export function patchEntityFile(content, patch) {
|
|
152
153
|
const match = FRONTMATTER_RE.exec(content);
|
|
@@ -156,19 +157,24 @@ export function patchEntityFile(content, patch) {
|
|
|
156
157
|
const before = validateEntityFrontmatter(parsed.frontmatter);
|
|
157
158
|
if (typeof before === "string")
|
|
158
159
|
return { ok: false, reason: `invalid_existing:${before}` };
|
|
159
|
-
const
|
|
160
|
+
const remove = new Set(patch.removeAliases ?? []);
|
|
161
|
+
const kept = before.aliases.filter((alias) => !remove.has(alias));
|
|
162
|
+
const aliases = [...kept];
|
|
160
163
|
for (const alias of patch.addAliases ?? []) {
|
|
161
164
|
if (!aliases.includes(alias))
|
|
162
165
|
aliases.push(alias);
|
|
163
166
|
}
|
|
164
167
|
const signalCount = Math.max(before.signal_count, patch.signalCount ?? 0);
|
|
165
|
-
const
|
|
168
|
+
const removed = kept.length !== before.aliases.length;
|
|
169
|
+
const aliasesChanged = removed || aliases.length !== before.aliases.length;
|
|
166
170
|
const countChanged = signalCount !== before.signal_count;
|
|
167
171
|
if (!aliasesChanged && !countChanged && !patch.bodyChanged)
|
|
168
172
|
return { ok: true, content, changed: false };
|
|
169
173
|
const newline = content.includes("\r\n") ? "\r\n" : "\n";
|
|
170
174
|
let lines = match[1].split(/\r?\n/);
|
|
171
|
-
if (
|
|
175
|
+
if (removed)
|
|
176
|
+
lines = replaceTopLevelKey(lines, "aliases", renderAliases(aliases));
|
|
177
|
+
else if (aliasesChanged)
|
|
172
178
|
lines = appendAliases(lines, aliases, aliases.slice(before.aliases.length));
|
|
173
179
|
if (lines && countChanged)
|
|
174
180
|
lines = replaceTopLevelKey(lines, "signal_count", `signal_count: ${signalCount}`);
|
|
@@ -27,16 +27,25 @@
|
|
|
27
27
|
*
|
|
28
28
|
* A signal whose `entity_refs` names a copy holds that copy: signals are
|
|
29
29
|
* server-owned, so the ref cannot be re-pointed from here. With `rewriteRefs`
|
|
30
|
-
* (flag `checkpoint.ontology.merge-rewrite-refs`)
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* (flag `checkpoint.ontology.merge-rewrite-refs`) invoice and bill links and
|
|
31
|
+
* knowledge links are re-pointed at the survivor and the survivor receives
|
|
32
|
+
* the copy's `external_ids` and `web_domains`. A signal ref or a link from a
|
|
33
|
+
* source still holds the pair (`server_owned_refs`): the sync client cannot
|
|
34
|
+
* push those files. A link from another copy or an external id conflict also
|
|
35
|
+
* holds a pair.
|
|
34
36
|
*
|
|
35
37
|
* With `pairKind: "variants"` (flag `checkpoint.ontology.merge-variants`) the
|
|
36
38
|
* pairs are two-file duplicate groups whose canonical names differ only in
|
|
37
39
|
* case, spacing or punctuation; the survivor is the spaced name, then the
|
|
38
40
|
* higher `signal_count`, then path order. The copy's canonical name becomes
|
|
39
41
|
* an alias of the survivor. Merge and delete work as for numbered copies.
|
|
42
|
+
*
|
|
43
|
+
* With `pairKind: "listed"` (flag `checkpoint.ontology.merge-listed-pairs`)
|
|
44
|
+
* the pairs are exactly the `only` entries, reviewed by a person: no finder
|
|
45
|
+
* runs, so the names may differ. Each entry must name two entities of one
|
|
46
|
+
* type; a copy listed twice, or listed both as a copy and as an original, is
|
|
47
|
+
* skipped as `listed_conflict`. Holds, merge and delete work as for numbered
|
|
48
|
+
* copies.
|
|
40
49
|
*/
|
|
41
50
|
import { type OntologyCompany } from "./company.js";
|
|
42
51
|
import { type EntityIndex } from "./tree.js";
|
|
@@ -46,14 +55,16 @@ export declare const MERGE_COPIES_FLAG = "checkpoint.ontology.merge-copies";
|
|
|
46
55
|
* names differ only in spacing, case or punctuation (CO-65).
|
|
47
56
|
*/
|
|
48
57
|
export declare const MERGE_VARIANTS_FLAG = "checkpoint.ontology.merge-variants";
|
|
58
|
+
/** `merge-copies --listed`: merge exactly the reviewed `--only` pairs (CO-74). */
|
|
59
|
+
export declare const MERGE_LISTED_PAIRS_FLAG = "checkpoint.ontology.merge-listed-pairs";
|
|
49
60
|
/**
|
|
50
61
|
* `merge-copies --rewrite-refs`: re-point signal `entity_refs`, invoice and
|
|
51
62
|
* bill links, source and knowledge links from a copy to its survivor, and
|
|
52
63
|
* carry the copy's `external_ids` and `web_domains` to the survivor.
|
|
53
64
|
*/
|
|
54
65
|
export declare const MERGE_REWRITE_REFS_FLAG = "checkpoint.ontology.merge-rewrite-refs";
|
|
55
|
-
export type CopySkipReason = "no_original" | "original_invalid" | "identity_differs" | "not_in_duplicate_group" | "group_size" | "type_differs" | "names_differ" | "numbered_copy" | "not_listed";
|
|
56
|
-
export type PairKind = "numbered" | "variants";
|
|
66
|
+
export type CopySkipReason = "no_original" | "original_invalid" | "identity_differs" | "not_in_duplicate_group" | "group_size" | "type_differs" | "names_differ" | "numbered_copy" | "not_listed" | "listed_conflict";
|
|
67
|
+
export type PairKind = "numbered" | "variants" | "listed";
|
|
57
68
|
export interface CopySkip {
|
|
58
69
|
copy: string;
|
|
59
70
|
reason: CopySkipReason;
|
|
@@ -97,7 +108,7 @@ export interface CopyPlan extends CopyPair {
|
|
|
97
108
|
/** Signal files whose `entity_refs` name the copy. */
|
|
98
109
|
signal_refs: string[];
|
|
99
110
|
/** Why the copy cannot be merged or deleted from here. */
|
|
100
|
-
held?: "signal_refs" | "linked_from_unwritable" | "merge_invalid" | "external_id_conflict";
|
|
111
|
+
held?: "signal_refs" | "linked_from_unwritable" | "merge_invalid" | "external_id_conflict" | "server_owned_refs";
|
|
101
112
|
detail?: string;
|
|
102
113
|
/** Keys added to the survivor. Set only when `rewriteRefs` is on. */
|
|
103
114
|
add_external_ids?: string[];
|
|
@@ -133,7 +144,7 @@ export interface MergeCopiesReport {
|
|
|
133
144
|
}[];
|
|
134
145
|
/** Listed entries the finder did not produce. Absent unless `--only` was set. */
|
|
135
146
|
only_unmatched?: ListedPair[];
|
|
136
|
-
/**
|
|
147
|
+
/** Knowledge files re-pointed at a survivor. Absent unless `rewriteRefs` was set. */
|
|
137
148
|
rewritten?: string[];
|
|
138
149
|
/** Entity-file links whose target file does not exist. */
|
|
139
150
|
broken_links_before: number;
|
|
@@ -171,6 +182,14 @@ export declare function findVariantPairs(index: EntityIndex): {
|
|
|
171
182
|
pairs: CopyPair[];
|
|
172
183
|
skipped: CopySkip[];
|
|
173
184
|
};
|
|
185
|
+
/**
|
|
186
|
+
* The reviewed pairs themselves, for `pairKind: "listed"`. An entry is kept
|
|
187
|
+
* when both files are indexed entities of one writable type. Read-only.
|
|
188
|
+
*/
|
|
189
|
+
export declare function findListedPairs(index: EntityIndex, only: ListedPair[]): {
|
|
190
|
+
pairs: CopyPair[];
|
|
191
|
+
skipped: CopySkip[];
|
|
192
|
+
};
|
|
174
193
|
/**
|
|
175
194
|
* Plan, merge or delete numbered copies for one company. The dry run writes
|
|
176
195
|
* nothing; merge writes entity files only; delete removes vault keys and
|
|
@@ -27,16 +27,25 @@
|
|
|
27
27
|
*
|
|
28
28
|
* A signal whose `entity_refs` names a copy holds that copy: signals are
|
|
29
29
|
* server-owned, so the ref cannot be re-pointed from here. With `rewriteRefs`
|
|
30
|
-
* (flag `checkpoint.ontology.merge-rewrite-refs`)
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* (flag `checkpoint.ontology.merge-rewrite-refs`) invoice and bill links and
|
|
31
|
+
* knowledge links are re-pointed at the survivor and the survivor receives
|
|
32
|
+
* the copy's `external_ids` and `web_domains`. A signal ref or a link from a
|
|
33
|
+
* source still holds the pair (`server_owned_refs`): the sync client cannot
|
|
34
|
+
* push those files. A link from another copy or an external id conflict also
|
|
35
|
+
* holds a pair.
|
|
34
36
|
*
|
|
35
37
|
* With `pairKind: "variants"` (flag `checkpoint.ontology.merge-variants`) the
|
|
36
38
|
* pairs are two-file duplicate groups whose canonical names differ only in
|
|
37
39
|
* case, spacing or punctuation; the survivor is the spaced name, then the
|
|
38
40
|
* higher `signal_count`, then path order. The copy's canonical name becomes
|
|
39
41
|
* an alias of the survivor. Merge and delete work as for numbered copies.
|
|
42
|
+
*
|
|
43
|
+
* With `pairKind: "listed"` (flag `checkpoint.ontology.merge-listed-pairs`)
|
|
44
|
+
* the pairs are exactly the `only` entries, reviewed by a person: no finder
|
|
45
|
+
* runs, so the names may differ. Each entry must name two entities of one
|
|
46
|
+
* type; a copy listed twice, or listed both as a copy and as an original, is
|
|
47
|
+
* skipped as `listed_conflict`. Holds, merge and delete work as for numbered
|
|
48
|
+
* copies.
|
|
40
49
|
*/
|
|
41
50
|
import * as fs from "node:fs";
|
|
42
51
|
import * as path from "node:path";
|
|
@@ -53,6 +62,8 @@ export const MERGE_COPIES_FLAG = "checkpoint.ontology.merge-copies";
|
|
|
53
62
|
* names differ only in spacing, case or punctuation (CO-65).
|
|
54
63
|
*/
|
|
55
64
|
export const MERGE_VARIANTS_FLAG = "checkpoint.ontology.merge-variants";
|
|
65
|
+
/** `merge-copies --listed`: merge exactly the reviewed `--only` pairs (CO-74). */
|
|
66
|
+
export const MERGE_LISTED_PAIRS_FLAG = "checkpoint.ontology.merge-listed-pairs";
|
|
56
67
|
/**
|
|
57
68
|
* `merge-copies --rewrite-refs`: re-point signal `entity_refs`, invoice and
|
|
58
69
|
* bill links, source and knowledge links from a copy to its survivor, and
|
|
@@ -219,6 +230,36 @@ export function findVariantPairs(index) {
|
|
|
219
230
|
pairs.sort((a, b) => a.copy.localeCompare(b.copy));
|
|
220
231
|
return { pairs, skipped };
|
|
221
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* The reviewed pairs themselves, for `pairKind: "listed"`. An entry is kept
|
|
235
|
+
* when both files are indexed entities of one writable type. Read-only.
|
|
236
|
+
*/
|
|
237
|
+
export function findListedPairs(index, only) {
|
|
238
|
+
const pairs = [];
|
|
239
|
+
const skipped = [];
|
|
240
|
+
const copyCount = new Map();
|
|
241
|
+
for (const entry of only)
|
|
242
|
+
copyCount.set(entry.copy, (copyCount.get(entry.copy) ?? 0) + 1);
|
|
243
|
+
const originals = new Set(only.map((entry) => entry.original));
|
|
244
|
+
for (const entry of only) {
|
|
245
|
+
const copy = index.byRel.get(entry.copy);
|
|
246
|
+
const original = index.byRel.get(entry.original);
|
|
247
|
+
if (copyCount.get(entry.copy) > 1 || originals.has(entry.copy) || entry.copy === entry.original) {
|
|
248
|
+
skipped.push({ copy: entry.copy, reason: "listed_conflict" });
|
|
249
|
+
}
|
|
250
|
+
else if (!copy || !original) {
|
|
251
|
+
skipped.push({ copy: entry.copy, reason: index.invalid.has(entry.original) ? "original_invalid" : "no_original" });
|
|
252
|
+
}
|
|
253
|
+
else if (copy.type !== original.type || !isWritableEntityType(copy.type)) {
|
|
254
|
+
skipped.push({ copy: entry.copy, reason: "type_differs" });
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
pairs.push({ type: copy.type, copy: entry.copy, original: entry.original });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
pairs.sort((a, b) => a.copy.localeCompare(b.copy));
|
|
261
|
+
return { pairs, skipped };
|
|
262
|
+
}
|
|
222
263
|
/** Resolve a relative link in `fromRel` to a company-relative path, or null. */
|
|
223
264
|
function resolveLink(fromRel, href) {
|
|
224
265
|
if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("#") || href.startsWith("/"))
|
|
@@ -525,76 +566,13 @@ function referencesCopy(fromRel, content, copy) {
|
|
|
525
566
|
}
|
|
526
567
|
return false;
|
|
527
568
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
function
|
|
534
|
-
|
|
535
|
-
if (!front)
|
|
536
|
-
return content;
|
|
537
|
-
const index = front.lines.findIndex((line) => /^entity_refs:\s*$/.test(line));
|
|
538
|
-
if (index === -1) {
|
|
539
|
-
const inline = front.lines.some((line) => /^entity_refs:\s*\S/.test(line));
|
|
540
|
-
if (inline && content.includes(copyRef))
|
|
541
|
-
return "signal_ref_format";
|
|
542
|
-
return content;
|
|
543
|
-
}
|
|
544
|
-
let end = index + 1;
|
|
545
|
-
// A block sequence may sit at the key's own indent: `entity_refs:\n- company/x`.
|
|
546
|
-
while (end < front.lines.length && (/^\s/.test(front.lines[end]) || /^- /.test(front.lines[end])))
|
|
547
|
-
end += 1;
|
|
548
|
-
const itemRe = /^(\s*)- (["']?)([^"'\n]+?)\2\s*$/;
|
|
549
|
-
const refs = [];
|
|
550
|
-
for (let i = index + 1; i < end; i += 1) {
|
|
551
|
-
const match = itemRe.exec(front.lines[i]);
|
|
552
|
-
if (!match)
|
|
553
|
-
continue;
|
|
554
|
-
const hadMd = match[3].endsWith(".md");
|
|
555
|
-
refs.push({
|
|
556
|
-
index: i,
|
|
557
|
-
value: hadMd ? match[3].slice(0, -3) : match[3],
|
|
558
|
-
hadMd,
|
|
559
|
-
indent: match[1],
|
|
560
|
-
quote: match[2],
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
const copyItems = refs.filter((item) => item.value === copyRef);
|
|
564
|
-
if (copyItems.length === 0)
|
|
565
|
-
return content;
|
|
566
|
-
const lines = [...front.lines];
|
|
567
|
-
const remove = new Set();
|
|
568
|
-
let seenOriginal = refs.some((item) => item.value === originalRef);
|
|
569
|
-
for (const item of copyItems) {
|
|
570
|
-
if (seenOriginal) {
|
|
571
|
-
remove.add(item.index);
|
|
572
|
-
}
|
|
573
|
-
else {
|
|
574
|
-
const value = item.hadMd ? `${originalRef}.md` : originalRef;
|
|
575
|
-
lines[item.index] = `${item.indent}- ${item.quote}${value}${item.quote}`;
|
|
576
|
-
seenOriginal = true;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
const next = withFrontLines(content, front, lines.filter((_, i) => !remove.has(i)), fileNewline(content));
|
|
580
|
-
const oldParsed = parseEntityFile(content);
|
|
581
|
-
const newParsed = parseEntityFile(next);
|
|
582
|
-
if (!oldParsed || !newParsed || oldParsed.body !== newParsed.body)
|
|
583
|
-
return "signal_ref_format";
|
|
584
|
-
const keys = new Set([...Object.keys(oldParsed.frontmatter), ...Object.keys(newParsed.frontmatter)]);
|
|
585
|
-
for (const key of keys) {
|
|
586
|
-
if (key === "entity_refs")
|
|
587
|
-
continue;
|
|
588
|
-
if (!isDeepStrictEqual(oldParsed.frontmatter[key], newParsed.frontmatter[key]))
|
|
589
|
-
return "signal_ref_format";
|
|
590
|
-
}
|
|
591
|
-
const newRefs = newParsed.frontmatter.entity_refs;
|
|
592
|
-
if (!Array.isArray(newRefs) || !newRefs.every((ref) => typeof ref === "string"))
|
|
593
|
-
return "signal_ref_format";
|
|
594
|
-
const normalized = newRefs.map((ref) => (ref.endsWith(".md") ? ref.slice(0, -3) : ref));
|
|
595
|
-
if (!normalized.includes(originalRef) || normalized.includes(copyRef))
|
|
596
|
-
return "signal_ref_format";
|
|
597
|
-
return next;
|
|
569
|
+
/**
|
|
570
|
+
* Signals and sources are written by the server pipeline and the sync
|
|
571
|
+
* client never pushes them, so a rewrite made here would not reach the
|
|
572
|
+
* vault (hq-cloud cloud-authoritative.ts).
|
|
573
|
+
*/
|
|
574
|
+
function isServerOwnedRel(rel) {
|
|
575
|
+
return rel.startsWith("signals/") || rel.startsWith("sources/");
|
|
598
576
|
}
|
|
599
577
|
const REF_KEYS = new Set(["system", "namespace", "objectType", "recordId"]);
|
|
600
578
|
function refKey(ref) {
|
|
@@ -871,6 +849,7 @@ function plan(company, index, pairs, allPairs, now, rewriteRefs, resolutions = n
|
|
|
871
849
|
const unwritable = rewriteRefs
|
|
872
850
|
? inbound.filter((rel) => copies.has(rel))
|
|
873
851
|
: inbound.filter((rel) => !rel.startsWith("ontology/entities/") || !isWritableEntityType(rel.split("/")[2]) || copies.has(rel));
|
|
852
|
+
const serverOwned = rewriteRefs ? [...refs, ...inbound.filter(isServerOwnedRel)] : [];
|
|
874
853
|
if (typeof merged === "string") {
|
|
875
854
|
entry.held = "merge_invalid";
|
|
876
855
|
entry.detail = merged;
|
|
@@ -879,6 +858,10 @@ function plan(company, index, pairs, allPairs, now, rewriteRefs, resolutions = n
|
|
|
879
858
|
entry.held = "external_id_conflict";
|
|
880
859
|
entry.detail = identity.conflicts.join(", ");
|
|
881
860
|
}
|
|
861
|
+
else if (serverOwned.length > 0) {
|
|
862
|
+
entry.held = "server_owned_refs";
|
|
863
|
+
entry.detail = [...new Set(serverOwned)].sort().join(", ");
|
|
864
|
+
}
|
|
882
865
|
else if (!rewriteRefs && refs.length > 0) {
|
|
883
866
|
entry.held = "signal_refs";
|
|
884
867
|
}
|
|
@@ -960,7 +943,13 @@ export async function mergeCopies(options) {
|
|
|
960
943
|
const { hqRoot, company, mode, now } = options;
|
|
961
944
|
const index = buildEntityIndex(company.dir);
|
|
962
945
|
const pairKind = options.pairKind ?? "numbered";
|
|
963
|
-
|
|
946
|
+
if (pairKind === "listed" && !options.only)
|
|
947
|
+
throw new Error("merge-copies: listed pairs need an only list");
|
|
948
|
+
const found = pairKind === "listed"
|
|
949
|
+
? findListedPairs(index, options.only)
|
|
950
|
+
: pairKind === "variants"
|
|
951
|
+
? findVariantPairs(index)
|
|
952
|
+
: findCopyPairs(index);
|
|
964
953
|
const listed = options.only
|
|
965
954
|
? found.pairs.filter((pair) => options.only.some((entry) => entry.copy === pair.copy && entry.original === pair.original))
|
|
966
955
|
: found.pairs;
|
|
@@ -1060,28 +1049,6 @@ export async function mergeCopies(options) {
|
|
|
1060
1049
|
report.errors.push({ path: rel, error: error.name });
|
|
1061
1050
|
}
|
|
1062
1051
|
}
|
|
1063
|
-
if (!rewriteRefs)
|
|
1064
|
-
continue;
|
|
1065
|
-
const copyRef = entityRefOf(entry.copy);
|
|
1066
|
-
const originalRef = entityRefOf(entry.original);
|
|
1067
|
-
for (const rel of entry.signal_refs) {
|
|
1068
|
-
try {
|
|
1069
|
-
const content = read(company, rel);
|
|
1070
|
-
const next = rewriteSignalRefs(content, copyRef, originalRef);
|
|
1071
|
-
if (next === "signal_ref_format") {
|
|
1072
|
-
report.errors.push({ path: rel, error: "signal_ref_format" });
|
|
1073
|
-
continue;
|
|
1074
|
-
}
|
|
1075
|
-
if (next === content)
|
|
1076
|
-
continue;
|
|
1077
|
-
safeWriteFile(hqRoot, [company.dir], path.join(company.dir, rel), next);
|
|
1078
|
-
if (!report.rewritten.includes(rel))
|
|
1079
|
-
report.rewritten.push(rel);
|
|
1080
|
-
}
|
|
1081
|
-
catch (error) {
|
|
1082
|
-
report.errors.push({ path: rel, error: error.name });
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
1052
|
}
|
|
1086
1053
|
}
|
|
1087
1054
|
if (mode === "delete") {
|
|
@@ -677,13 +677,13 @@ function scrubbedSentryEnvelope(event, overrides, additionalTags = {}) {
|
|
|
677
677
|
...sentryEnvelope(event, overrides, additionalTags),
|
|
678
678
|
}, {});
|
|
679
679
|
}
|
|
680
|
-
/**
|
|
680
|
+
/** Group hook-timeout conditions by normalized kind while retaining triage tags. */
|
|
681
681
|
function hookTimeoutExceededOverrides(event) {
|
|
682
682
|
if (!isHookTimeoutEvent(event))
|
|
683
683
|
return undefined;
|
|
684
684
|
return {
|
|
685
685
|
level: "warning",
|
|
686
|
-
fingerprint: ["hook-timeout",
|
|
686
|
+
fingerprint: ["hook-timeout", hookTimeoutKind(event)],
|
|
687
687
|
};
|
|
688
688
|
}
|
|
689
689
|
/**
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { type FlagClientOptions, type FlagResolution } from "@indigoai-us/hq-flags-client";
|
|
7
7
|
/** Outposts hand running services to the next daemon on a CLI update instead of restarting them all. */
|
|
8
8
|
export declare const SUPERVISOR_HANDOFF_FLAG = "daemon.supervisor-handoff";
|
|
9
|
+
/** Self-updates build the new CLI beside the running one and swap it in, so `hq` never disappears mid-update. */
|
|
10
|
+
export declare const STAGED_SELF_UPDATE_FLAG = "cli.staged-self-update";
|
|
9
11
|
export interface DaemonFlagClient {
|
|
10
12
|
ready(): Promise<void>;
|
|
11
13
|
explain(flagKey: string, options?: {
|
package/dist/lib/daemon/flag.js
CHANGED
|
@@ -9,6 +9,8 @@ import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
|
|
|
9
9
|
import { resolveProcessFlagCompanyContext } from "../flag-registry.js";
|
|
10
10
|
/** Outposts hand running services to the next daemon on a CLI update instead of restarting them all. */
|
|
11
11
|
export const SUPERVISOR_HANDOFF_FLAG = "daemon.supervisor-handoff";
|
|
12
|
+
/** Self-updates build the new CLI beside the running one and swap it in, so `hq` never disappears mid-update. */
|
|
13
|
+
export const STAGED_SELF_UPDATE_FLAG = "cli.staged-self-update";
|
|
12
14
|
const READ_TIMEOUT_MS = 5_000;
|
|
13
15
|
export async function readDaemonFlag(flagKey, options = {}) {
|
|
14
16
|
const env = options.env ?? process.env;
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -62,5 +62,16 @@ export declare function rotateLogs(logsDir: string, maxBytes?: number): number;
|
|
|
62
62
|
* an on-failure unit down. The daemon's updater handles CLI updates.
|
|
63
63
|
*/
|
|
64
64
|
export declare function unitProcessEnv(base: NodeJS.ProcessEnv, unit: UnitPlan): NodeJS.ProcessEnv;
|
|
65
|
+
/**
|
|
66
|
+
* A systemd system unit starts the daemon as the login user without the
|
|
67
|
+
* variables a login session sets, so `systemctl --user` from its services
|
|
68
|
+
* cannot find the user bus ("Failed to connect to bus: No medium found").
|
|
69
|
+
* Point them at the user's runtime dir when it exists and nothing set one.
|
|
70
|
+
*/
|
|
71
|
+
export declare function withUserRuntimeDir(env: NodeJS.ProcessEnv, host: {
|
|
72
|
+
platform: NodeJS.Platform;
|
|
73
|
+
uid: number;
|
|
74
|
+
exists: (dir: string) => boolean;
|
|
75
|
+
}): NodeJS.ProcessEnv;
|
|
65
76
|
export declare function runDaemon(opts: RunDaemonOptions): Promise<number>;
|
|
66
77
|
//# sourceMappingURL=run.d.ts.map
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -233,8 +233,24 @@ function isFifo(file) {
|
|
|
233
233
|
return false;
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* A systemd system unit starts the daemon as the login user without the
|
|
238
|
+
* variables a login session sets, so `systemctl --user` from its services
|
|
239
|
+
* cannot find the user bus ("Failed to connect to bus: No medium found").
|
|
240
|
+
* Point them at the user's runtime dir when it exists and nothing set one.
|
|
241
|
+
*/
|
|
242
|
+
export function withUserRuntimeDir(env, host) {
|
|
243
|
+
if (host.platform !== "linux" || host.uid === 0 || env.XDG_RUNTIME_DIR)
|
|
244
|
+
return env;
|
|
245
|
+
const dir = `/run/user/${host.uid}`;
|
|
246
|
+
return host.exists(dir) ? { ...env, XDG_RUNTIME_DIR: dir } : env;
|
|
247
|
+
}
|
|
236
248
|
export async function runDaemon(opts) {
|
|
237
|
-
const env = opts.env ?? process.env
|
|
249
|
+
const env = withUserRuntimeDir(opts.env ?? process.env, {
|
|
250
|
+
platform: process.platform,
|
|
251
|
+
uid: process.getuid?.() ?? -1,
|
|
252
|
+
exists: fs.existsSync,
|
|
253
|
+
});
|
|
238
254
|
const home = opts.home ?? os.homedir();
|
|
239
255
|
const platform = process.platform;
|
|
240
256
|
const paths = daemonPaths(home, env);
|
|
@@ -12,8 +12,9 @@ export interface RegisteredLead {
|
|
|
12
12
|
status: string;
|
|
13
13
|
prd: string;
|
|
14
14
|
}
|
|
15
|
+
export type AutopilotCondition = "review_finished_worker" | "fill_grant" | "plan_free_slots" | "flat_kpi" | "blocker_record_needs_update" | "project_ready_to_close" | "stale_worker_ledger" | "story_without_live_lane";
|
|
15
16
|
export interface AutopilotWake {
|
|
16
|
-
condition:
|
|
17
|
+
condition: AutopilotCondition;
|
|
17
18
|
lead_lane: string;
|
|
18
19
|
story_id: string;
|
|
19
20
|
worker_lane?: string;
|
|
@@ -30,6 +31,14 @@ export interface AutopilotPassResult {
|
|
|
30
31
|
story_id: string;
|
|
31
32
|
worker_lane: string;
|
|
32
33
|
}>;
|
|
34
|
+
escalated: Array<{
|
|
35
|
+
team: string;
|
|
36
|
+
lead_lane: string;
|
|
37
|
+
condition: AutopilotCondition;
|
|
38
|
+
story_id: string;
|
|
39
|
+
at: string;
|
|
40
|
+
fingerprint: string;
|
|
41
|
+
}>;
|
|
33
42
|
skipped: Array<{
|
|
34
43
|
lead_lane: string;
|
|
35
44
|
reason: string;
|
|
@@ -38,15 +47,18 @@ export interface AutopilotPassResult {
|
|
|
38
47
|
interface ProjectStory {
|
|
39
48
|
id: string;
|
|
40
49
|
status: string;
|
|
50
|
+
passes?: boolean;
|
|
41
51
|
briefPath?: string;
|
|
42
52
|
laneIds?: string[];
|
|
43
53
|
updatedAt?: string;
|
|
54
|
+
externalBlocker?: string;
|
|
44
55
|
}
|
|
45
56
|
export declare function planLeadAutopilotWakes(input: {
|
|
46
57
|
lead: LaneRecord;
|
|
47
58
|
children: readonly LaneRecord[];
|
|
48
59
|
stories: readonly ProjectStory[];
|
|
49
60
|
now: Date;
|
|
61
|
+
managerStatus?: Record<string, unknown>;
|
|
50
62
|
keepAliveResumed?: boolean;
|
|
51
63
|
}): AutopilotWake[];
|
|
52
64
|
export declare function runLeadAutopilotPass(input: {
|