@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 +25 -0
- package/dist/commands/core-checkpoint.d.ts +1 -1
- package/dist/commands/core-checkpoint.js +142 -28
- package/package.json +1 -1
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,
|
|
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,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
or
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
200
|
-
|
|
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
|
|
234
|
-
|
|
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
|
-
|
|
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
|
|
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)")
|