@indigoai-us/hq-cli 5.81.0 → 5.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.83.0]
6
+
7
+ ### Added
8
+
9
+ - Checkpoint Stop-gate eligibility now also accepts outpost machine
10
+ identities whose `custom:delegatedEmail` claim carries an eligible
11
+ domain (default `getindigo.ai`, plus `HQ_CHECKPOINT_GATE_DOMAINS`),
12
+ so outposts provisioned by eligible users enforce the gate; other
13
+ orgs' outposts remain ineligible. Output/verdict stay `1`/`0` with
14
+ no claim values ever printed. (#291)
15
+
16
+ ## [5.82.0]
17
+
18
+ ### Added
19
+
20
+ - `hq core checkpoint` reaches full `/checkpoint`-skill parity: new
21
+ `--insight`, `--commit`, `--initial-commit`, and `--worker*` options
22
+ (payload equivalents included), automatic caller-repo `remote_url`
23
+ capture, `commits_made` derivation from `--initial-commit`, and a
24
+ sibling prompt that executes the complete checkpoint flow — in-place
25
+ thread upgrade/rename, knowledge-repo scan, journal close, legacy
26
+ checkpoint, thread index refresh, document-release best-effort,
27
+ transcript-derived learnings — under explicit allow-list write
28
+ bounds. (#289)
29
+
5
30
  ## [5.81.0]
6
31
 
7
32
  ### Added
@@ -11,7 +11,7 @@ import { Command } from "commander";
11
11
  * Kept in TypeScript rather than in a bundled asset: it is an instruction to
12
12
  * a locally-installed agent, not a scaffold script that should be packaged.
13
13
  */
14
- export declare const SIBLING_PROMPT_TEMPLATE = "You are the HQ checkpoint sibling \u2014 a background maintenance agent for this\nHQ install. Your parent session's state is in <payloadPath>. Work\nquietly and do not ask questions; if something is ambiguous, record it in the\nreport instead of guessing.\n\n1. Read the payload. If it lists a transcript path that exists, you may read\n its tail for context (last ~200 lines); never quote secrets from it.\n2. Verify the checkpoint thread file named in the payload exists and is valid\n JSON; repair or enrich it if needed (keep its shape).\n3. For each entry in \"learnings\": if it is a reusable rule, distill it into a\n policy file under personal/policies/ (or companies/<company>/policies/ when\n the payload names a company and the lesson is company-specific), following\n core/knowledge/public/hq-core/policies-spec.md. Skip duplicates \u2014 search\n existing policies first.\n4. For durable facts (not rules), update knowledge under personal/knowledge/\n or companies/<company>/knowledge/.\n5. Hook or automation improvements go ONLY under personal/hooks/ as proposals.\n You must never write into .claude/, core/, .agents/, .codex/, or repos/.\n6. Write <runDir>/report.md \u2014 full prose: what you read, what you changed\n (paths), what you skipped and why.\n7. If workspace/checkpoints/sibling/pending.jsonl is non-empty when you\n finish, process those payloads the same way, then truncate the file.\n";
14
+ export declare const SIBLING_PROMPT_TEMPLATE = "You are the HQ checkpoint sibling \u2014 a background maintenance agent for this\nHQ install. Your parent session's state is in <payloadPath>. Work\nquietly and do not ask questions; if something is ambiguous, record it in the\nreport instead of guessing.\n\n1. Read the payload. If it lists a transcript path that exists, read its tail (~400 lines)\n both for session context and to extract additional reusable learnings/insights\n the parent did not pass explicitly. Never quote secrets\n or tokens from the transcript. If .claude/skills/checkpoint/SKILL.md exists\n under this HQ root, read it and follow it wherever it goes beyond these instructions;\n the write bounds below always win over the skill text.\n2. Upgrade the thread file named in the payload IN PLACE: verify/repair its\n JSON; fill git.remote_url, git.initial_commit, git.commits_made, and\n git.knowledge_repos by scanning core/knowledge/public/*,\n core/knowledge/private/*, personal/knowledge/*, and companies/*/knowledge\n for symlinks or directories containing .git, recording dirty repositories\n as {\"<name>\": {\"commit\": \"<short>\", \"dirty\": true}}. Fill worker,\n next_steps, and insights; set type to \"checkpoint\"; then rename the file to\n drop -auto- from its filename. Use the renamed path in every reference you\n write afterwards.\n3. For every explicit or transcript-derived learning that is a reusable rule,\n distill a non-duplicate policy file under personal/policies/ or, only when\n the payload names a company and the rule is company-specific,\n companies/<company>/policies/, following\n core/knowledge/public/hq-core/policies-spec.md. Store up to two explicit or\n transcript-derived insights per core/knowledge/public/hq-core/insights-spec.md\n when present, otherwise workspace/insights/. Durable facts (not rules) may\n go under personal/knowledge/ or companies/<company>/knowledge/ only.\n4. Close an active session journal fail-soft with\n bash .claude/skills/_shared/journal.sh close \"<project_dir>\" \"<one-line synthesis>\".\n Write a legacy checkpoint JSON under workspace/checkpoints/<id>.json with\n id, created_at, summary, files, and next_steps for backward compatibility.\n5. Update workspace/threads/recent.md and regenerate\n workspace/threads/INDEX.md. For each company whose knowledge path appears\n in files_touched, regenerate companies/<company>/knowledge/INDEX.md under\n core/knowledge/public/hq-core/index-md-spec.md. Mechanical index generation\n is allowed for those companies, but knowledge/policy content writes remain\n restricted to the payload's named company.\n6. Run .claude/skills/document-release/SKILL.md best-effort when it exists;\n skip silently on any failure. Hook or automation improvements go ONLY under\n personal/hooks/ as proposals.\n7. WRITE BOUNDS: you may write only under personal/, workspace/, and companies/<company>/ as constrained above. You must NEVER write into .claude/, core/, .agents/, .codex/, repos/, or anywhere outside the HQ root.\n8. Write <runDir>/report.md \u2014 full prose: what you read, what you changed\n (paths), and what you skipped and why. If\n workspace/checkpoints/sibling/pending.jsonl is non-empty when you finish,\n process those payloads with this same flow, then truncate the file.\n";
15
15
  export declare function renderSiblingPrompt(runDir: string, payloadPath: string): string;
16
16
  /** Attach the native checkpoint command to the hidden `hq core` group. */
17
17
  export declare function registerCoreCheckpointCommand(core: Command): void;
@@ -36,23 +36,47 @@ HQ install. Your parent session's state is in <payloadPath>. Work
36
36
  quietly and do not ask questions; if something is ambiguous, record it in the
37
37
  report instead of guessing.
38
38
 
39
- 1. Read the payload. If it lists a transcript path that exists, you may read
40
- its tail for context (last ~200 lines); never quote secrets from it.
41
- 2. Verify the checkpoint thread file named in the payload exists and is valid
42
- JSON; repair or enrich it if needed (keep its shape).
43
- 3. For each entry in "learnings": if it is a reusable rule, distill it into a
44
- policy file under personal/policies/ (or companies/<company>/policies/ when
45
- the payload names a company and the lesson is company-specific), following
46
- core/knowledge/public/hq-core/policies-spec.md. Skip duplicates — search
47
- existing policies first.
48
- 4. For durable facts (not rules), update knowledge under personal/knowledge/
49
- or companies/<company>/knowledge/.
50
- 5. Hook or automation improvements go ONLY under personal/hooks/ as proposals.
51
- You must never write into .claude/, core/, .agents/, .codex/, or repos/.
52
- 6. Write <runDir>/report.md full prose: what you read, what you changed
53
- (paths), what you skipped and why.
54
- 7. If workspace/checkpoints/sibling/pending.jsonl is non-empty when you
55
- finish, process those payloads the same way, then truncate the file.
39
+ 1. Read the payload. If it lists a transcript path that exists, read its tail (~400 lines)
40
+ both for session context and to extract additional reusable learnings/insights
41
+ the parent did not pass explicitly. Never quote secrets
42
+ or tokens from the transcript. If .claude/skills/checkpoint/SKILL.md exists
43
+ under this HQ root, read it and follow it wherever it goes beyond these instructions;
44
+ the write bounds below always win over the skill text.
45
+ 2. Upgrade the thread file named in the payload IN PLACE: verify/repair its
46
+ JSON; fill git.remote_url, git.initial_commit, git.commits_made, and
47
+ git.knowledge_repos by scanning core/knowledge/public/*,
48
+ core/knowledge/private/*, personal/knowledge/*, and companies/*/knowledge
49
+ for symlinks or directories containing .git, recording dirty repositories
50
+ as {"<name>": {"commit": "<short>", "dirty": true}}. Fill worker,
51
+ next_steps, and insights; set type to "checkpoint"; then rename the file to
52
+ drop -auto- from its filename. Use the renamed path in every reference you
53
+ write afterwards.
54
+ 3. For every explicit or transcript-derived learning that is a reusable rule,
55
+ distill a non-duplicate policy file under personal/policies/ or, only when
56
+ the payload names a company and the rule is company-specific,
57
+ companies/<company>/policies/, following
58
+ core/knowledge/public/hq-core/policies-spec.md. Store up to two explicit or
59
+ transcript-derived insights per core/knowledge/public/hq-core/insights-spec.md
60
+ when present, otherwise workspace/insights/. Durable facts (not rules) may
61
+ go under personal/knowledge/ or companies/<company>/knowledge/ only.
62
+ 4. Close an active session journal fail-soft with
63
+ bash .claude/skills/_shared/journal.sh close "<project_dir>" "<one-line synthesis>".
64
+ Write a legacy checkpoint JSON under workspace/checkpoints/<id>.json with
65
+ id, created_at, summary, files, and next_steps for backward compatibility.
66
+ 5. Update workspace/threads/recent.md and regenerate
67
+ workspace/threads/INDEX.md. For each company whose knowledge path appears
68
+ in files_touched, regenerate companies/<company>/knowledge/INDEX.md under
69
+ core/knowledge/public/hq-core/index-md-spec.md. Mechanical index generation
70
+ is allowed for those companies, but knowledge/policy content writes remain
71
+ restricted to the payload's named company.
72
+ 6. Run .claude/skills/document-release/SKILL.md best-effort when it exists;
73
+ skip silently on any failure. Hook or automation improvements go ONLY under
74
+ personal/hooks/ as proposals.
75
+ 7. WRITE BOUNDS: you may write only under personal/, workspace/, and companies/<company>/ as constrained above. You must NEVER write into .claude/, core/, .agents/, .codex/, repos/, or anywhere outside the HQ root.
76
+ 8. Write <runDir>/report.md — full prose: what you read, what you changed
77
+ (paths), and what you skipped and why. If
78
+ workspace/checkpoints/sibling/pending.jsonl is non-empty when you finish,
79
+ process those payloads with this same flow, then truncate the file.
56
80
  `;
57
81
  export function renderSiblingPrompt(runDir, payloadPath) {
58
82
  return SIBLING_PROMPT_TEMPLATE
@@ -105,6 +129,27 @@ function asStringList(value, field) {
105
129
  }
106
130
  return value;
107
131
  }
132
+ function asWorker(value) {
133
+ if (value === undefined || value === null)
134
+ return {};
135
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
136
+ usage("checkpoint: payload worker must be an object");
137
+ }
138
+ const worker = value;
139
+ return {
140
+ id: asOptionalString(worker.id, "worker.id"),
141
+ skill: asOptionalString(worker.skill, "worker.skill"),
142
+ state: asOptionalString(worker.state, "worker.state"),
143
+ };
144
+ }
145
+ function resolveWorker(options, command, payload) {
146
+ const payloadWorker = asWorker(payload.worker);
147
+ const id = wasPassed(command, "worker") ? options.worker : payloadWorker.id;
148
+ const skill = wasPassed(command, "workerSkill") ? options.workerSkill : payloadWorker.skill;
149
+ const suppliedState = wasPassed(command, "workerState") ? options.workerState : payloadWorker.state;
150
+ const state = suppliedState ?? (id ? "completed" : null);
151
+ return { id: id ?? null, skill: skill ?? null, state };
152
+ }
108
153
  function wasPassed(command, option) {
109
154
  return command.getOptionValueSource(option) === "cli";
110
155
  }
@@ -129,6 +174,9 @@ function mergeInput(options, command, payload) {
129
174
  const learnings = wasPassed(command, "learning")
130
175
  ? options.learning
131
176
  : asStringList(payload.learnings, "learnings");
177
+ const insights = wasPassed(command, "insight")
178
+ ? options.insight
179
+ : asStringList(payload.insights, "insights");
132
180
  const decisions = wasPassed(command, "decision")
133
181
  ? options.decision
134
182
  : asStringList(payload.decisions, "decisions");
@@ -138,6 +186,12 @@ function mergeInput(options, command, payload) {
138
186
  const tags = wasPassed(command, "tag")
139
187
  ? options.tag
140
188
  : asStringList(payload.tags, "tags");
189
+ const commits = wasPassed(command, "commit")
190
+ ? options.commit
191
+ : asStringList(payload.commits, "commits");
192
+ const initialCommit = wasPassed(command, "initialCommit")
193
+ ? options.initialCommit
194
+ : asOptionalString(payload.initial_commit, "initial_commit");
141
195
  const trigger = (wasPassed(command, "trigger")
142
196
  ? options.trigger
143
197
  : asOptionalString(payload.trigger, "trigger")) ?? DEFAULT_TRIGGER;
@@ -152,9 +206,13 @@ function mergeInput(options, command, payload) {
152
206
  summary,
153
207
  files,
154
208
  learnings,
209
+ insights,
155
210
  decisions,
156
211
  nextSteps,
157
212
  tags,
213
+ commits,
214
+ initialCommit,
215
+ worker: resolveWorker(options, command, payload),
158
216
  trigger,
159
217
  company,
160
218
  sessionId,
@@ -190,19 +248,49 @@ function readGitState(repoDir) {
190
248
  branch: read(["rev-parse", "--abbrev-ref", "HEAD"]),
191
249
  current_commit: read(["rev-parse", "--short", "HEAD"]),
192
250
  dirty: read(["status", "--porcelain"]) !== "",
251
+ remote_url: (() => {
252
+ try {
253
+ return read(["remote", "get-url", "origin"]);
254
+ }
255
+ catch {
256
+ return null;
257
+ }
258
+ })(),
193
259
  };
194
260
  }
195
261
  catch {
196
262
  return null;
197
263
  }
198
264
  }
199
- function gitStateFor(cwd, liveRoot) {
200
- return (readGitState(cwd) ??
265
+ function commitsSince(repoDir, initialCommit) {
266
+ try {
267
+ const output = execFileSync("git", ["-C", repoDir, "log", "--no-decorate", "--pretty=format:%h: %s", `${initialCommit}..HEAD`], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
268
+ return output ? output.split("\n") : [];
269
+ }
270
+ catch {
271
+ return [];
272
+ }
273
+ }
274
+ function gitStateFor(cwd, liveRoot, initialCommit, explicitCommits) {
275
+ const callerState = readGitState(cwd);
276
+ const state = callerState ??
201
277
  readGitState(liveRoot) ?? {
202
278
  branch: "unknown",
203
279
  current_commit: "unknown",
204
280
  dirty: false,
205
- });
281
+ remote_url: null,
282
+ };
283
+ return {
284
+ ...state,
285
+ // Remote URLs and derived history describe the caller's repository only.
286
+ remote_url: callerState?.remote_url ?? null,
287
+ initial_commit: initialCommit ?? null,
288
+ commits_made: explicitCommits.length > 0
289
+ ? explicitCommits
290
+ : initialCommit && callerState
291
+ ? commitsSince(cwd, initialCommit)
292
+ : [],
293
+ };
206
294
  }
207
295
  function writeStamps(liveRoot, sessionId) {
208
296
  const stateDir = path.join(liveRoot, "workspace", "orchestrator", "hook-state");
@@ -230,17 +318,21 @@ function gateEligibility() {
230
318
  const tokens = JSON.parse(fs.readFileSync(tokenPath, "utf8"));
231
319
  if (typeof tokens.idToken !== "string")
232
320
  return false;
233
- const email = peekIdToken(tokens.idToken).email;
234
- if (typeof email !== "string")
235
- return false;
236
- const domain = email.split("@").at(-1)?.toLowerCase();
237
- if (!domain)
238
- return false;
321
+ const claims = peekIdToken(tokens.idToken);
322
+ const candidateEmails = [claims.email, claims["custom:delegatedEmail"]];
239
323
  const configured = (process.env.HQ_CHECKPOINT_GATE_DOMAINS ?? "")
240
324
  .split(",")
241
325
  .map((candidate) => candidate.trim().toLowerCase())
242
326
  .filter(Boolean);
243
- return domain === "getindigo.ai" || configured.includes(domain);
327
+ const hasEligibleDomain = (candidate) => {
328
+ if (typeof candidate !== "string")
329
+ return false;
330
+ const domain = candidate.split("@").at(-1)?.toLowerCase();
331
+ if (!domain)
332
+ return false;
333
+ return domain === "getindigo.ai" || configured.includes(domain);
334
+ };
335
+ return candidateEmails.some(hasEligibleDomain);
244
336
  }
245
337
  catch {
246
338
  return false;
@@ -287,6 +379,7 @@ function siblingPayload(input, threadPath) {
287
379
  summary: input.summary ?? "",
288
380
  files: input.files,
289
381
  learnings: input.learnings,
382
+ insights: input.insights,
290
383
  decisions: input.decisions,
291
384
  next_steps: input.nextSteps,
292
385
  tags: input.tags,
@@ -294,6 +387,7 @@ function siblingPayload(input, threadPath) {
294
387
  company: input.company ?? null,
295
388
  session_id: input.sessionId ?? null,
296
389
  transcript: input.transcript ?? null,
390
+ worker: input.worker,
297
391
  thread_path: threadPath,
298
392
  pending_payloads: [],
299
393
  };
@@ -410,9 +504,15 @@ function hasGateProbeConflict(options, command) {
410
504
  "summary",
411
505
  "file",
412
506
  "learning",
507
+ "insight",
413
508
  "decision",
414
509
  "next",
415
510
  "tag",
511
+ "commit",
512
+ "initialCommit",
513
+ "worker",
514
+ "workerSkill",
515
+ "workerState",
416
516
  "trigger",
417
517
  "company",
418
518
  "sessionId",
@@ -456,6 +556,7 @@ function runCheckpoint(options, command, group) {
456
556
  const threadId = `T-${formatTimestamp(now)}-auto-${summarySlug(input.summary)}`;
457
557
  const threadPath = path.join(liveRoot, "workspace", "threads", `${threadId}.json`);
458
558
  const relativeThreadPath = path.relative(liveRoot, threadPath);
559
+ const git = gitStateFor(process.cwd(), liveRoot, input.initialCommit, input.commits);
459
560
  const stampPaths = [
460
561
  path.join(liveRoot, "workspace", "orchestrator", "hook-state", "checkpoint-cli-last"),
461
562
  path.join(liveRoot, "workspace", "orchestrator", "hook-state", `checkpoint-cli-last-${input.sessionId ? input.sessionId.replace(/[^A-Za-z0-9._-]/g, "_") || "unknown" : "unknown"}`),
@@ -465,6 +566,11 @@ function runCheckpoint(options, command, group) {
465
566
  live_root: liveRoot,
466
567
  idle: false,
467
568
  thread: relativeThreadPath,
569
+ checkpoint: {
570
+ git,
571
+ insights: input.insights,
572
+ worker: input.worker,
573
+ },
468
574
  stamps: stampPaths.map((stampPath) => path.relative(liveRoot, stampPath)),
469
575
  sibling: options.agent === false ? null : { backend },
470
576
  });
@@ -478,12 +584,14 @@ function runCheckpoint(options, command, group) {
478
584
  updated_at: now.toISOString(),
479
585
  workspace_root: liveRoot,
480
586
  cwd: process.cwd(),
481
- git: gitStateFor(process.cwd(), liveRoot),
587
+ git,
482
588
  conversation_summary: input.summary,
483
589
  files_touched: input.files,
484
590
  next_steps: input.nextSteps,
485
591
  learnings: input.learnings,
592
+ insights: input.insights,
486
593
  decisions: input.decisions,
594
+ worker: input.worker,
487
595
  session_id: input.sessionId ?? null,
488
596
  metadata: {
489
597
  title: titleFor(input.summary),
@@ -516,9 +624,15 @@ export function registerCoreCheckpointCommand(core) {
516
624
  .option("--summary <text>", "1–2 sentence outcome summary")
517
625
  .option("--file <path>", "file touched this turn", collect, [])
518
626
  .option("--learning <text>", "reusable lesson", collect, [])
627
+ .option("--insight <text>", "session insight", collect, [])
519
628
  .option("--decision <text>", "decision made", collect, [])
520
629
  .option("--next <text>", "remaining next step", collect, [])
521
630
  .option("--tag <tag>", "checkpoint tag", collect, [])
631
+ .option("--commit <entry>", "session commit (<hash>: <message>)", collect, [])
632
+ .option("--initial-commit <sha>", "commit at the start of this session")
633
+ .option("--worker <id>", "active worker identifier")
634
+ .option("--worker-skill <name>", "active worker skill")
635
+ .option("--worker-state <state>", "active worker state")
522
636
  .option("--trigger <name>", "checkpoint trigger", DEFAULT_TRIGGER)
523
637
  .option("--company <slug>", "active company scope")
524
638
  .option("--session-id <id>", "caller session id (default: $CLAUDE_CODE_SESSION_ID when set)")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.81.0",
3
+ "version": "5.83.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {