@indigoai-us/hq-cli 5.345.19 → 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 +3 -0
- package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +1 -1
- 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/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/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
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
|
+
|
|
5
8
|
## [5.345.19] — 2026-10-06
|
|
6
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.
|
|
7
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.
|
|
@@ -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"
|
|
@@ -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
|
|
@@ -21,13 +21,44 @@ resolve_uid() {
|
|
|
21
21
|
tr -d '[:space:]' < "$file"
|
|
22
22
|
return
|
|
23
23
|
fi
|
|
24
|
-
|
|
24
|
+
if [ -f "$ROOT/companies/manifest.yaml" ]; then
|
|
25
|
+
local manifest_uid
|
|
26
|
+
manifest_uid="$(awk -v company="$company" '
|
|
27
|
+
/^companies:[[:space:]]*$/ { in_companies=1; next }
|
|
28
|
+
in_companies && /^[^[:space:]]/ { in_companies=0 }
|
|
29
|
+
in_companies && /^ [^ #][^:]*:/ {
|
|
30
|
+
slug=$1; sub(/[[:space:]]+$/, "", slug); sub(/:$/, "", slug); in_company=(slug == company); next
|
|
31
|
+
}
|
|
32
|
+
in_company && /^ [^[:space:]#]/ { exit }
|
|
33
|
+
in_company && $1 == "cloud_uid:" { gsub(/["\047]/, "", $2); sub(/[[:space:]]+$/, "", $2); print $2; exit }
|
|
34
|
+
' "$ROOT/companies/manifest.yaml")"
|
|
35
|
+
if [ -n "$manifest_uid" ]; then
|
|
36
|
+
printf '%s\n' "$manifest_uid"
|
|
37
|
+
return
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
die "no company uid for $company (set HQ_COMPANY_UID, companies/$company/.company-uid, or manifest cloud_uid)"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
mesh_project_id_for_prd() {
|
|
44
|
+
local company="$1" project="$2" board rel resolved
|
|
45
|
+
board="$(board_path "$company")"
|
|
46
|
+
rel="companies/$company/projects/$project/prd.json"
|
|
47
|
+
resolved=""
|
|
48
|
+
if [ -f "$board" ]; then
|
|
49
|
+
resolved="$(jq -r --arg path "$rel" --arg project "$project" '
|
|
50
|
+
[.projects[]? | select(.prd_path == $path or .id == $project)] | first | (.mesh_project_id // .id // empty)
|
|
51
|
+
' "$board" 2>/dev/null | tr -d '\r' || true)"
|
|
52
|
+
fi
|
|
53
|
+
printf '%s\n' "${resolved:-$project}"
|
|
25
54
|
}
|
|
26
55
|
|
|
27
56
|
has_board() {
|
|
28
|
-
local company="$1" project="$2" uid
|
|
57
|
+
local company="$1" project="$2" uid mesh_project
|
|
29
58
|
uid="$(resolve_uid "$company")"
|
|
30
|
-
|
|
59
|
+
mesh_project="$(mesh_project_id_for_prd "$company" "$project")"
|
|
60
|
+
[ -f "${HOME}/.hq/work-mesh/cache/projects/${uid}/${mesh_project}.json" ] \
|
|
61
|
+
|| [ -f "${HOME}/.hq/work-mesh/cache/projects/${uid}/${project}.json" ]
|
|
31
62
|
}
|
|
32
63
|
|
|
33
64
|
story_not_done() {
|
|
@@ -69,7 +100,7 @@ mark_pending() {
|
|
|
69
100
|
prd="$ROOT/companies/$company/projects/$project/prd.json"
|
|
70
101
|
rel="companies/$company/projects/$project/prd.json"
|
|
71
102
|
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
72
|
-
title="$(jq -r '.name // empty' "$prd" 2>/dev/null || true)"
|
|
103
|
+
title="$(jq -r '.name // empty' "$prd" 2>/dev/null | tr -d '\r' || true)"
|
|
73
104
|
[ -n "$title" ] || title="$project"
|
|
74
105
|
mkdir -p "$(dirname "$board")"
|
|
75
106
|
if [ ! -f "$board" ]; then
|
|
@@ -108,19 +139,21 @@ mark_pending() {
|
|
|
108
139
|
register_one() {
|
|
109
140
|
local company="$1" project="$2"
|
|
110
141
|
local prd="$ROOT/companies/$company/projects/$project/prd.json"
|
|
142
|
+
local mesh_project
|
|
143
|
+
mesh_project="$(mesh_project_id_for_prd "$company" "$project")"
|
|
111
144
|
[ -f "$prd" ] || die "registration incomplete: missing $prd"
|
|
112
145
|
if ! cli_has_project_ensure; then
|
|
113
146
|
mark_pending "$company" "$project"
|
|
114
147
|
die "hq-cli ${MIN_HQ_CLI} or newer is required; ${company}/${project} stays local until then"
|
|
115
148
|
fi
|
|
116
149
|
local ensure_json
|
|
117
|
-
if ! ensure_json="$(hq mesh project ensure "$
|
|
150
|
+
if ! ensure_json="$(hq mesh project ensure "$mesh_project" --company "$company" --stories-file "$prd" --json)"; then
|
|
118
151
|
echo "registration incomplete: hq mesh project ensure failed for $company/$project" >&2
|
|
119
152
|
exit 1
|
|
120
153
|
fi
|
|
121
154
|
local thread channel
|
|
122
|
-
thread="$(printf '%s' "$ensure_json" | jq -er '.threadId // empty')" || die "registration incomplete: ensure output missing threadId"
|
|
123
|
-
channel="$(printf '%s' "$ensure_json" | jq -er '.channelId // empty')" || die "registration incomplete: ensure output missing channelId"
|
|
155
|
+
thread="$(printf '%s' "$ensure_json" | jq -er '.threadId // empty' | tr -d '\r')" || die "registration incomplete: ensure output missing threadId"
|
|
156
|
+
channel="$(printf '%s' "$ensure_json" | jq -er '.channelId // empty' | tr -d '\r')" || die "registration incomplete: ensure output missing channelId"
|
|
124
157
|
[ -n "$thread" ] || die "registration incomplete: empty threadId"
|
|
125
158
|
[ -n "$channel" ] || die "registration incomplete: empty channelId"
|
|
126
159
|
|
|
@@ -128,7 +161,7 @@ register_one() {
|
|
|
128
161
|
local rel="companies/$company/projects/$project/prd.json"
|
|
129
162
|
local now title tmp
|
|
130
163
|
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
131
|
-
title="$(jq -r '.name // empty' "$prd")"
|
|
164
|
+
title="$(jq -r '.name // empty' "$prd" | tr -d '\r')"
|
|
132
165
|
[ -n "$title" ] || title="$project"
|
|
133
166
|
mkdir -p "$(dirname "$board")"
|
|
134
167
|
if [ ! -f "$board" ]; then
|
|
@@ -136,7 +169,8 @@ register_one() {
|
|
|
136
169
|
fi
|
|
137
170
|
tmp="$(mktemp)"
|
|
138
171
|
jq \
|
|
139
|
-
--arg id "$
|
|
172
|
+
--arg id "$mesh_project" \
|
|
173
|
+
--arg project "$project" \
|
|
140
174
|
--arg path "$rel" \
|
|
141
175
|
--arg thread "$thread" \
|
|
142
176
|
--arg channel "$channel" \
|
|
@@ -144,9 +178,9 @@ register_one() {
|
|
|
144
178
|
--arg title "$title" \
|
|
145
179
|
'
|
|
146
180
|
.projects = (.projects // []) |
|
|
147
|
-
if any(.projects[]; .id == $id or .prd_path == $path or .mesh_project_id == $id) then
|
|
181
|
+
if any(.projects[]; .id == $id or .id == $project or .prd_path == $path or .mesh_project_id == $id) then
|
|
148
182
|
.projects |= map(
|
|
149
|
-
if .id == $id or .prd_path == $path or .mesh_project_id == $id then
|
|
183
|
+
if .id == $id or .id == $project or .prd_path == $path or .mesh_project_id == $id then
|
|
150
184
|
. + {threadId:$thread, channelId:$channel, updated_at:$now, prd_path:(.prd_path // $path), mesh_project_id:$id} | del(.pending_registration)
|
|
151
185
|
else . end
|
|
152
186
|
)
|
|
@@ -169,14 +203,45 @@ register_one() {
|
|
|
169
203
|
jq -e \
|
|
170
204
|
--arg id "$project" \
|
|
171
205
|
--arg path "$rel" \
|
|
206
|
+
--arg mesh "$mesh_project" \
|
|
172
207
|
--arg thread "$thread" \
|
|
173
208
|
--arg channel "$channel" \
|
|
174
|
-
'any(.projects[]?; (.id == $id or .prd_path == $path or .mesh_project_id == $
|
|
209
|
+
'any(.projects[]?; (.id == $id or .prd_path == $path or .mesh_project_id == $mesh) and .mesh_project_id == $mesh and .threadId == $thread and .channelId == $channel)' \
|
|
175
210
|
"$board" >/dev/null \
|
|
176
211
|
|| die "registration incomplete: board.json entry for $project did not verify"
|
|
177
212
|
echo "registered $company/$project thread=$thread channel=$channel"
|
|
178
213
|
}
|
|
179
214
|
|
|
215
|
+
register_brainstorm() {
|
|
216
|
+
local company="$1" project="$2" board title description ensure_json thread channel tmp
|
|
217
|
+
resolve_uid "$company" >/dev/null
|
|
218
|
+
board="$(board_path "$company")"
|
|
219
|
+
[ -f "$board" ] || die "registration incomplete: missing $board"
|
|
220
|
+
title="$(jq -r --arg id "$project" '[.projects[]? | select(.id == $id or .mesh_project_id == $id)] | first | .title // empty' "$board" | tr -d '\r')"
|
|
221
|
+
description="$(jq -r --arg id "$project" '[.projects[]? | select(.id == $id or .mesh_project_id == $id)] | first | .description // empty' "$board" | tr -d '\r')"
|
|
222
|
+
[ -n "$title" ] || die "registration incomplete: missing local Board project $company/$project"
|
|
223
|
+
if ! ensure_json="$(hq mesh project set "$project" --company "$company" --name "$title" --description "$description" --create --json)"; then
|
|
224
|
+
die "registration incomplete: hq mesh project set failed for $company/$project"
|
|
225
|
+
fi
|
|
226
|
+
thread="$(printf '%s' "$ensure_json" | jq -er '.registration.threadId // .threadId // empty' | tr -d '\r')" \
|
|
227
|
+
|| die "registration incomplete: project set output missing threadId"
|
|
228
|
+
channel="$(printf '%s' "$ensure_json" | jq -er '.registration.channelId // .channelId // empty' | tr -d '\r')" \
|
|
229
|
+
|| die "registration incomplete: project set output missing channelId"
|
|
230
|
+
[ -n "$thread" ] && [ -n "$channel" ] || die "registration incomplete: empty threadId or channelId"
|
|
231
|
+
tmp="$(mktemp)"
|
|
232
|
+
jq --arg id "$project" --arg thread "$thread" --arg channel "$channel" --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '
|
|
233
|
+
.projects |= map(if .id == $id or .mesh_project_id == $id then . + {
|
|
234
|
+
threadId:$thread, channelId:$channel, mesh_project_id:$id,
|
|
235
|
+
mesh_registration_prompt_state:"accepted", updated_at:$now
|
|
236
|
+
} | del(.pending_registration) else . end)
|
|
237
|
+
' "$board" > "$tmp"
|
|
238
|
+
mv "$tmp" "$board"
|
|
239
|
+
jq -e --arg id "$project" --arg thread "$thread" --arg channel "$channel" \
|
|
240
|
+
'any(.projects[]?; (.id == $id or .mesh_project_id == $id) and .threadId == $thread and .channelId == $channel)' \
|
|
241
|
+
"$board" >/dev/null || die "registration incomplete: local Board registration did not verify"
|
|
242
|
+
echo "registered $company/$project thread=$thread channel=$channel"
|
|
243
|
+
}
|
|
244
|
+
|
|
180
245
|
audit() {
|
|
181
246
|
local company="$1"
|
|
182
247
|
[ -n "$company" ] || die "usage: register-project.sh --audit <company>"
|
|
@@ -248,7 +313,10 @@ retry_pending() {
|
|
|
248
313
|
done < <(jq -r '[.projects[]? | select(.pending_registration == true) | .id // empty] | .[]' "$board" 2>/dev/null | tr -d '\r' || true)
|
|
249
314
|
}
|
|
250
315
|
|
|
251
|
-
if [ "${1:-}" = "--
|
|
316
|
+
if [ "${1:-}" = "--brainstorm" ]; then
|
|
317
|
+
[ "$#" -eq 3 ] || die "usage: register-project.sh --brainstorm <company> <board-project-id>"
|
|
318
|
+
register_brainstorm "$2" "$3"
|
|
319
|
+
elif [ "${1:-}" = "--audit" ]; then
|
|
252
320
|
audit "${2:-}"
|
|
253
321
|
elif [ "${1:-}" = "--backfill" ]; then
|
|
254
322
|
shift
|
|
@@ -258,5 +326,5 @@ elif [ "${1:-}" = "--retry-pending" ]; then
|
|
|
258
326
|
elif [ $# -eq 2 ]; then
|
|
259
327
|
register_one "$1" "$2"
|
|
260
328
|
else
|
|
261
|
-
die "usage: register-project.sh <company> <project> | --audit <company> | --backfill <company> --only-active [--yes] | --retry-pending <company>"
|
|
329
|
+
die "usage: register-project.sh <company> <project> | --brainstorm <company> <board-project-id> | --audit <company> | --backfill <company> --only-active [--yes] | --retry-pending <company>"
|
|
262
330
|
fi
|
|
@@ -199,11 +199,11 @@ existing_state() {
|
|
|
199
199
|
fi
|
|
200
200
|
|
|
201
201
|
local record_thread record_session record_at record_epoch record_generation current_epoch
|
|
202
|
-
record_thread="$(jq -r '.thread_id' "$record_path")"
|
|
203
|
-
record_session="$(jq -r '.session_id' "$record_path")"
|
|
204
|
-
record_at="$(jq -r '.resumed_at' "$record_path")"
|
|
205
|
-
record_epoch="$(jq -r '.resumed_epoch' "$record_path")"
|
|
206
|
-
record_generation="$(jq -r '.generation' "$record_path")"
|
|
202
|
+
record_thread="$(jq -r '.thread_id' "$record_path" | tr -d '\r')"
|
|
203
|
+
record_session="$(jq -r '.session_id' "$record_path" | tr -d '\r')"
|
|
204
|
+
record_at="$(jq -r '.resumed_at' "$record_path" | tr -d '\r')"
|
|
205
|
+
record_epoch="$(jq -r '.resumed_epoch' "$record_path" | tr -d '\r')"
|
|
206
|
+
record_generation="$(jq -r '.generation' "$record_path" | tr -d '\r')"
|
|
207
207
|
|
|
208
208
|
if [[ "$record_thread" != "$thread_id" || ! "$record_epoch" =~ ^[0-9]+$ ]]; then
|
|
209
209
|
emit_stale "invalid-record" "$record_session" "$record_at" "$record_generation"
|
|
@@ -113,7 +113,10 @@ const sortKeys = (v) =>
|
|
|
113
113
|
|
|
114
114
|
function readJson(p, dflt) {
|
|
115
115
|
if (!fs.existsSync(p)) return dflt;
|
|
116
|
-
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch (e) {
|
|
116
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch (e) {
|
|
117
|
+
console.error(`share-suggestion-state: unable to read ${p} (${e.name || "Error"})`);
|
|
118
|
+
process.exit(3);
|
|
119
|
+
}
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
function writeJson(p, payload) {
|
|
@@ -537,5 +540,10 @@ process.exit(0);
|
|
|
537
540
|
JS
|
|
538
541
|
}
|
|
539
542
|
|
|
540
|
-
|
|
543
|
+
# Exit 3 means an existing state file could not be read; callers see that
|
|
544
|
+
# failure. Every other error is logged and the helper exits 0.
|
|
545
|
+
main "$@"
|
|
546
|
+
status=$?
|
|
547
|
+
[ "$status" -eq 3 ] && exit 3
|
|
548
|
+
[ "$status" -eq 0 ] || log_error "internal error"
|
|
541
549
|
exit 0
|
|
@@ -144,9 +144,41 @@ if [ "${HQ_WORK_MESH_RECONCILE_STUB:-}" = "1" ]; then
|
|
|
144
144
|
fi
|
|
145
145
|
|
|
146
146
|
HQ_BIN="$(command -v hq 2>/dev/null || true)"
|
|
147
|
-
if [ -
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
if [ -z "$HQ_BIN" ]; then
|
|
148
|
+
printf '%s\n' 'Work Mesh reconcile skipped: hq CLI not found on PATH.' >&2
|
|
149
|
+
else
|
|
150
|
+
flag_reader="$ROOT/.claude/hooks/work-mesh-daemon-not-loaded-flag.cjs"
|
|
151
|
+
daemon_warning_enabled=false
|
|
152
|
+
if [[ -n "${HQ_FLAGS_API_URL:-}" \
|
|
153
|
+
&& "${HQ_COMPANY_UID:-}" =~ ^cmp_[A-Za-z0-9]{3,128}$ ]] \
|
|
154
|
+
&& command -v node >/dev/null 2>&1 && [[ -f "$flag_reader" ]]; then
|
|
155
|
+
daemon_warning_enabled="$(HQ_CLI_BIN="$HQ_BIN" node "$flag_reader")" || daemon_warning_enabled=false
|
|
156
|
+
fi
|
|
157
|
+
if [ "$daemon_warning_enabled" = "true" ]; then
|
|
158
|
+
# Status only reads the installed service and its live state. Keep this hook
|
|
159
|
+
# bounded, and fail quiet when the CLI or service manager cannot answer.
|
|
160
|
+
daemon_status_file="$(mktemp "${TMPDIR:-/tmp}/hq-work-mesh-daemon-status.XXXXXX" 2>/dev/null)" || daemon_status_file=""
|
|
161
|
+
if [ -n "$daemon_status_file" ]; then
|
|
162
|
+
HQ_NO_UPDATE_CHECK=1 "$HQ_BIN" mesh daemon status --json >"$daemon_status_file" 2>/dev/null &
|
|
163
|
+
daemon_status_pid=$!
|
|
164
|
+
( sleep 2; kill "$daemon_status_pid" 2>/dev/null || true ) &
|
|
165
|
+
daemon_status_watchdog=$!
|
|
166
|
+
daemon_status_rc=0
|
|
167
|
+
wait "$daemon_status_pid" || daemon_status_rc=$?
|
|
168
|
+
kill "$daemon_status_watchdog" 2>/dev/null || true
|
|
169
|
+
wait "$daemon_status_watchdog" 2>/dev/null || true
|
|
170
|
+
if [ "$daemon_status_rc" -eq 0 ] && command -v jq >/dev/null 2>&1 && \
|
|
171
|
+
jq -e '(.ok == true) and (.running == false) and (.message | type == "string" and contains("installed ("))' \
|
|
172
|
+
"$daemon_status_file" >/dev/null 2>&1; then
|
|
173
|
+
printf '%s\n' 'Work Mesh daemon is installed but not loaded; start it with: hq mesh daemon install' >&2
|
|
174
|
+
fi
|
|
175
|
+
rm -f "$daemon_status_file" 2>/dev/null || true
|
|
176
|
+
fi
|
|
177
|
+
fi
|
|
178
|
+
if [ -f "$OBS_FILE" ]; then
|
|
179
|
+
nohup "$HQ_BIN" mesh context reconcile --observation-file "$OBS_FILE" --machine \
|
|
180
|
+
>/dev/null 2>&1 </dev/null &
|
|
181
|
+
disown 2>/dev/null || true
|
|
182
|
+
fi
|
|
151
183
|
fi
|
|
152
184
|
exit 0
|
|
@@ -249,7 +249,7 @@ for wt in "${CANDIDATES[@]:-}"; do
|
|
|
249
249
|
created_epoch="$wt_mtime"
|
|
250
250
|
stamp_file="$GC_META_DIR/$(path_key "$wt").json"
|
|
251
251
|
if [[ -f "$stamp_file" ]]; then
|
|
252
|
-
stamped=$(jq -r '.createdAtEpoch // empty' "$stamp_file" 2>/dev/null || echo "")
|
|
252
|
+
stamped=$(jq -r '.createdAtEpoch // empty' "$stamp_file" 2>/dev/null | tr -d '\r' || echo "")
|
|
253
253
|
[[ -n "$stamped" ]] && created_epoch="$stamped"
|
|
254
254
|
fi
|
|
255
255
|
age_days=$(( (NOW - created_epoch) / 86400 ))
|
|
@@ -799,15 +799,119 @@ export function worklistCommand(args, root) {
|
|
|
799
799
|
if (validation.code !== 0)
|
|
800
800
|
return err(2, `${validation.stderr}${validation.stdout}${validation.stderr}`);
|
|
801
801
|
const ledger = path.join(directory, ".processed");
|
|
802
|
+
const skipLedger = path.join(directory, ".skipped");
|
|
803
|
+
const parserVersion = 2;
|
|
802
804
|
if (doneItem) {
|
|
803
|
-
fs.appendFileSync(ledger, `${path.basename(doneItem)}\n`);
|
|
805
|
+
fs.appendFileSync(ledger, `${path.basename(doneItem)}\t${parserVersion}\n`);
|
|
804
806
|
return ok();
|
|
805
807
|
}
|
|
808
|
+
const hasFrontmatterField = (file, field) => {
|
|
809
|
+
if (!fs.existsSync(file))
|
|
810
|
+
return false;
|
|
811
|
+
let frontmatter = 0;
|
|
812
|
+
for (const rawLine of fs.readFileSync(file, "utf8").split("\n")) {
|
|
813
|
+
const line = rawLine.replace(/\r$/, "");
|
|
814
|
+
if (line === "---") {
|
|
815
|
+
frontmatter += 1;
|
|
816
|
+
if (frontmatter === 2)
|
|
817
|
+
return false;
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (frontmatter === 1 && line.startsWith(`${field}:`))
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
return false;
|
|
824
|
+
};
|
|
825
|
+
const rewriteIfChanged = (file, original, output) => {
|
|
826
|
+
if (output === original)
|
|
827
|
+
return;
|
|
828
|
+
const temporary = `${file}.native-${process.pid}`;
|
|
829
|
+
fs.writeFileSync(temporary, output);
|
|
830
|
+
fs.renameSync(temporary, file);
|
|
831
|
+
};
|
|
832
|
+
// Match the shell's parser-version migration. Unstamped legacy rows are
|
|
833
|
+
// stamped unless they carry the old resolved_participants bug signature;
|
|
834
|
+
// older parser versions are dropped so those sources are re-evaluated.
|
|
835
|
+
let processed = "";
|
|
836
|
+
if (fs.existsSync(ledger)) {
|
|
837
|
+
const original = fs.readFileSync(ledger, "utf8");
|
|
838
|
+
const kept = [];
|
|
839
|
+
let changed = false;
|
|
840
|
+
const rows = original.split("\n");
|
|
841
|
+
if (rows.at(-1) === "")
|
|
842
|
+
rows.pop();
|
|
843
|
+
for (const rawLine of rows) {
|
|
844
|
+
const fields = rawLine.split("\t");
|
|
845
|
+
const basename = (fields[0] ?? "").replace(/\r$/, "");
|
|
846
|
+
const version = (fields[1] ?? "").replace(/\r$/, "");
|
|
847
|
+
if (!basename)
|
|
848
|
+
continue;
|
|
849
|
+
if (version) {
|
|
850
|
+
if (/^[+-]?\d+$/.test(version) && Number(version) < parserVersion) {
|
|
851
|
+
changed = true;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
kept.push(`${basename}\t${version}`);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
changed = true;
|
|
858
|
+
if (hasFrontmatterField(path.join(directory, basename), "resolved_participants"))
|
|
859
|
+
continue;
|
|
860
|
+
kept.push(`${basename}\t${parserVersion}`);
|
|
861
|
+
}
|
|
862
|
+
processed = kept.join("\n");
|
|
863
|
+
if (kept.length > 0)
|
|
864
|
+
processed += "\n";
|
|
865
|
+
if (changed)
|
|
866
|
+
rewriteIfChanged(ledger, original, processed);
|
|
867
|
+
}
|
|
868
|
+
let skipped = "";
|
|
869
|
+
if (fs.existsSync(skipLedger)) {
|
|
870
|
+
const original = fs.readFileSync(skipLedger, "utf8");
|
|
871
|
+
const kept = [];
|
|
872
|
+
let changed = false;
|
|
873
|
+
const rows = original.split("\n");
|
|
874
|
+
if (rows.at(-1) === "")
|
|
875
|
+
rows.pop();
|
|
876
|
+
for (const rawLine of rows) {
|
|
877
|
+
const fields = rawLine.split("\t");
|
|
878
|
+
const basename = fields[0] ?? "";
|
|
879
|
+
const reason = fields[1] ?? "";
|
|
880
|
+
const version = fields[2] ?? "";
|
|
881
|
+
if (!basename)
|
|
882
|
+
continue;
|
|
883
|
+
if (!version || (/^[+-]?\d+$/.test(version) && Number(version) < parserVersion)) {
|
|
884
|
+
changed = true;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
kept.push(`${basename}\t${reason}\t${version}`);
|
|
888
|
+
}
|
|
889
|
+
skipped = kept.join("\n");
|
|
890
|
+
if (kept.length > 0)
|
|
891
|
+
skipped += "\n";
|
|
892
|
+
if (changed)
|
|
893
|
+
rewriteIfChanged(skipLedger, original, skipped);
|
|
894
|
+
}
|
|
895
|
+
const isProcessed = (basename) => processed.split("\n").some((line) => {
|
|
896
|
+
if (!line)
|
|
897
|
+
return false;
|
|
898
|
+
const [name, version] = line.split("\t");
|
|
899
|
+
if (name !== basename)
|
|
900
|
+
return false;
|
|
901
|
+
if (version === undefined || version === "")
|
|
902
|
+
return true;
|
|
903
|
+
return Number(version) >= parserVersion;
|
|
904
|
+
});
|
|
905
|
+
const isSkippedAtCurrentVersion = (basename) => skipped.split("\n").some((line) => {
|
|
906
|
+
if (!line)
|
|
907
|
+
return false;
|
|
908
|
+
const fields = line.split("\t");
|
|
909
|
+
return fields[0] === basename && fields[2] === String(parserVersion);
|
|
910
|
+
});
|
|
806
911
|
const run = awkField(sourceFile, "run", false);
|
|
807
912
|
const rule = awkField(sourceFile, "audience_rule", false);
|
|
808
913
|
if (run === "cloud" && !force)
|
|
809
914
|
return err(4, "assigned to cloud agent (run: cloud); pass --force to run locally\n");
|
|
810
|
-
const processed = fs.existsSync(ledger) ? fs.readFileSync(ledger, "utf8").split("\n") : [];
|
|
811
915
|
const entries = fs.existsSync(directory)
|
|
812
916
|
? fs.readdirSync(directory).filter((name) => name.endsWith(".md") && !name.startsWith("."))
|
|
813
917
|
.filter((name) => { try {
|
|
@@ -821,12 +925,12 @@ export function worklistCommand(args, root) {
|
|
|
821
925
|
const output = [];
|
|
822
926
|
let count = 0;
|
|
823
927
|
for (const name of entries) {
|
|
824
|
-
if (
|
|
928
|
+
if (isProcessed(name) || isSkippedAtCurrentVersion(name))
|
|
825
929
|
continue;
|
|
826
930
|
if (/^[+-]?\d+$/.test(limit) && count >= Number(limit))
|
|
827
931
|
break;
|
|
828
932
|
if (!/^[+-]?\d+$/.test(limit)) {
|
|
829
|
-
process.stderr.write(`${resolveBundledAsset(`${ASSET}ontology-source-worklist.sh`)}: line
|
|
933
|
+
process.stderr.write(`${resolveBundledAsset(`${ASSET}ontology-source-worklist.sh`)}: line 186: [: ${limit}: integer expression expected\n`);
|
|
830
934
|
}
|
|
831
935
|
count += 1;
|
|
832
936
|
const file = path.join(directory, name);
|
|
@@ -860,6 +964,8 @@ export function worklistCommand(args, root) {
|
|
|
860
964
|
}
|
|
861
965
|
}
|
|
862
966
|
if (!audience) {
|
|
967
|
+
fs.appendFileSync(skipLedger, `${name}\tno-audience\t${parserVersion}\n`);
|
|
968
|
+
skipped += `${name}\tno-audience\t${parserVersion}\n`;
|
|
863
969
|
output.push(`${JSON.stringify({ file: relative, skip: "no-audience" })}\n`);
|
|
864
970
|
continue;
|
|
865
971
|
}
|
|
@@ -51,11 +51,44 @@ function companyUid(company, deps) {
|
|
|
51
51
|
return direct;
|
|
52
52
|
const file = path.join(deps.root, "companies", company, ".company-uid");
|
|
53
53
|
try {
|
|
54
|
-
|
|
54
|
+
const fromFile = deps.fs.readFileSync(file, "utf8").replace(/[\s\u0085\u00a0]+/g, "");
|
|
55
|
+
if (fromFile)
|
|
56
|
+
return fromFile;
|
|
57
|
+
}
|
|
58
|
+
catch { /* shell falls through to companies/manifest.yaml */ }
|
|
59
|
+
const manifest = path.join(deps.root, "companies", "manifest.yaml");
|
|
60
|
+
let contents;
|
|
61
|
+
try {
|
|
62
|
+
contents = deps.fs.readFileSync(manifest, "utf8");
|
|
55
63
|
}
|
|
56
64
|
catch {
|
|
57
65
|
return null;
|
|
58
66
|
}
|
|
67
|
+
let inCompanies = false;
|
|
68
|
+
let inCompany = false;
|
|
69
|
+
for (const line of contents.split(/\r?\n/u)) {
|
|
70
|
+
if (/^companies:[\t ]*$/u.test(line)) {
|
|
71
|
+
inCompanies = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (inCompanies && /^[^\t ]/u.test(line)) {
|
|
75
|
+
inCompanies = false;
|
|
76
|
+
inCompany = false;
|
|
77
|
+
}
|
|
78
|
+
if (inCompanies && /^ {2}[^ #][^:]*:/u.test(line)) {
|
|
79
|
+
const slug = line.trim().split(/[\t ]+/u, 1)[0]?.replace(/:$/u, "");
|
|
80
|
+
inCompany = slug === company;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (inCompany && /^ {2}[^\t ]/u.test(line))
|
|
84
|
+
break;
|
|
85
|
+
if (inCompany) {
|
|
86
|
+
const fields = line.trim().split(/[\t ]+/u);
|
|
87
|
+
if (fields[0] === "cloud_uid:" && fields[1])
|
|
88
|
+
return fields[1].replace(/["']/gu, "").replace(/[\t ]+$/u, "");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
59
92
|
}
|
|
60
93
|
function boardPath(company, deps) { return path.join(deps.root, "companies", company, "board.json"); }
|
|
61
94
|
function runHelpProbe(deps) {
|
|
@@ -217,7 +250,7 @@ function audit(company, deps) {
|
|
|
217
250
|
return false;
|
|
218
251
|
const uid = companyUid(company, deps);
|
|
219
252
|
if (uid === null) {
|
|
220
|
-
stderr += `no company uid for ${company} (set HQ_COMPANY_UID
|
|
253
|
+
stderr += `no company uid for ${company} (set HQ_COMPANY_UID, companies/${company}/.company-uid, or manifest cloud_uid)\n`;
|
|
221
254
|
return true;
|
|
222
255
|
}
|
|
223
256
|
return !deps.fs.existsSync(path.join(deps.env.HOME || os.homedir(), ".hq/work-mesh/cache/projects", uid, `${project}.json`));
|
|
@@ -243,7 +276,7 @@ function storyNotDone(prd, deps, diagnostics) {
|
|
|
243
276
|
diagnostics.push(jq.stderr);
|
|
244
277
|
else if (jq.error?.code === "ENOENT") {
|
|
245
278
|
const script = resolveBundledAsset(REGISTER_PROJECT_ASSET);
|
|
246
|
-
diagnostics.push(`${script}: line
|
|
279
|
+
diagnostics.push(`${script}: line 65: jq: command not found\n`);
|
|
247
280
|
}
|
|
248
281
|
return false;
|
|
249
282
|
}
|