@indigoai-us/hq-cli 5.316.0 → 5.318.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.
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env bash
2
+ # session-title-config.sh — resolve the session auto-naming settings.
3
+ #
4
+ # Prints three lines to stdout:
5
+ # enabled=true|false
6
+ # mode=full|auto
7
+ # desktop_autoname=ignore-first|respect
8
+ #
9
+ # Resolution, first match wins: env HQ_SESSION_TITLE / HQ_DISABLED_HOOKS, then
10
+ # personal/settings/session-title.yaml, then core/settings/session-title.yaml,
11
+ # then the built-in defaults (enabled=true, mode=full).
12
+ #
13
+ # Deliberately grep/sed only — no python3, no node. This runs inside the
14
+ # SessionStart/UserPromptSubmit hook path, which must stay dependency-free on
15
+ # machines that have neither (see core/scripts/tests/hooks-no-python.test.sh).
16
+ #
17
+ # Usage: session-title-config.sh [--root <hq-root>]
18
+ set -uo pipefail
19
+
20
+ HQ_ROOT="${HQ_ROOT:-${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}}"
21
+ while [ $# -gt 0 ]; do
22
+ case "$1" in
23
+ --root) HQ_ROOT="${2:-$HQ_ROOT}"; shift 2 ;;
24
+ *) shift ;;
25
+ esac
26
+ done
27
+
28
+ ENABLED="true"
29
+ MODE="full"
30
+ DESKTOP_AUTONAME="ignore-first"
31
+
32
+ # Read a top-level scalar key from a simple YAML file. Ignores comments,
33
+ # indented keys, and inline trailing comments. Good enough for flat settings;
34
+ # these files are documented as flat.
35
+ yaml_scalar() {
36
+ local file="$1" key="$2" line=""
37
+ [ -f "$file" ] || return 1
38
+ line="$(grep -E "^${key}:[[:space:]]*" "$file" 2>/dev/null | head -n 1)" || return 1
39
+ [ -n "$line" ] || return 1
40
+ line="${line#*:}"
41
+ line="$(printf '%s' "$line" | sed -E 's/[[:space:]]*#.*$//; s/^[[:space:]]+//; s/[[:space:]]+$//; s/^["'"'"']//; s/["'"'"']$//')"
42
+ [ -n "$line" ] || return 1
43
+ printf '%s' "$line"
44
+ }
45
+
46
+ # Lowest precedence first, so later reads overwrite earlier ones.
47
+ for f in "$HQ_ROOT/core/settings/session-title.yaml" \
48
+ "$HQ_ROOT/personal/settings/session-title.yaml"; do
49
+ v="$(yaml_scalar "$f" enabled)" && [ -n "$v" ] && ENABLED="$v"
50
+ v="$(yaml_scalar "$f" mode)" && [ -n "$v" ] && MODE="$v"
51
+ v="$(yaml_scalar "$f" desktop_autoname)" && [ -n "$v" ] && DESKTOP_AUTONAME="$v"
52
+ done
53
+
54
+ case "$ENABLED" in
55
+ 0|false|FALSE|False|off|OFF|no|NO) ENABLED="false" ;;
56
+ *) ENABLED="true" ;;
57
+ esac
58
+ case "$MODE" in
59
+ auto|AUTO|hook|deterministic) MODE="auto" ;;
60
+ *) MODE="full" ;;
61
+ esac
62
+ case "$DESKTOP_AUTONAME" in
63
+ respect|RESPECT|off|OFF|false|FALSE|no|NO) DESKTOP_AUTONAME="respect" ;;
64
+ *) DESKTOP_AUTONAME="ignore-first" ;;
65
+ esac
66
+
67
+ # Env wins over every file.
68
+ case "${HQ_SESSION_TITLE:-}" in
69
+ 0|false|FALSE|off|OFF|no|NO) ENABLED="false" ;;
70
+ auto|AUTO) MODE="auto" ;;
71
+ esac
72
+ case "${HQ_DISABLED_HOOKS:-}" in
73
+ *session-title*) ENABLED="false" ;;
74
+ esac
75
+
76
+ printf 'enabled=%s\nmode=%s\ndesktop_autoname=%s\n' "$ENABLED" "$MODE" "$DESKTOP_AUTONAME"
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env bash
2
+ # session-title.sh — compute an HQ session-title string for the Claude Code
3
+ # sidebar ("Recents") / terminal tab title / `/resume` picker.
4
+ #
5
+ # Pure compute: reads existing session + orchestrator state and prints ONE
6
+ # title line to stdout. It does NOT emit hook JSON — the SessionStart /
7
+ # UserPromptSubmit wrapper (.claude/hooks/session-title.sh) wraps this output
8
+ # in the hookSpecificOutput.sessionTitle envelope.
9
+ #
10
+ # Title convention: {glyph} {COMPANY} · {Product} · {subject}
11
+ # - emoji is a STATUS flag only (▶️ running, ✅ recently completed); it is
12
+ # omitted otherwise — the command word already conveys the mode.
13
+ # - company : slug from the active project path, or "hq-core" for builder
14
+ # work, or the sole company on a single-company HQ; else dropped.
15
+ # - project : active project slug; dropped when there is no project.
16
+ # - command : active slash command / mode word (e.g. brainstorm, plan,
17
+ # run-project), defaulting to "chat".
18
+ #
19
+ # Usage: session-title.sh --session-id <id> [--command <word>]
20
+ set -uo pipefail
21
+
22
+ # core/scripts/ -> ../.. is the HQ root. It was "/.." (i.e. core/), which
23
+ # silently resolved every lookup against the wrong tree whenever
24
+ # CLAUDE_PROJECT_DIR was unset, yielding a projectless title.
25
+ HQ_ROOT="${HQ_ROOT:-${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}}"
26
+
27
+ SESSION_ID="default"
28
+ COMMAND=""
29
+ SESSION_CWD=""
30
+ while [ $# -gt 0 ]; do
31
+ case "$1" in
32
+ --session-id) SESSION_ID="${2:-default}"; shift 2 ;;
33
+ --command) COMMAND="${2:-}"; shift 2 ;;
34
+ --cwd) SESSION_CWD="${2:-}"; shift 2 ;;
35
+ *) shift ;;
36
+ esac
37
+ done
38
+
39
+ command -v node >/dev/null 2>&1 || exit 0
40
+
41
+ HQ_ROOT="$HQ_ROOT" SESSION_ID="$SESSION_ID" CMD="$COMMAND" SESSION_CWD="$SESSION_CWD" node - <<'JS'
42
+ const fs = require("fs");
43
+ const path = require("path");
44
+
45
+ const hq = process.env.HQ_ROOT || "";
46
+ const sid = process.env.SESSION_ID || "default";
47
+ let command = (process.env.CMD || "").trim().replace(/^\/+/, "");
48
+
49
+ const key = sid.replace(/[^A-Za-z0-9._-]/g, "_") || "default";
50
+ const state = path.join(hq, ".claude", "state");
51
+
52
+ const firstLine = (p) => {
53
+ try {
54
+ for (let line of fs.readFileSync(p, "utf8").split(/\r?\n/)) {
55
+ line = line.trim();
56
+ if (line) return line;
57
+ }
58
+ } catch (e) {}
59
+ return "";
60
+ };
61
+
62
+ // --- resolve active project path (session-scoped, then global fallback) ---
63
+ // Session-scoped ONLY. There used to be a fallback to the machine-global
64
+ // .claude/state/active-session-project here; it made every session without its
65
+ // own entry inherit whatever project was last active anywhere on the box (142
66
+ // unrelated sessions all named "learn-policies-only · chat"). Session-scoped
67
+ // state must fail to empty, never to a shared global.
68
+ let projPath = firstLine(path.join(state, "auto-session-project-" + key));
69
+
70
+ let company = "";
71
+ let project = "";
72
+ if (projPath) {
73
+ let rel = projPath;
74
+ try {
75
+ const resolved = fs.realpathSync(projPath);
76
+ const hqResolved = fs.realpathSync(hq);
77
+ const r = path.relative(hqResolved, resolved);
78
+ if (r === "" || r.startsWith("..") || path.isAbsolute(r)) throw new Error("outside hq");
79
+ rel = r;
80
+ } catch (e) {
81
+ rel = projPath.split(hq.replace(/\/+$/, "") + "/").join("");
82
+ }
83
+ const parts = rel.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
84
+ if (parts.length >= 2 && parts[0] === "companies") {
85
+ company = parts[1];
86
+ project = parts[parts.length - 1];
87
+ } else if (parts.length && parts[0] === "personal") {
88
+ company = "personal"; // rendered as the "ME" org token
89
+ project = parts[parts.length - 1];
90
+ } else if (parts.length) {
91
+ project = parts[parts.length - 1];
92
+ }
93
+ }
94
+
95
+ // --- company fallbacks when no project resolved ---
96
+ if (!company && command === "hqwork") company = "hq-core";
97
+
98
+ if (!company && !project) {
99
+ const manifest = path.join(hq, "companies", "manifest.yaml");
100
+ const slugs = [];
101
+ try {
102
+ let inCompanies = false;
103
+ for (const line of fs.readFileSync(manifest, "utf8").split(/\r?\n/)) {
104
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
105
+ if (inCompanies && /^[^\s#]/.test(line)) break;
106
+ if (/^companies:\s*$/.test(line)) { inCompanies = true; continue; }
107
+ if (inCompanies) {
108
+ const m = line.match(/^ ([a-z][a-z0-9_-]*):\s*$/);
109
+ if (m && m[1] !== "_template") slugs.push(m[1]);
110
+ }
111
+ }
112
+ } catch (e) {}
113
+ if (slugs.length === 1) company = slugs[0];
114
+ }
115
+
116
+ // --- status emoji from the orchestrator ---
117
+ let emoji = "";
118
+ const orch = path.join(hq, "workspace", "orchestrator", "state.json");
119
+ if (project) {
120
+ try {
121
+ const data = JSON.parse(fs.readFileSync(orch, "utf8"));
122
+ for (const p of data.projects || []) {
123
+ const nm = p.name || "";
124
+ const prd = String(p.prdPath || "").replace(/\\/g, "/");
125
+ const expectedProjectPath = company === "personal"
126
+ ? `personal/projects/${project}/`
127
+ : company ? `companies/${company}/projects/${project}/` : "";
128
+ const matchesProject = prd
129
+ ? expectedProjectPath
130
+ ? prd.replace(/^\/+/, "").includes(expectedProjectPath)
131
+ : nm === project || prd.includes("/" + project + "/")
132
+ : nm === project;
133
+ if (matchesProject) {
134
+ const st = p.state || "";
135
+ if (st === "IN_PROGRESS") {
136
+ emoji = "▶️";
137
+ } else if (st === "COMPLETED") {
138
+ const ts = p.updatedAt || p.updated_at || "";
139
+ const t = new Date(ts);
140
+ if (!isNaN(t.getTime()) && (Date.now() - t.getTime()) / 1000 < 86400) {
141
+ emoji = "✅"; // only recent completions
142
+ }
143
+ }
144
+ break;
145
+ }
146
+ }
147
+ } catch (e) {}
148
+ }
149
+
150
+ // --- mode ---
151
+ // The emoji encodes what the session IS DOING, not what domain it belongs to.
152
+ // State is what a human scans a sidebar for ("does this need me?"); domain is
153
+ // already implied by the subject line. Keyed on the active slash command,
154
+ // which the wrapper hook already tracks turn to turn.
155
+ //
156
+ // Exactly ONE glyph, never two — a second glyph beside the first reads as a
157
+ // badge pair and makes the left edge busier without making it more scannable.
158
+ // The single slot shows the most USEFUL thing about the session right now,
159
+ // picked by the precedence ladder in hq-session-title-grammar: work that needs
160
+ // the user or has terminated outranks the session kind, which outranks the
161
+ // workflow stage, which outranks the craft.
162
+ //
163
+ // This script only ever emits a stage or kind glyph. It never emits 🙋 (needs
164
+ // the user) — it cannot know that — and never a craft glyph, since its only
165
+ // subject is a directory slug and any keyword match against that slug is
166
+ // redundant with the slug by construction. Both are the model's job.
167
+ //
168
+ // A handoff has two moments and they are different rows in a sidebar:
169
+ // 📝 the handoff is being written (the session is wrapping up) and 📤 it is
170
+ // ready (the session is closed; resume from the thread). This script emits 📝
171
+ // while /handoff runs; only the model can know when it finished, so it sets 📤.
172
+ // Both are distinct from ✅ "the work shipped" and 🤝 "a person owns it now".
173
+ const MODES = [
174
+ [/^(brainstorm|idea|dream|discover|strategize)$/, "💭"],
175
+ [/^(plan|prd|deep-plan|storyboard|architect|review-plan|codebase-design)$/, "📐"],
176
+ [/^(run-project|execute-task|ship|land|land-batch|run-pipeline|orchestrate|tdd|deploy)$/, "⚡"],
177
+ [/^(review|code-review|security-review)$/, "👀"],
178
+ [/^(quality-gate|smoke|verify|diagnose|investigate)$/, "🧪"],
179
+ [/^(handoff|checkpoint-handoff|retro)$/, "📝"],
180
+ [/^(delegate|new-hire|new-agent|promote)$/, "🤝"],
181
+ [/^(dm|hq-slack|meeting-notes|work-broadcast|signals|hq-share)$/, "💬"],
182
+ [/^(schedule|job|loop)$/, "🔁"],
183
+ ];
184
+
185
+ const modeFor = (word) => {
186
+ for (const [re, ic] of MODES) if (re.test(word)) return ic;
187
+ return "";
188
+ };
189
+
190
+ if (!command) command = "chat";
191
+
192
+ // Precedence: a finished or running project outranks the command word — "it
193
+ // shipped" and "it is executing right now" beat "you typed /plan an hour ago".
194
+ // `emoji` is set above from orchestrator state (✅ recent completion, ▶️ running).
195
+ const mode = emoji === "▶️" ? "⚡" : (emoji || modeFor(command));
196
+
197
+ // --- company ---
198
+ // Always the first text token. Long slugs get an explicit short form from the
199
+ // settings `aliases:` block rather than a mid-word truncation — a single-word
200
+ // slug sliced at 8 characters is unreadable.
201
+ // Built-ins only. Company short forms are USER data — a release-shipped file
202
+ // must never carry tenant slugs (enforced by the slug-scan / denylist-scan CI
203
+ // gates). Users map their own long slugs in personal/settings/session-title.yaml:
204
+ //
205
+ // aliases:
206
+ // some-long-company-slug: SHORT
207
+ //
208
+ const ALIASES = { personal: "ME", "hq-core": "HQ" };
209
+
210
+ // Merge user aliases from the settings files, lowest precedence first.
211
+ const readAliases = (file) => {
212
+ let text = "";
213
+ try { text = fs.readFileSync(file, "utf8"); } catch (e) { return; }
214
+ let inBlock = false;
215
+ for (const raw of text.split(/\r?\n/)) {
216
+ const line = raw.replace(/#.*$/, "").replace(/\s+$/, "");
217
+ if (!line.trim()) continue;
218
+ if (/^aliases:\s*$/.test(line)) { inBlock = true; continue; }
219
+ if (!/^\s/.test(line)) { inBlock = false; continue; } // any new top-level key ends it
220
+ if (!inBlock) continue;
221
+ const m = line.match(/^\s+["']?([A-Za-z0-9._-]+)["']?:\s*["']?([^"'\s]+)["']?\s*$/);
222
+ if (m) ALIASES[m[1]] = m[2];
223
+ }
224
+ };
225
+ readAliases(path.join(hq, "core", "settings", "session-title.yaml"));
226
+ readAliases(path.join(hq, "personal", "settings", "session-title.yaml"));
227
+
228
+ let org = "";
229
+ if (company) {
230
+ org = ALIASES[company];
231
+ if (!org) {
232
+ const head = company.split("-")[0].toUpperCase();
233
+ org = head.length <= 8 ? head : head.slice(0, 8);
234
+ }
235
+ }
236
+
237
+ // --- compose ---
238
+ // Grammar: {glyph} {COMPANY} · {Product} · {subject}
239
+ //
240
+ // Every rendered glyph must mean something. When the mode is unknown the title
241
+ // simply starts with the company — a placeholder icon (the old "🟦") is worse
242
+ // than nothing, because it trains the eye to ignore the emoji column.
243
+ //
244
+ // The hook can only ever put a directory slug in the subject slot. A session
245
+ // that knows what it is actually about should overwrite this via
246
+ // set_session_title, keeping the same grammar.
247
+ // --- product / repo ---
248
+ // The repo or product the session is working in, rendered between the company
249
+ // and the subject so identity survives a narrow sidebar's truncation. Derived
250
+ // from the session cwd when it sits inside repos/; otherwise left to the model.
251
+ //
252
+ // A leading company prefix is stripped, because the company token already said
253
+ // it: repos/public/hq-work under company HQ renders as "Work", not "Hq-work".
254
+ let product = "";
255
+ const cwd = process.env.SESSION_CWD || "";
256
+ if (cwd) {
257
+ const rel = cwd.split(/[\\/]/).filter(Boolean);
258
+ const i = rel.lastIndexOf("repos");
259
+ if (i !== -1 && rel.length > i + 1) {
260
+ // repos/<public|private>/<repo>/... or repos/<repo>/...
261
+ let name = rel[i + 1];
262
+ if ((name === "public" || name === "private") && rel.length > i + 2) name = rel[i + 2];
263
+ if (name) {
264
+ // "hq-work" -> ["HQ", "Work"]. Two-letter tokens are acronyms, not words.
265
+ let words = name.split("-").filter(Boolean).map((w) =>
266
+ w.length <= 2 ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)
267
+ );
268
+ // Drop a leading word the company token already said: under company HQ,
269
+ // "hq-work" is "Work", not "HQ Work".
270
+ if (org && words.length > 1 && words[0].toUpperCase() === org.toUpperCase()) {
271
+ words = words.slice(1);
272
+ }
273
+ product = words.join(" ");
274
+ }
275
+ }
276
+ }
277
+
278
+ const subject = project || "";
279
+
280
+ // --- stub suppression -------------------------------------------------------
281
+ // With no project AND no repo/product, every remaining ingredient is
282
+ // information-free: a bare command word ("chat", "startwork") or a lone org
283
+ // token ("HQ"). Emitting one of those is actively harmful — it is not merely
284
+ // useless, it OVERWRITES the host's written summary ("HQ core skill cloning")
285
+ // with a word that distinguishes nothing, and because the stub never changes,
286
+ // the wrapper's change-only cadence then goes silent for the rest of the
287
+ // session. Sessions were sitting on 1281 prompts still titled "chat".
288
+ //
289
+ // Printing nothing makes the wrapper exit before it emits or records anything,
290
+ // so the host's own title stands. The moment a project or repo does resolve,
291
+ // the normal path takes the title back.
292
+ if (!project && !product) process.exit(0);
293
+
294
+ const parts = [org, product, subject].filter(Boolean);
295
+ if (!parts.length) parts.push(command);
296
+
297
+ const compose = (ps) => (mode ? mode + " " : "") + ps.join(" · ");
298
+
299
+ const MAX = 56;
300
+ let title = compose(parts);
301
+ if (title.length > MAX && parts.length > 1) title = compose([parts[parts.length - 1]]);
302
+ if (title.length > MAX) title = title.slice(0, MAX - 1).replace(/\s+$/, "") + "…";
303
+
304
+ console.log(title);
305
+ JS
@@ -5971,6 +5971,58 @@ export declare const COMMAND_CATALOG: readonly [{
5971
5971
  }];
5972
5972
  readonly options: readonly [];
5973
5973
  readonly subcommands: readonly [];
5974
+ }, {
5975
+ readonly name: "session-journal";
5976
+ readonly description: "Write and read session journal entries";
5977
+ readonly aliases: readonly [];
5978
+ readonly hidden: false;
5979
+ readonly usage: "[args...]";
5980
+ readonly arguments: readonly [{
5981
+ readonly name: "args";
5982
+ readonly required: false;
5983
+ readonly variadic: true;
5984
+ }];
5985
+ readonly options: readonly [];
5986
+ readonly subcommands: readonly [];
5987
+ }, {
5988
+ readonly name: "session-project";
5989
+ readonly description: "Find and update native session projects";
5990
+ readonly aliases: readonly [];
5991
+ readonly hidden: false;
5992
+ readonly usage: "[args...]";
5993
+ readonly arguments: readonly [{
5994
+ readonly name: "args";
5995
+ readonly required: false;
5996
+ readonly variadic: true;
5997
+ }];
5998
+ readonly options: readonly [];
5999
+ readonly subcommands: readonly [];
6000
+ }, {
6001
+ readonly name: "session-title";
6002
+ readonly description: "Compute the current session title";
6003
+ readonly aliases: readonly [];
6004
+ readonly hidden: false;
6005
+ readonly usage: "[args...]";
6006
+ readonly arguments: readonly [{
6007
+ readonly name: "args";
6008
+ readonly required: false;
6009
+ readonly variadic: true;
6010
+ }];
6011
+ readonly options: readonly [];
6012
+ readonly subcommands: readonly [];
6013
+ }, {
6014
+ readonly name: "session-title-config";
6015
+ readonly description: "Resolve session title settings";
6016
+ readonly aliases: readonly [];
6017
+ readonly hidden: false;
6018
+ readonly usage: "[args...]";
6019
+ readonly arguments: readonly [{
6020
+ readonly name: "args";
6021
+ readonly required: false;
6022
+ readonly variadic: true;
6023
+ }];
6024
+ readonly options: readonly [];
6025
+ readonly subcommands: readonly [];
5974
6026
  }, {
5975
6027
  readonly name: "worktree-gc";
5976
6028
  readonly description: "Safely collect old, preserved HQ worktrees";
@@ -7681,6 +7681,70 @@ export const COMMAND_CATALOG = [
7681
7681
  "options": [],
7682
7682
  "subcommands": []
7683
7683
  },
7684
+ {
7685
+ "name": "session-journal",
7686
+ "description": "Write and read session journal entries",
7687
+ "aliases": [],
7688
+ "hidden": false,
7689
+ "usage": "[args...]",
7690
+ "arguments": [
7691
+ {
7692
+ "name": "args",
7693
+ "required": false,
7694
+ "variadic": true
7695
+ }
7696
+ ],
7697
+ "options": [],
7698
+ "subcommands": []
7699
+ },
7700
+ {
7701
+ "name": "session-project",
7702
+ "description": "Find and update native session projects",
7703
+ "aliases": [],
7704
+ "hidden": false,
7705
+ "usage": "[args...]",
7706
+ "arguments": [
7707
+ {
7708
+ "name": "args",
7709
+ "required": false,
7710
+ "variadic": true
7711
+ }
7712
+ ],
7713
+ "options": [],
7714
+ "subcommands": []
7715
+ },
7716
+ {
7717
+ "name": "session-title",
7718
+ "description": "Compute the current session title",
7719
+ "aliases": [],
7720
+ "hidden": false,
7721
+ "usage": "[args...]",
7722
+ "arguments": [
7723
+ {
7724
+ "name": "args",
7725
+ "required": false,
7726
+ "variadic": true
7727
+ }
7728
+ ],
7729
+ "options": [],
7730
+ "subcommands": []
7731
+ },
7732
+ {
7733
+ "name": "session-title-config",
7734
+ "description": "Resolve session title settings",
7735
+ "aliases": [],
7736
+ "hidden": false,
7737
+ "usage": "[args...]",
7738
+ "arguments": [
7739
+ {
7740
+ "name": "args",
7741
+ "required": false,
7742
+ "variadic": true
7743
+ }
7744
+ ],
7745
+ "options": [],
7746
+ "subcommands": []
7747
+ },
7684
7748
  {
7685
7749
  "name": "worktree-gc",
7686
7750
  "description": "Safely collect old, preserved HQ worktrees",
@@ -318,6 +318,7 @@ function hardPolicyLines(root, company, deps) {
318
318
  let delimiters = 0;
319
319
  let id = "";
320
320
  let enforcement = "";
321
+ let retired = false;
321
322
  let ruleSection = false;
322
323
  let rule = "";
323
324
  for (const line of lines) {
@@ -335,7 +336,7 @@ function hardPolicyLines(root, company, deps) {
335
336
  if (enforceMatch)
336
337
  enforcement = (enforceMatch[1] ?? "").replace(/[\t ]/g, "");
337
338
  if (/^status:[\t ]*retired/.test(line))
338
- enforcement = "";
339
+ retired = true;
339
340
  }
340
341
  if (delimiters >= 2 && line === "## Rule") {
341
342
  ruleSection = true;
@@ -351,7 +352,7 @@ function hardPolicyLines(root, company, deps) {
351
352
  rule = `${rule.slice(0, 157)}...`;
352
353
  }
353
354
  }
354
- if (enforcement !== "hard" || !rule)
355
+ if (enforcement !== "hard" || retired || !rule)
355
356
  continue;
356
357
  const policyId = id || basename.replace(/\.md$/, "");
357
358
  result.push(`- [hard] **${policyId}**: ${rule} Full text: \`companies/${company}/policies/${basename}\`.`);
@@ -0,0 +1,10 @@
1
+ export type SessionHelperRuntimeOverrides = {
2
+ env?: NodeJS.ProcessEnv;
3
+ now?: () => Date;
4
+ };
5
+ export type SessionHelperRuntime = {
6
+ env: NodeJS.ProcessEnv;
7
+ now: () => Date;
8
+ };
9
+ export declare function resolveSessionHelperRuntime(overrides?: SessionHelperRuntimeOverrides): SessionHelperRuntime;
10
+ //# sourceMappingURL=core-session-helper-runtime.d.ts.map
@@ -0,0 +1,7 @@
1
+ export function resolveSessionHelperRuntime(overrides = {}) {
2
+ return {
3
+ env: overrides.env ?? process.env,
4
+ now: overrides.now ?? (() => new Date()),
5
+ };
6
+ }
7
+ //# sourceMappingURL=core-session-helper-runtime.js.map
@@ -0,0 +1,4 @@
1
+ import { type SessionHelperRuntimeOverrides } from "./core-session-helper-runtime.js";
2
+ /** Native implementation of the fail-soft session journal helper. */
3
+ export declare function runSessionJournalFast(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, dependencies?: SessionHelperRuntimeOverrides): number;
4
+ //# sourceMappingURL=core-session-journal-native.d.ts.map