@indigoai-us/hq-cli 5.345.18 → 5.345.20
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 +10 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +12 -5
- package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +1 -1
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +12 -5
- package/assets/scaffold/core/scripts/hq-job-probe.sh +12 -5
- package/assets/scaffold/core/scripts/jobs-validate.sh +12 -5
- package/assets/scaffold/core/scripts/lib/transcript-tail.sh +33 -23
- package/assets/scaffold/core/scripts/ontology-garden.mjs +86 -3
- package/assets/scaffold/core/scripts/ontology-source-worklist.sh +141 -4
- package/assets/scaffold/core/scripts/register-project.sh +82 -14
- package/assets/scaffold/core/scripts/resume-thread-lock.sh +5 -5
- package/assets/scaffold/core/scripts/session-journal.sh +55 -6
- package/assets/scaffold/core/scripts/share-suggestion-state.sh +10 -2
- package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +36 -4
- package/assets/scaffold/core/scripts/worktree-gc.sh +1 -1
- package/dist/commands/core-ontology-native.js +110 -4
- package/dist/commands/core-register-project-native.js +36 -3
- package/dist/commands/core-session-journal-native.js +2 -2
- package/dist/commands/pack-install.d.ts +4 -0
- package/dist/commands/pack-install.js +12 -4
- package/dist/commands/packs.js +4 -1
- package/dist/commands/pkg-install.js +3 -2
- package/dist/lib/daemon/run.d.ts +2 -2
- package/dist/lib/daemon/run.js +5 -11
- package/dist/lib/doctor/checks/anywhere.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.20] — 2026-10-06
|
|
6
|
+
- Keep bundled scaffold scripts and `hq core register-project` aligned with the shared source, including company UID lookup from `companies/manifest.yaml`.
|
|
7
|
+
|
|
8
|
+
## [5.345.19] — 2026-10-06
|
|
9
|
+
- Fix installation and updates of the published hq-anywhere pack through its built-in alias while continuing to reject reserved names from user-supplied remote sources.
|
|
10
|
+
- With `hq-anywhere-runtime` enabled, hqd now starts on Windows through the user-scoped named-pipe host and refuses service only when secure pipe setup or ACL verification fails. `hq doctor` describes unsafe named-pipe startup as an ACL setup failure and recommends the daemon install or restart action based on service state.
|
|
11
|
+
|
|
5
12
|
## [5.345.18] — 2026-10-06
|
|
6
13
|
- Git Bash forwarders now keep slash commands intact and convert file path arguments that hq needs on Windows.
|
|
7
14
|
- Lane placement and Claude account-balance refusals now show the requested placement, actual host, balance host when different, and the model values and sources used for an unavailable-account decision.
|
|
8
15
|
|
|
16
|
+
## [5.345.17] — 2026-10-06
|
|
17
|
+
- Git Bash forwarders now keep slash commands intact and convert file path arguments that hq needs on Windows.
|
|
18
|
+
|
|
9
19
|
## [5.345.16] — 2026-10-06
|
|
10
20
|
- Claude account model settings are defaults, so explicit lane models remain eligible; explicit account selections on create, resume, and message are recorded as CLI choices.
|
|
11
21
|
- When `hq-anywhere-runtime` is enabled, `hq install --global --runtime claude --via plugin` installs the `hq` plugin from the `indigoai-us/hq-packages` Claude marketplace, and `hq install @indigoai-us/hq-anywhere` installs the Codex pack from that repository. The flag-off path refuses the HQ Anywhere alias without fetching a package source.
|
|
@@ -35,16 +35,23 @@ die() { echo "hq-delegate-grant: $*" >&2; exit 1; }
|
|
|
35
35
|
|
|
36
36
|
command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
|
|
37
37
|
|
|
38
|
-
# jq.exe
|
|
38
|
+
# jq.exe on MSYS may use CRT text mode unless --binary is supported.
|
|
39
39
|
case "${OSTYPE:-}:${MSYSTEM:-}" in
|
|
40
40
|
msys*:*|*:MINGW*)
|
|
41
41
|
jq() {
|
|
42
|
-
local stdout_file stderr_file status
|
|
42
|
+
local stdout_file stderr_file status binary_supported
|
|
43
43
|
stdout_file="$(mktemp)" || return $?
|
|
44
44
|
stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
|
|
45
|
-
if command jq --binary
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
if command jq --binary -n 'null' >/dev/null 2>&1; then binary_supported=1; else binary_supported=0; fi
|
|
46
|
+
if [[ "$binary_supported" == 1 ]]; then
|
|
47
|
+
if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
48
|
+
command cat "$stdout_file"
|
|
49
|
+
command cat "$stderr_file" >&2
|
|
50
|
+
else
|
|
51
|
+
if command jq "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
52
|
+
command sed -b 's/\r$//' "$stdout_file"
|
|
53
|
+
command sed -b 's/\r$//' "$stderr_file" >&2
|
|
54
|
+
fi
|
|
48
55
|
rm -f "$stdout_file" "$stderr_file"
|
|
49
56
|
return "$status"
|
|
50
57
|
}
|
|
@@ -90,7 +90,7 @@ fi
|
|
|
90
90
|
|
|
91
91
|
# --- parse names (KEY= lines; comments and blanks ignored) --------------------
|
|
92
92
|
|
|
93
|
-
NAMES="$(grep -E '^[A-
|
|
93
|
+
NAMES="$(grep -E '^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*=' "$ENV_SCHEMA" | cut -d= -f1 | sort -u)"
|
|
94
94
|
if [ -z "$NAMES" ]; then
|
|
95
95
|
update_manifest '.secrets = [] | .secretsSkipped = false'
|
|
96
96
|
echo "hq-delegate-secrets: $ENV_SCHEMA declares no secret names — nothing to grant"
|
|
@@ -40,16 +40,23 @@ die() { echo "hq-delegate-send: $*" >&2; exit 1; }
|
|
|
40
40
|
|
|
41
41
|
command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
|
|
42
42
|
|
|
43
|
-
# jq.exe
|
|
43
|
+
# jq.exe on MSYS may use CRT text mode unless --binary is supported.
|
|
44
44
|
case "${OSTYPE:-}:${MSYSTEM:-}" in
|
|
45
45
|
msys*:*|*:MINGW*)
|
|
46
46
|
jq() {
|
|
47
|
-
local stdout_file stderr_file status
|
|
47
|
+
local stdout_file stderr_file status binary_supported
|
|
48
48
|
stdout_file="$(mktemp)" || return $?
|
|
49
49
|
stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
|
|
50
|
-
if command jq --binary
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
if command jq --binary -n 'null' >/dev/null 2>&1; then binary_supported=1; else binary_supported=0; fi
|
|
51
|
+
if [[ "$binary_supported" == 1 ]]; then
|
|
52
|
+
if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
53
|
+
command cat "$stdout_file"
|
|
54
|
+
command cat "$stderr_file" >&2
|
|
55
|
+
else
|
|
56
|
+
if command jq "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
57
|
+
command sed -b 's/\r$//' "$stdout_file"
|
|
58
|
+
command sed -b 's/\r$//' "$stderr_file" >&2
|
|
59
|
+
fi
|
|
53
60
|
rm -f "$stdout_file" "$stderr_file"
|
|
54
61
|
return "$status"
|
|
55
62
|
}
|
|
@@ -120,16 +120,23 @@ done
|
|
|
120
120
|
command -v yq >/dev/null 2>&1 || die "yq is required (mikefarah/yq)"
|
|
121
121
|
command -v jq >/dev/null 2>&1 || die "jq is required"
|
|
122
122
|
|
|
123
|
-
# jq.exe
|
|
123
|
+
# jq.exe on MSYS may use CRT text mode unless --binary is supported.
|
|
124
124
|
case "${OSTYPE:-}:${MSYSTEM:-}" in
|
|
125
125
|
msys*:*|*:MINGW*)
|
|
126
126
|
jq() {
|
|
127
|
-
local stdout_file stderr_file status
|
|
127
|
+
local stdout_file stderr_file status binary_supported
|
|
128
128
|
stdout_file="$(mktemp)" || return $?
|
|
129
129
|
stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
|
|
130
|
-
if command jq --binary
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
if command jq --binary -n 'null' >/dev/null 2>&1; then binary_supported=1; else binary_supported=0; fi
|
|
131
|
+
if [[ "$binary_supported" == 1 ]]; then
|
|
132
|
+
if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
133
|
+
command cat "$stdout_file"
|
|
134
|
+
command cat "$stderr_file" >&2
|
|
135
|
+
else
|
|
136
|
+
if command jq "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
137
|
+
command sed -b 's/\r$//' "$stdout_file"
|
|
138
|
+
command sed -b 's/\r$//' "$stderr_file" >&2
|
|
139
|
+
fi
|
|
133
140
|
rm -f "$stdout_file" "$stderr_file"
|
|
134
141
|
return "$status"
|
|
135
142
|
}
|
|
@@ -40,16 +40,23 @@ command -v jq >/dev/null 2>&1 || {
|
|
|
40
40
|
exit 2
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
# jq.exe
|
|
43
|
+
# jq.exe on MSYS may use CRT text mode unless --binary is supported.
|
|
44
44
|
case "${OSTYPE:-}:${MSYSTEM:-}" in
|
|
45
45
|
msys*:*|*:MINGW*)
|
|
46
46
|
jq() {
|
|
47
|
-
local stdout_file stderr_file status
|
|
47
|
+
local stdout_file stderr_file status binary_supported
|
|
48
48
|
stdout_file="$(mktemp)" || return $?
|
|
49
49
|
stderr_file="$(mktemp)" || { status=$?; rm -f "$stdout_file"; return "$status"; }
|
|
50
|
-
if command jq --binary
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
if command jq --binary -n 'null' >/dev/null 2>&1; then binary_supported=1; else binary_supported=0; fi
|
|
51
|
+
if [[ "$binary_supported" == 1 ]]; then
|
|
52
|
+
if command jq --binary "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
53
|
+
command cat "$stdout_file"
|
|
54
|
+
command cat "$stderr_file" >&2
|
|
55
|
+
else
|
|
56
|
+
if command jq "$@" >"$stdout_file" 2>"$stderr_file"; then status=0; else status=$?; fi
|
|
57
|
+
command sed -b 's/\r$//' "$stdout_file"
|
|
58
|
+
command sed -b 's/\r$//' "$stderr_file" >&2
|
|
59
|
+
fi
|
|
53
60
|
rm -f "$stdout_file" "$stderr_file"
|
|
54
61
|
return "$status"
|
|
55
62
|
}
|
|
@@ -54,45 +54,55 @@ hq_transcript_tail() {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
# hq_transcript_tail_with_latest_assistant <path> [max_bytes] [scan_cap_bytes]
|
|
57
|
-
# Return the normal bounded suffix plus the
|
|
58
|
-
# when later
|
|
59
|
-
#
|
|
60
|
-
#
|
|
57
|
+
# Return the normal bounded suffix plus the newest complete assistant record
|
|
58
|
+
# when later records pushed it outside that suffix. The third argument remains
|
|
59
|
+
# accepted for caller compatibility, but reverse scanning is no longer capped.
|
|
60
|
+
# Return 1 only when the transcript cannot be read or no reverse-line tool is
|
|
61
|
+
# available; callers handling user-facing policy checks must fail open silently.
|
|
61
62
|
hq_transcript_tail_with_latest_assistant() {
|
|
62
63
|
[ "$#" -ge 1 ] && [ "$#" -le 3 ] || return 2
|
|
63
64
|
local path="$1" max_bytes="${2:-$HQ_TRANSCRIPT_TAIL_MAX_BYTES}"
|
|
64
|
-
local scan_cap="${3:-16777216}"
|
|
65
|
+
local scan_cap="${3:-16777216}" normal_tail assistant_record reverse_mode line=""
|
|
65
66
|
case "$scan_cap" in ''|*[!0-9]*) return 2 ;; esac
|
|
66
67
|
[ "$scan_cap" -gt 0 ] || return 2
|
|
67
68
|
|
|
69
|
+
# Most turns keep their newest assistant record in the ordinary bounded
|
|
70
|
+
# suffix, so preserve that fast path before any reverse scan.
|
|
68
71
|
normal_tail="$(hq_transcript_tail "$path" "$max_bytes")" || return $?
|
|
69
|
-
assistant_record="$(printf '%s\n' "$normal_tail" | jq -Rrc 'fromjson? | select(.type == "assistant")' | tail -n 1)"
|
|
72
|
+
assistant_record="$(printf '%s\n' "$normal_tail" | jq -Rrc 'fromjson? | select(.type == "assistant" or (.type == "response_item" and .payload.type == "message" and .payload.role == "assistant"))' | tail -n 1)"
|
|
70
73
|
if [ -n "$assistant_record" ]; then
|
|
71
74
|
[ -z "$normal_tail" ] || printf '%s\n' "$normal_tail"
|
|
72
75
|
return 0
|
|
73
76
|
fi
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
# GNU tac and BSD/macOS tail -r both stream records from EOF toward the
|
|
79
|
+
# beginning. grep discards ordinary tool-output lines before Bash reads them;
|
|
80
|
+
# jq confirms candidates and the loop exits at the newest assistant.
|
|
81
|
+
if command -v tac >/dev/null 2>&1; then
|
|
82
|
+
reverse_mode=tac
|
|
83
|
+
elif tail -r /dev/null >/dev/null 2>&1; then
|
|
84
|
+
reverse_mode=tail-r
|
|
81
85
|
else
|
|
82
|
-
|
|
86
|
+
printf '%s\n' "hq_transcript_tail_with_latest_assistant: no reverse-line reader is available" >&2
|
|
87
|
+
return 1
|
|
83
88
|
fi
|
|
84
|
-
|
|
85
|
-
assistant_record="$(
|
|
89
|
+
|
|
90
|
+
assistant_record="$(
|
|
91
|
+
if [ "$reverse_mode" = tac ]; then
|
|
92
|
+
{ tac "$path" 2>/dev/null || true; }
|
|
93
|
+
else
|
|
94
|
+
{ tail -r "$path" 2>/dev/null || true; }
|
|
95
|
+
fi | { grep -E '"assistant"' || true; } | while IFS= read -r line || [ -n "$line" ]; do
|
|
96
|
+
# grep filters large non-assistant records before Bash reads their lines.
|
|
97
|
+
if printf '%s\n' "$line" | jq -Rre 'fromjson? | select(.type == "assistant" or (.type == "response_item" and .payload.type == "message" and .payload.role == "assistant"))' >/dev/null 2>&1; then
|
|
98
|
+
printf '%s\n' "$line"
|
|
99
|
+
break
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
)"
|
|
86
103
|
if [ -n "$assistant_record" ]; then
|
|
87
104
|
printf '%s\n' "$assistant_record"
|
|
88
|
-
[ -z "$normal_tail" ] || printf '%s\n' "$normal_tail"
|
|
89
|
-
return 0
|
|
90
105
|
fi
|
|
91
|
-
|
|
92
|
-
if [ "$size" -gt "$scan_cap" ]; then
|
|
93
|
-
printf '%s\n' "hq_transcript_tail_with_latest_assistant: latest assistant record exceeds the ${scan_cap}-byte verification window" >&2
|
|
94
|
-
return 3
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
106
|
[ -z "$normal_tail" ] || printf '%s\n' "$normal_tail"
|
|
107
|
+
return 0
|
|
98
108
|
}
|
|
@@ -80,6 +80,69 @@ function writeDoc(file, fm, body, dry) {
|
|
|
80
80
|
const slugify = s => s.toLowerCase().normalize("NFKD").replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
81
81
|
const today = () => new Date().toISOString().slice(0, 10);
|
|
82
82
|
const now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
83
|
+
const yyyyMm = () => new Date().toISOString().slice(0, 7);
|
|
84
|
+
|
|
85
|
+
// ── Fact-file compaction ─────────────────────────────────────────────
|
|
86
|
+
// Fact pages append one line per signal-mention of an entity (see step 3
|
|
87
|
+
// below). Without a bound a hot entity accumulates every mention it has
|
|
88
|
+
// ever received, so the page becomes a flat log. Compaction keeps the
|
|
89
|
+
// most recent N lines inline and rolls the older ones into a dated,
|
|
90
|
+
// append-only archive next to the fact file. `signal_count` on the
|
|
91
|
+
// entity itself is unaffected — it is the true total.
|
|
92
|
+
const BACKREF_COMPACT_THRESHOLD = Number(process.env.HQ_ONTOLOGY_BACKREF_THRESHOLD || 50);
|
|
93
|
+
const BACKREF_KEEP_INLINE = Number(process.env.HQ_ONTOLOGY_BACKREF_KEEP_INLINE || 25);
|
|
94
|
+
const SUMMARY_MARKER = "<!-- backref-archive -->";
|
|
95
|
+
|
|
96
|
+
function isBackrefLine(line) { return /^- \[[a-z_]+\] .+ \(signal [0-9a-f]{12}\)\s*$/.test(line); }
|
|
97
|
+
function isSummaryLine(line) { return line.includes(SUMMARY_MARKER); }
|
|
98
|
+
|
|
99
|
+
function readArchiveIndex(file) {
|
|
100
|
+
if (!fs.existsSync(file)) return { version: 1, signalIds: [] };
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
103
|
+
if (parsed && Array.isArray(parsed.signalIds)) {
|
|
104
|
+
return { version: 1, signalIds: parsed.signalIds.filter(s => typeof s === "string") };
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.warn(`ontology-garden: archive index unreadable at ${file}: ${err.message || err}; skipping compaction`);
|
|
108
|
+
}
|
|
109
|
+
// Present but unreadable: never compact on top of it, or the rewrite would
|
|
110
|
+
// drop every archived id it lists.
|
|
111
|
+
return { version: 1, signalIds: [], unreadable: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Compact `body` when its backref-line count exceeds the threshold.
|
|
115
|
+
// Returns { body, archivedLines, archivedIds, changed }.
|
|
116
|
+
function compactFactBody(body, { slug, archivedTotalBefore, dry }) {
|
|
117
|
+
const lines = body.split("\n").map(l => l.trimEnd()).filter(l => l.length > 0);
|
|
118
|
+
const backrefLines = lines.filter(isBackrefLine);
|
|
119
|
+
const other = lines.filter(l => !isBackrefLine(l) && !isSummaryLine(l));
|
|
120
|
+
if (backrefLines.length <= BACKREF_COMPACT_THRESHOLD) {
|
|
121
|
+
return { body, archivedLines: [], archivedIds: [], changed: false };
|
|
122
|
+
}
|
|
123
|
+
const archiveCount = backrefLines.length - BACKREF_KEEP_INLINE;
|
|
124
|
+
const archivedLines = backrefLines.slice(0, archiveCount);
|
|
125
|
+
const inlineLines = backrefLines.slice(archiveCount);
|
|
126
|
+
const archivedIds = archivedLines
|
|
127
|
+
.map(l => (l.match(/\(signal ([0-9a-f]{12})\)$/) || [])[1])
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
const archivedTotal = archivedTotalBefore + archivedLines.length;
|
|
130
|
+
const stamp = yyyyMm();
|
|
131
|
+
const summary = `- ${SUMMARY_MARKER} _${archivedTotal} older mention${archivedTotal === 1 ? "" : "s"} archived in [history](./${slug}.history/${stamp}.md)_`;
|
|
132
|
+
const nextBody = [summary, ...other, ...inlineLines].join("\n");
|
|
133
|
+
return { body: nextBody, archivedLines, archivedIds, changed: true, stamp };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function appendArchiveFile(file, isoDate, lines, dry) {
|
|
137
|
+
if (dry) return;
|
|
138
|
+
const batch = `\n<!-- archived ${isoDate} -->\n${lines.join("\n")}\n`;
|
|
139
|
+
const prev = fs.existsSync(file) ? fs.readFileSync(file, "utf8").replace(/\s+$/, "") : "";
|
|
140
|
+
const next = prev ? `${prev}\n${batch}` : `# Back-reference archive\n${batch}`;
|
|
141
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
142
|
+
const tmp = `${file}.tmp`;
|
|
143
|
+
fs.writeFileSync(tmp, next);
|
|
144
|
+
fs.renameSync(tmp, file);
|
|
145
|
+
}
|
|
83
146
|
|
|
84
147
|
function listCandidates(dir) {
|
|
85
148
|
if (!fs.existsSync(dir)) return [];
|
|
@@ -193,10 +256,30 @@ function main() {
|
|
|
193
256
|
for (const slug of hit) {
|
|
194
257
|
const e = entities.get(slug);
|
|
195
258
|
const ffile = path.join(ont, "facts", `@${key}`, e.type, `${slug}.md`);
|
|
196
|
-
const
|
|
259
|
+
const sid12 = id.slice(0, 12);
|
|
260
|
+
const line = `- [${type}] ${body.split("\n")[0]} (signal ${sid12})`;
|
|
197
261
|
const prev = fs.existsSync(ffile) ? readDoc(ffile) : { fm: { entity: slug, type: e.type, audience_key: key }, body: "" };
|
|
198
|
-
|
|
199
|
-
|
|
262
|
+
const idxFile = path.join(path.dirname(ffile), `${slug}.history`, "_index.json");
|
|
263
|
+
const archiveIndex = readArchiveIndex(idxFile);
|
|
264
|
+
const alreadyArchived = archiveIndex.signalIds.includes(sid12);
|
|
265
|
+
if (!prev.body.includes(`signal ${sid12}`) && !alreadyArchived) {
|
|
266
|
+
let nextBody = [prev.body, line].filter(Boolean).join("\n");
|
|
267
|
+
const compact = compactFactBody(nextBody, {
|
|
268
|
+
slug,
|
|
269
|
+
archivedTotalBefore: archiveIndex.signalIds.length,
|
|
270
|
+
dry: a.dryRun,
|
|
271
|
+
});
|
|
272
|
+
if (compact.changed && !archiveIndex.unreadable) {
|
|
273
|
+
const archiveFile = path.join(path.dirname(ffile), `${slug}.history`, `${compact.stamp}.md`);
|
|
274
|
+
appendArchiveFile(archiveFile, today(), compact.archivedLines, a.dryRun);
|
|
275
|
+
if (!a.dryRun) {
|
|
276
|
+
const nextIdx = { version: 1, signalIds: [...archiveIndex.signalIds, ...compact.archivedIds] };
|
|
277
|
+
fs.mkdirSync(path.dirname(idxFile), { recursive: true });
|
|
278
|
+
fs.writeFileSync(idxFile, JSON.stringify(nextIdx) + "\n");
|
|
279
|
+
}
|
|
280
|
+
nextBody = compact.body;
|
|
281
|
+
}
|
|
282
|
+
writeDoc(ffile, prev.fm, nextBody, a.dryRun);
|
|
200
283
|
report.facts_written++;
|
|
201
284
|
}
|
|
202
285
|
if (key === "company") {
|
|
@@ -32,10 +32,77 @@ while [ $# -gt 0 ]; do
|
|
|
32
32
|
esac
|
|
33
33
|
done
|
|
34
34
|
[ -n "$co" ] && [ -n "$ch" ] || { echo "usage: ontology-source-worklist.sh --company <co> --channel <ch> [--limit N] [--force] [--mark-done <file>]" >&2; exit 2; }
|
|
35
|
-
D="$root/companies/$co/sources/$ch"; Y="$D/source.yaml"; ledger="$D/.processed"
|
|
35
|
+
D="$root/companies/$co/sources/$ch"; Y="$D/source.yaml"; ledger="$D/.processed"; skip_ledger="$D/.skipped"
|
|
36
|
+
# Bumped when the audience parser gains new recognisable frontmatter fields; items
|
|
37
|
+
# previously skipped as no-audience under an older PARSER_VERSION get re-evaluated
|
|
38
|
+
# on the next run instead of staying stuck forever (see feedback_b8d61e28).
|
|
39
|
+
PARSER_VERSION=2
|
|
36
40
|
bash "$here/source-yaml-validate.sh" "$Y" >/dev/null || { bash "$here/source-yaml-validate.sh" "$Y" >&2; exit 2; }
|
|
37
41
|
|
|
38
|
-
if [ -n "$done_item" ]; then
|
|
42
|
+
if [ -n "$done_item" ]; then printf '%s\t%s\n' "$(basename "$done_item")" "$PARSER_VERSION" >> "$ledger"; exit 0; fi
|
|
43
|
+
|
|
44
|
+
# Migrate `.processed` to parser-version stamps so a successfully processed item
|
|
45
|
+
# stays processed across runs. Entries are `basename\tversion`; new mark-done
|
|
46
|
+
# writes always include the stamp. Convergence rule for legacy UNSTAMPED
|
|
47
|
+
# entries from the pre-fix slack-dm bug (254 DMs recorded in `.processed` as
|
|
48
|
+
# plain basenames while no candidates were ever written): drop ones whose file
|
|
49
|
+
# carries `resolved_participants:` (the exact bug signature) so the main loop
|
|
50
|
+
# re-evaluates them exactly once; the next mark-done writes them back stamped
|
|
51
|
+
# at the current version. Any other unstamped entry is upgraded in place to a
|
|
52
|
+
# stamp — never re-stripped again. Stamped entries at a version below current
|
|
53
|
+
# PARSER_VERSION are also dropped so a parser upgrade re-evaluates them.
|
|
54
|
+
# ontology-candidate ids are content-addressed, so a true reprocess is idempotent.
|
|
55
|
+
if [ -f "$ledger" ]; then
|
|
56
|
+
tmp="$ledger.heal.$$"
|
|
57
|
+
: > "$tmp"
|
|
58
|
+
changed=0
|
|
59
|
+
while IFS=$'\t' read -r b ver || [ -n "$b" ]; do
|
|
60
|
+
b="${b%$'\r'}"; ver="${ver%$'\r'}"
|
|
61
|
+
[ -n "$b" ] || continue
|
|
62
|
+
if [ -n "${ver:-}" ]; then
|
|
63
|
+
if [ "$ver" -lt "$PARSER_VERSION" ] 2>/dev/null; then changed=1; continue; fi
|
|
64
|
+
printf '%s\t%s\n' "$b" "$ver" >> "$tmp"
|
|
65
|
+
continue
|
|
66
|
+
fi
|
|
67
|
+
item="$D/$b"
|
|
68
|
+
if [ -f "$item" ] && awk '/^---$/{fm++; if(fm==2) exit} fm==1 && /^resolved_participants:/{found=1; exit} END{exit !found}' "$item" 2>/dev/null; then
|
|
69
|
+
changed=1; continue
|
|
70
|
+
fi
|
|
71
|
+
printf '%s\t%s\n' "$b" "$PARSER_VERSION" >> "$tmp"
|
|
72
|
+
changed=1
|
|
73
|
+
done < "$ledger"
|
|
74
|
+
if [ "$changed" = 1 ]; then mv "$tmp" "$ledger"; else rm -f "$tmp"; fi
|
|
75
|
+
fi
|
|
76
|
+
is_processed() { # 0 if $1 is recorded in .processed at >= current parser version (or unstamped — treated as current after migration)
|
|
77
|
+
[ -f "$ledger" ] || return 1
|
|
78
|
+
awk -F '\t' -v b="$1" -v v="$PARSER_VERSION" '
|
|
79
|
+
{ sub(/\r$/, "") }
|
|
80
|
+
$1==b { if (NF < 2 || $2=="") { found=1; exit } if (($2+0) >= v) { found=1; exit } }
|
|
81
|
+
END { exit !found }
|
|
82
|
+
' "$ledger"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Walk `.skipped`: entries whose recorded version is < current PARSER_VERSION
|
|
86
|
+
# are dropped so the main loop re-evaluates them. Entries at the current
|
|
87
|
+
# version stay as the ignore set for this run.
|
|
88
|
+
declare_skipped=""
|
|
89
|
+
if [ -f "$skip_ledger" ]; then
|
|
90
|
+
tmp="$skip_ledger.rev.$$"
|
|
91
|
+
: > "$tmp"
|
|
92
|
+
changed=0
|
|
93
|
+
while IFS=$'\t' read -r b reason ver || [ -n "$b" ]; do
|
|
94
|
+
[ -n "$b" ] || continue
|
|
95
|
+
if [ -z "${ver:-}" ] || [ "$ver" -lt "$PARSER_VERSION" ] 2>/dev/null; then
|
|
96
|
+
changed=1; continue
|
|
97
|
+
fi
|
|
98
|
+
printf '%s\t%s\t%s\n' "$b" "$reason" "$ver" >> "$tmp"
|
|
99
|
+
done < "$skip_ledger"
|
|
100
|
+
if [ "$changed" = 1 ]; then mv "$tmp" "$skip_ledger"; else rm -f "$tmp"; fi
|
|
101
|
+
fi
|
|
102
|
+
skipped_at_current() { # 0 if $1 is recorded as skipped at the current parser version
|
|
103
|
+
[ -f "$skip_ledger" ] || return 1
|
|
104
|
+
awk -F '\t' -v b="$1" -v v="$PARSER_VERSION" '$1==b && $3==v { found=1; exit } END { exit !found }' "$skip_ledger"
|
|
105
|
+
}
|
|
39
106
|
|
|
40
107
|
get() { awk -v k="$1" '$0 ~ "^"k":" { sub("^"k":[[:space:]]*", ""); gsub(/^["\x27]|["\x27]$/, ""); print; exit }' "$Y"; }
|
|
41
108
|
run="$(get run)"; rule="$(get audience_rule)"
|
|
@@ -53,11 +120,69 @@ item_list() { # frontmatter list field: inline [a, b] or block "- a"
|
|
|
53
120
|
' "$1"
|
|
54
121
|
}
|
|
55
122
|
|
|
123
|
+
# participant_ids: for a frontmatter block list of mappings under key $2 (e.g.
|
|
124
|
+
# resolved_participants:), emit one id per entry: the entry's `person_uid:` if
|
|
125
|
+
# present, otherwise its `email:`. Entries with only `slack_user_id:` (no
|
|
126
|
+
# person_uid, no email) are skipped — the audience must stay exactly the
|
|
127
|
+
# resolvable DM participants, never widened. Scope: ONLY the mapping list
|
|
128
|
+
# directly under $2 in frontmatter.
|
|
129
|
+
participant_ids() {
|
|
130
|
+
awk -v k="$2" '
|
|
131
|
+
{ sub(/\r$/, "") }
|
|
132
|
+
BEGIN{fm=0; inlist=0; have_pid=0; have_email=0; pid=""; em=""}
|
|
133
|
+
function flush() {
|
|
134
|
+
if (have_pid && pid != "") { print pid }
|
|
135
|
+
else if (have_email && em != "") { print em }
|
|
136
|
+
have_pid=0; have_email=0; pid=""; em=""
|
|
137
|
+
}
|
|
138
|
+
/^---$/ { fm++; if (fm==2) { if (inlist) flush(); exit } next }
|
|
139
|
+
fm==1 && $0 ~ "^"k":" { inlist=1; next }
|
|
140
|
+
fm==1 && inlist {
|
|
141
|
+
# end of list = a line that is not indented beyond col 0 and not a bare "-"
|
|
142
|
+
if ($0 ~ /^[^ \t-]/) { flush(); inlist=0; next }
|
|
143
|
+
is_dash = ($0 ~ /^[[:space:]]*-[[:space:]]/)
|
|
144
|
+
if (is_dash) {
|
|
145
|
+
flush()
|
|
146
|
+
s=$0; sub(/^[[:space:]]*-[[:space:]]*/, "", s)
|
|
147
|
+
# inline mapping (JSON-style) on same line as the dash
|
|
148
|
+
if (s ~ /^\{/) {
|
|
149
|
+
gsub(/[{}"\x27]/, "", s); n=split(s, kv, ",")
|
|
150
|
+
for (i=1;i<=n;i++) {
|
|
151
|
+
gsub(/^[ \t]+|[ \t]+$/, "", kv[i])
|
|
152
|
+
if (match(kv[i], /^person_uid[[:space:]]*:[[:space:]]*/)) { pid=substr(kv[i], RLENGTH+1); have_pid=1 }
|
|
153
|
+
else if (match(kv[i], /^email[[:space:]]*:[[:space:]]*/)) { em=substr(kv[i], RLENGTH+1); have_email=1 }
|
|
154
|
+
}
|
|
155
|
+
flush()
|
|
156
|
+
next
|
|
157
|
+
}
|
|
158
|
+
# First mapping key on same line as the dash, e.g.
|
|
159
|
+
# - person_uid: prs_01ALICE
|
|
160
|
+
# Fall through to the key/value handlers below with the de-dashed line.
|
|
161
|
+
line = s
|
|
162
|
+
} else {
|
|
163
|
+
line = $0; sub(/^[[:space:]]+/, "", line)
|
|
164
|
+
}
|
|
165
|
+
if (line ~ /^person_uid:/) {
|
|
166
|
+
v=line; sub(/^person_uid:[[:space:]]*/, "", v); gsub(/["\x27]/, "", v); gsub(/^[ \t]+|[ \t]+$/, "", v)
|
|
167
|
+
if (v != "") { pid=v; have_pid=1 }
|
|
168
|
+
next
|
|
169
|
+
}
|
|
170
|
+
if (line ~ /^email:/) {
|
|
171
|
+
v=line; sub(/^email:[[:space:]]*/, "", v); gsub(/["\x27]/, "", v); gsub(/^[ \t]+|[ \t]+$/, "", v)
|
|
172
|
+
if (v != "") { em=v; have_email=1 }
|
|
173
|
+
next
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
END { if (inlist) flush() }
|
|
177
|
+
' "$1"
|
|
178
|
+
}
|
|
179
|
+
|
|
56
180
|
n=0
|
|
57
181
|
for f in "$D"/*.md; do
|
|
58
182
|
[ -f "$f" ] || continue
|
|
59
183
|
b="$(basename "$f")"
|
|
60
|
-
|
|
184
|
+
is_processed "$b" && continue
|
|
185
|
+
skipped_at_current "$b" && continue
|
|
61
186
|
[ "$n" -ge "$limit" ] && break
|
|
62
187
|
n=$((n+1))
|
|
63
188
|
rel="sources/$ch/$b"
|
|
@@ -68,12 +193,24 @@ for f in "$D"/*.md; do
|
|
|
68
193
|
*)
|
|
69
194
|
aud="$(item_list "$f" audience | paste -sd, -)"
|
|
70
195
|
[ -n "$aud" ] || aud="$(item_list "$f" attendees | grep -E '@|^(prs|agt)_' | paste -sd, -)"
|
|
196
|
+
# Slack DM items (hq-pro inbound slack-personal-dm.ts) only carry
|
|
197
|
+
# resolved_participants: a block list of mappings with person_uid /
|
|
198
|
+
# slack_user_id / email. Derive audience from those, scoped exactly to
|
|
199
|
+
# the DM's own participants (never widened to company). Only entries
|
|
200
|
+
# whose person_uid or email resolves become audience ids; a slack_user_id
|
|
201
|
+
# alone is not a usable audience key, so that entry is dropped — matching
|
|
202
|
+
# the frontmatter contract exercised in hq-pro
|
|
203
|
+
# test/sources/inbound/slack-personal-dm.test.ts.
|
|
204
|
+
[ -n "$aud" ] || aud="$(participant_ids "$f" resolved_participants | grep -E '@|^(prs|agt)_' | paste -sd, -)"
|
|
71
205
|
if [ -z "$aud" ]; then
|
|
72
206
|
aud="$("$HQ" files --company "$co" acl "$rel" --json 2>/dev/null \
|
|
73
207
|
| jq -r '[.direct[]? | select(.granteeType=="person" or .granteeType=="email" or .granteeType=="agent") | select(.permission=="read" or .permission=="write" or .permission=="admin") | .granteeId] | unique | join(",")' 2>/dev/null)"
|
|
74
208
|
fi ;;
|
|
75
209
|
esac
|
|
76
|
-
if [ -z "$aud" ]; then
|
|
210
|
+
if [ -z "$aud" ]; then
|
|
211
|
+
printf '%s\t%s\t%s\n' "$b" "no-audience" "$PARSER_VERSION" >> "$skip_ledger"
|
|
212
|
+
jq -cn --arg f "$rel" '{file:$f, skip:"no-audience"}'; continue
|
|
213
|
+
fi
|
|
77
214
|
key="$(HQ_ROOT="$root" bash "$here/ontology-candidate.sh" key "$aud")"
|
|
78
215
|
jq -cn --arg f "$rel" --arg k "$key" --arg a "$aud" '{file:$f, audience_key:$k, audience:($a|split(","))}'
|
|
79
216
|
done
|