@indigoai-us/hq-cli 5.89.2 → 5.91.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 +26 -0
- package/dist/commands/core-checkpoint.d.ts +1 -1
- package/dist/commands/core-checkpoint.js +27 -8
- package/dist/commands/core.d.ts +15 -0
- package/dist/commands/core.js +42 -36
- package/dist/lib/core-utils/common.d.ts +14 -0
- package/dist/lib/core-utils/common.js +26 -0
- package/dist/lib/core-utils/detect-stale-core-policy-mirror.d.ts +7 -0
- package/dist/lib/core-utils/detect-stale-core-policy-mirror.js +101 -0
- package/dist/lib/core-utils/hq-status-summary.d.ts +9 -0
- package/dist/lib/core-utils/hq-status-summary.js +96 -0
- package/dist/lib/core-utils/ontology-readme-drift.d.ts +7 -0
- package/dist/lib/core-utils/ontology-readme-drift.js +135 -0
- package/dist/lib/core-utils/resize-screenshot.d.ts +10 -0
- package/dist/lib/core-utils/resize-screenshot.js +50 -0
- package/dist/lib/core-utils/token-usage-report.d.ts +8 -0
- package/dist/lib/core-utils/token-usage-report.js +196 -0
- package/dist/lib/core-utils/worktree.d.ts +13 -0
- package/dist/lib/core-utils/worktree.js +113 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.91.0]
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- The `hq core checkpoint` maintenance sibling now curates the policy and
|
|
10
|
+
knowledge stores instead of only appending to them. It searches the existing
|
|
11
|
+
policies first and then amends, supersedes, merges or creates, and corrects a
|
|
12
|
+
stale durable fact in place rather than shadowing it with a contradictory
|
|
13
|
+
copy. Deletion is bounded: only as part of a merge or supersede, only inside
|
|
14
|
+
the policy directories the sibling could already write, never a HARD-marked
|
|
15
|
+
policy (those are flagged in the report for a human), and never a file it has
|
|
16
|
+
not read. Every amendment, supersession, merge and deletion must be named in
|
|
17
|
+
the run report with its reason. (#320)
|
|
18
|
+
|
|
19
|
+
## [5.90.0]
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Six bundled `hq core` utility scripts (detect-stale-core-policy-mirror,
|
|
24
|
+
hq-status-summary, ontology-readme-drift, resize-screenshot,
|
|
25
|
+
token-usage-report, worktree) now run as native, tested TypeScript instead of
|
|
26
|
+
bundled shell assets, proven equivalent by a shell-vs-native differential
|
|
27
|
+
suite. Command surfaces are unchanged, the scaffold files still ship, and
|
|
28
|
+
the commands no longer depend on bundled scaffold assets being present.
|
|
29
|
+
(#321)
|
|
30
|
+
|
|
5
31
|
## [5.89.2]
|
|
6
32
|
|
|
7
33
|
### Fixed
|
|
@@ -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, 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
|
|
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 FIRST search the existing policies for one the learning refines, contradicts\n or duplicates. Then take exactly one of these actions and name it in the\n report \u2014 the policy set is curated, not append-only:\n - AMEND an existing policy in place when the learning sharpens it, narrows\n its scope, or adds a case, and the rule as written is still correct.\n - SUPERSEDE it when the learning contradicts it: rewrite the rule to what is\n now true and record inside the file what changed and why.\n - MERGE near-duplicates into the single best-named file, then delete the\n files you merged away.\n - CREATE a new policy only when no existing policy covers the rule.\n Follow core/knowledge/public/hq-core/policies-spec.md. Write under\n personal/policies/ or, only when the payload names a company and the rule is\n company-specific, companies/<company>/policies/.\n DELETION BOUNDS: delete a policy only as the MERGE or SUPERSEDE step above,\n only inside those two directories, and never one whose body marks it HARD \u2014\n if a hard policy now looks wrong, leave it untouched and flag it in the\n report for a human to decide. Never delete a file you have not read.\n Apply the same curation to durable facts (not rules) under\n personal/knowledge/ or companies/<company>/knowledge/: correct a stale fact\n in place rather than appending a second, contradictory copy of it.\n Store up to two explicit or transcript-derived insights per\n core/knowledge/public/hq-core/insights-spec.md when present, otherwise\n workspace/insights/.\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. List every policy or knowledge file\n you amended, superseded, merged or deleted with the reason, so a human can\n audit and reverse it; a deletion you do not name in the report is a defect.\n Then drain the queue: while\n workspace/checkpoints/sibling/pending.jsonl exists and is non-empty, claim\n it atomically by renaming it aside \u2014\n mv workspace/checkpoints/sibling/pending.jsonl <runDir>/pending-claimed-N.jsonl\n (N counting up from 2) \u2014 and process the claimed payloads with this same\n flow. Repeat until a claim finds nothing left, then update the report.\n NEVER read the queue and truncate it in place: a payload appended between\n your read and the truncate is lost, and its checkpoint is never enriched.\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;
|
|
@@ -56,13 +56,29 @@ report instead of guessing.
|
|
|
56
56
|
drop -auto- from its filename. Use the renamed path in every reference you
|
|
57
57
|
write afterwards.
|
|
58
58
|
3. For every explicit or transcript-derived learning that is a reusable rule,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
when
|
|
65
|
-
|
|
59
|
+
FIRST search the existing policies for one the learning refines, contradicts
|
|
60
|
+
or duplicates. Then take exactly one of these actions and name it in the
|
|
61
|
+
report — the policy set is curated, not append-only:
|
|
62
|
+
- AMEND an existing policy in place when the learning sharpens it, narrows
|
|
63
|
+
its scope, or adds a case, and the rule as written is still correct.
|
|
64
|
+
- SUPERSEDE it when the learning contradicts it: rewrite the rule to what is
|
|
65
|
+
now true and record inside the file what changed and why.
|
|
66
|
+
- MERGE near-duplicates into the single best-named file, then delete the
|
|
67
|
+
files you merged away.
|
|
68
|
+
- CREATE a new policy only when no existing policy covers the rule.
|
|
69
|
+
Follow core/knowledge/public/hq-core/policies-spec.md. Write under
|
|
70
|
+
personal/policies/ or, only when the payload names a company and the rule is
|
|
71
|
+
company-specific, companies/<company>/policies/.
|
|
72
|
+
DELETION BOUNDS: delete a policy only as the MERGE or SUPERSEDE step above,
|
|
73
|
+
only inside those two directories, and never one whose body marks it HARD —
|
|
74
|
+
if a hard policy now looks wrong, leave it untouched and flag it in the
|
|
75
|
+
report for a human to decide. Never delete a file you have not read.
|
|
76
|
+
Apply the same curation to durable facts (not rules) under
|
|
77
|
+
personal/knowledge/ or companies/<company>/knowledge/: correct a stale fact
|
|
78
|
+
in place rather than appending a second, contradictory copy of it.
|
|
79
|
+
Store up to two explicit or transcript-derived insights per
|
|
80
|
+
core/knowledge/public/hq-core/insights-spec.md when present, otherwise
|
|
81
|
+
workspace/insights/.
|
|
66
82
|
4. Close an active session journal fail-soft with
|
|
67
83
|
bash .claude/skills/_shared/journal.sh close "<project_dir>" "<one-line synthesis>".
|
|
68
84
|
Write a legacy checkpoint JSON under workspace/checkpoints/<id>.json with
|
|
@@ -78,7 +94,10 @@ report instead of guessing.
|
|
|
78
94
|
personal/hooks/ as proposals.
|
|
79
95
|
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.
|
|
80
96
|
8. Write <runDir>/report.md — full prose: what you read, what you changed
|
|
81
|
-
(paths), and what you skipped and why.
|
|
97
|
+
(paths), and what you skipped and why. List every policy or knowledge file
|
|
98
|
+
you amended, superseded, merged or deleted with the reason, so a human can
|
|
99
|
+
audit and reverse it; a deletion you do not name in the report is a defect.
|
|
100
|
+
Then drain the queue: while
|
|
82
101
|
workspace/checkpoints/sibling/pending.jsonl exists and is non-empty, claim
|
|
83
102
|
it atomically by renaming it aside —
|
|
84
103
|
mv workspace/checkpoints/sibling/pending.jsonl <runDir>/pending-claimed-N.jsonl
|
package/dist/commands/core.d.ts
CHANGED
|
@@ -85,6 +85,21 @@ export type WorkerSubcommand = ScaffoldAsset & {
|
|
|
85
85
|
export type NativeScaffoldCommand = ScaffoldAsset & {
|
|
86
86
|
name: string;
|
|
87
87
|
};
|
|
88
|
+
type NativeUtilityCommand = {
|
|
89
|
+
name: string;
|
|
90
|
+
root: ScaffoldRoot;
|
|
91
|
+
summary: string;
|
|
92
|
+
run: (args: string[], context: {
|
|
93
|
+
cwd: string;
|
|
94
|
+
hqRoot?: string;
|
|
95
|
+
}) => number;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* These commands are now native implementations. Their retained shell assets
|
|
99
|
+
* are deliberately claimed in SCAFFOLD_ONLY_ASSETS: loose HQ trees still ship
|
|
100
|
+
* them for legacy callers, but `hq core` never dispatches through them again.
|
|
101
|
+
*/
|
|
102
|
+
export declare const NATIVE_UTILITY_COMMANDS: NativeUtilityCommand[];
|
|
88
103
|
export declare const NATIVE_SCAFFOLD_COMMANDS: NativeScaffoldCommand[];
|
|
89
104
|
/**
|
|
90
105
|
* The `hq core worker <name>` subgroup — worker-scoped skill maintenance.
|
package/dist/commands/core.js
CHANGED
|
@@ -35,6 +35,25 @@ import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
|
35
35
|
import { renderIndexTarget } from "../lib/index-render/index.js";
|
|
36
36
|
import { generateWorkersRegistry } from "../lib/workers-registry/index.js";
|
|
37
37
|
import { scanPackages } from "../lib/scan-packages/index.js";
|
|
38
|
+
import { detectStaleCorePolicyMirror } from "../lib/core-utils/detect-stale-core-policy-mirror.js";
|
|
39
|
+
import { summarizeHqStatus } from "../lib/core-utils/hq-status-summary.js";
|
|
40
|
+
import { ontologyReadmeDrift } from "../lib/core-utils/ontology-readme-drift.js";
|
|
41
|
+
import { resizeScreenshot } from "../lib/core-utils/resize-screenshot.js";
|
|
42
|
+
import { tokenUsageReport } from "../lib/core-utils/token-usage-report.js";
|
|
43
|
+
import { createWorktree } from "../lib/core-utils/worktree.js";
|
|
44
|
+
/**
|
|
45
|
+
* These commands are now native implementations. Their retained shell assets
|
|
46
|
+
* are deliberately claimed in SCAFFOLD_ONLY_ASSETS: loose HQ trees still ship
|
|
47
|
+
* them for legacy callers, but `hq core` never dispatches through them again.
|
|
48
|
+
*/
|
|
49
|
+
export const NATIVE_UTILITY_COMMANDS = [
|
|
50
|
+
{ name: "detect-stale-core-policy-mirror", root: "live", summary: "Find (and prune) stale personal-policy mirrors in core", run: (args, context) => detectStaleCorePolicyMirror(args, { root: context.hqRoot }) },
|
|
51
|
+
{ name: "hq-status-summary", root: "cwd", summary: "Summarize working-tree status across the HQ tree", run: (args, context) => summarizeHqStatus(args, { cwd: context.cwd, root: context.hqRoot }) },
|
|
52
|
+
{ name: "ontology-readme-drift", root: "live", summary: "Check a company's ontology README for drift", run: (args, context) => ontologyReadmeDrift(args, { root: context.hqRoot }) },
|
|
53
|
+
{ name: "resize-screenshot", root: "cwd", summary: "Downscale a screenshot for context efficiency", run: (args) => resizeScreenshot(args) },
|
|
54
|
+
{ name: "token-usage-report", root: "cwd", summary: "Report session token usage", run: (args) => tokenUsageReport(args) },
|
|
55
|
+
{ name: "worktree", root: "live", summary: "Create a git worktree under workspace/worktrees/", run: (args, context) => createWorktree(args, { cwd: context.cwd, hqRoot: context.hqRoot }) },
|
|
56
|
+
];
|
|
38
57
|
export const NATIVE_SCAFFOLD_COMMANDS = [
|
|
39
58
|
{
|
|
40
59
|
name: "generate-workers-registry",
|
|
@@ -158,12 +177,6 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
158
177
|
root: "cwd",
|
|
159
178
|
summary: "One-shot: mirror threads into company workspaces",
|
|
160
179
|
},
|
|
161
|
-
{
|
|
162
|
-
name: "detect-stale-core-policy-mirror",
|
|
163
|
-
asset: "core/scripts/detect-stale-core-policy-mirror.sh",
|
|
164
|
-
root: "live",
|
|
165
|
-
summary: "Find (and prune) stale personal-policy mirrors in core",
|
|
166
|
-
},
|
|
167
180
|
{
|
|
168
181
|
name: "qmd-reindex-after-sync",
|
|
169
182
|
asset: "core/scripts/qmd-reindex-after-sync.sh",
|
|
@@ -177,36 +190,6 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
177
190
|
root: "live",
|
|
178
191
|
summary: "Install/inspect the Codex skill bridge",
|
|
179
192
|
},
|
|
180
|
-
{
|
|
181
|
-
name: "hq-status-summary",
|
|
182
|
-
asset: "core/scripts/hq-status-summary.sh",
|
|
183
|
-
root: "cwd",
|
|
184
|
-
summary: "Summarize working-tree status across the HQ tree",
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
name: "ontology-readme-drift",
|
|
188
|
-
asset: "core/scripts/ontology-readme-drift.sh",
|
|
189
|
-
root: "live",
|
|
190
|
-
summary: "Check a company's ontology README for drift",
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
name: "resize-screenshot",
|
|
194
|
-
asset: "core/scripts/resize-screenshot.sh",
|
|
195
|
-
root: "cwd",
|
|
196
|
-
summary: "Downscale a screenshot for context efficiency",
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
name: "token-usage-report",
|
|
200
|
-
asset: "core/scripts/token-usage-report.sh",
|
|
201
|
-
root: "cwd",
|
|
202
|
-
summary: "Report session token usage",
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
name: "worktree",
|
|
206
|
-
asset: "core/scripts/worktree.sh",
|
|
207
|
-
root: "live",
|
|
208
|
-
summary: "Create a git worktree under workspace/worktrees/",
|
|
209
|
-
},
|
|
210
193
|
];
|
|
211
194
|
/**
|
|
212
195
|
* Bundled solely for HQ-root scaffolds, which still need these as loose files.
|
|
@@ -214,6 +197,12 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
214
197
|
* direct `hq core` command.
|
|
215
198
|
*/
|
|
216
199
|
export const SCAFFOLD_ONLY_ASSETS = [
|
|
200
|
+
{ asset: "core/scripts/detect-stale-core-policy-mirror.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
201
|
+
{ asset: "core/scripts/hq-status-summary.sh", root: "cwd", summary: "Scaffold-only native utility companion" },
|
|
202
|
+
{ asset: "core/scripts/ontology-readme-drift.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
203
|
+
{ asset: "core/scripts/resize-screenshot.sh", root: "cwd", summary: "Scaffold-only native utility companion" },
|
|
204
|
+
{ asset: "core/scripts/token-usage-report.sh", root: "cwd", summary: "Scaffold-only native utility companion" },
|
|
205
|
+
{ asset: "core/scripts/worktree.sh", root: "live", summary: "Scaffold-only native utility companion" },
|
|
217
206
|
{
|
|
218
207
|
asset: "core/scripts/rebuild-all-indexes.sh",
|
|
219
208
|
root: "cwd",
|
|
@@ -348,6 +337,23 @@ export function registerCoreCommands(program) {
|
|
|
348
337
|
process.exit(result.status);
|
|
349
338
|
});
|
|
350
339
|
}
|
|
340
|
+
for (const entry of NATIVE_UTILITY_COMMANDS) {
|
|
341
|
+
core
|
|
342
|
+
.command(entry.name)
|
|
343
|
+
.description(entry.summary)
|
|
344
|
+
.allowUnknownOption()
|
|
345
|
+
.allowExcessArguments()
|
|
346
|
+
.helpOption(false)
|
|
347
|
+
.argument("[args...]", "arguments accepted for shell ABI compatibility")
|
|
348
|
+
.action((args = [], _opts, cmd) => {
|
|
349
|
+
const scope = core.opts();
|
|
350
|
+
const operands = cmd.args.length > 0 ? cmd.args : args;
|
|
351
|
+
const { cwd, hqRoot } = resolveEntryRoot(entry, scope);
|
|
352
|
+
const code = entry.run(operands, { cwd, hqRoot });
|
|
353
|
+
if (code !== 0)
|
|
354
|
+
process.exit(code);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
351
357
|
// `hq core worker <name>` — a nested subgroup for worker-scoped skill
|
|
352
358
|
// maintenance. Nested (rather than flat `hq core worker-<name>`) so the two
|
|
353
359
|
// related operations read as one family.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type Write = (text: string) => void;
|
|
2
|
+
export interface UtilityIo {
|
|
3
|
+
stdout?: Write;
|
|
4
|
+
stderr?: Write;
|
|
5
|
+
}
|
|
6
|
+
export declare function ioFor(io?: UtilityIo): Required<UtilityIo>;
|
|
7
|
+
export declare function line(write: Write, value: string): void;
|
|
8
|
+
export declare function command(command: string, args: string[], cwd?: string): {
|
|
9
|
+
status: number;
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function exists(pathname: string): boolean;
|
|
14
|
+
//# sourceMappingURL=common.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
export function ioFor(io = {}) {
|
|
4
|
+
return {
|
|
5
|
+
stdout: io.stdout ?? ((text) => process.stdout.write(text)),
|
|
6
|
+
stderr: io.stderr ?? ((text) => process.stderr.write(text)),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function line(write, value) {
|
|
10
|
+
write(`${value}\n`);
|
|
11
|
+
}
|
|
12
|
+
export function command(command, args, cwd) {
|
|
13
|
+
const result = spawnSync(command, args, { cwd, encoding: "utf8" });
|
|
14
|
+
if (result.error)
|
|
15
|
+
throw result.error;
|
|
16
|
+
return { status: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
17
|
+
}
|
|
18
|
+
export function exists(pathname) {
|
|
19
|
+
try {
|
|
20
|
+
return Boolean(fs.lstatSync(pathname));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=common.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type UtilityIo } from "./common.js";
|
|
2
|
+
export type CorePolicyMirrorOptions = UtilityIo & {
|
|
3
|
+
root?: string;
|
|
4
|
+
};
|
|
5
|
+
/** Find stale personal-policy mirror files, retaining the shell command's modes and exit ABI. */
|
|
6
|
+
export declare function detectStaleCorePolicyMirror(args?: string[], options?: CorePolicyMirrorOptions): number;
|
|
7
|
+
//# sourceMappingURL=detect-stale-core-policy-mirror.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** Native implementation of core/scripts/detect-stale-core-policy-mirror.sh. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { ioFor, line } from "./common.js";
|
|
5
|
+
function namesIn(directory) {
|
|
6
|
+
try {
|
|
7
|
+
return fs.readdirSync(directory).filter((name) => name.endsWith(".md")).sort();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Find stale personal-policy mirror files, retaining the shell command's modes and exit ABI. */
|
|
14
|
+
export function detectStaleCorePolicyMirror(args = [], options = {}) {
|
|
15
|
+
const { stdout, stderr } = ioFor(options);
|
|
16
|
+
const arg = args[0] ?? "";
|
|
17
|
+
const mode = arg === "" ? "report" : arg === "--json" ? "json" : arg === "--check" ? "check" : arg === "--prune-identical" ? "prune" : arg === "-h" || arg === "--help" ? "help" : "invalid";
|
|
18
|
+
if (mode === "invalid") {
|
|
19
|
+
line(stderr, `detect-stale-core-policy-mirror: unknown argument: ${arg}`);
|
|
20
|
+
line(stderr, "usage: detect-stale-core-policy-mirror.sh [--json|--check|--prune-identical]");
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
if (mode === "help") {
|
|
24
|
+
stdout("# detect-stale-core-policy-mirror.sh — find leftover core/policies/ copies of a\n# personal/policies/ twin from before the personal→core policy mirror was\n# retired.\n#\n# Background: HQ used to symlink personal/policies/<slug>.md into\n# core/policies/<slug>.md at reindex time. That mirror is retired — the policy\n# trigger hook (.claude/hooks/inject-policy-on-trigger.sh) now reads\n# personal/policies/ DIRECTLY, ahead of core/policies/, and dedups by policy id.\n# But an install upgraded across the retirement can still carry stale REGULAR\n# FILE copies of personal policies inside core/policies/. Those copies are\n# release-owned scaffold: /update-hq will not refresh them from personal/, and a\n# copy that is missing the when:/on: trigger frontmatter its personal twin has\n# gained (via migrate-policy-triggers.sh) can shadow or diverge from the live\n# rule in confusing ways.\n#\n# This tool classifies every core/policies twin of a personal policy:\n# identical — byte-identical to the personal twin: an orphaned mirror copy,\n# behavior-neutral to remove (the personal twin still loads, and\n# /update-hq restores any genuinely release-shipped file).\n# symlink — a leftover personal→core mirror symlink: resolves back to\n# personal, so also a behavior-neutral orphan.\n# diverged — content differs from the personal twin. DRIFT CAN RUN EITHER\n# DIRECTION (the core copy may be the newer, genericized one).\n# These are NEVER pruned — a human must classify each.\n#\n# Modes:\n# (default) human-readable report; always exit 0.\n# --json machine-readable JSON array; exit 0.\n# --check exit 0 if no twins, 1 if any twin exists (advisory gate).\n# --prune-identical remove ONLY identical + leftover-symlink orphans; never\n# touch diverged; print each action; exit 0.\n#\n# It never writes to personal/, never syncs personal→core, and never removes a\n# core/policies file that has no personal twin (release-shipped policies are\n# left untouched by construction).\n");
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const root = options.root ?? process.env.HQ_ROOT ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
28
|
+
const personal = path.join(root, "personal/policies");
|
|
29
|
+
const core = path.join(root, "core/policies");
|
|
30
|
+
const mirrors = [];
|
|
31
|
+
for (const policy of namesIn(personal)) {
|
|
32
|
+
if (policy === "example-policy.md" || policy === "README.md")
|
|
33
|
+
continue;
|
|
34
|
+
const personalFile = path.join(personal, policy);
|
|
35
|
+
const coreFile = path.join(core, policy);
|
|
36
|
+
let coreStat;
|
|
37
|
+
try {
|
|
38
|
+
coreStat = fs.lstatSync(coreFile);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
let status;
|
|
44
|
+
if (coreStat.isSymbolicLink())
|
|
45
|
+
status = "symlink";
|
|
46
|
+
else
|
|
47
|
+
status = fs.readFileSync(personalFile).equals(fs.readFileSync(coreFile)) ? "identical" : "diverged";
|
|
48
|
+
mirrors.push({ policy, status, core_path: `core/policies/${policy}`, personal_path: `personal/policies/${policy}` });
|
|
49
|
+
}
|
|
50
|
+
const orphaned = mirrors.filter((item) => item.status !== "diverged");
|
|
51
|
+
const diverged = mirrors.filter((item) => item.status === "diverged");
|
|
52
|
+
if (mode === "json") {
|
|
53
|
+
line(stdout, JSON.stringify(mirrors));
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
if (mode === "check") {
|
|
57
|
+
if (!mirrors.length) {
|
|
58
|
+
line(stdout, "detect-stale-core-policy-mirror: clean (no core/policies twins of a personal policy)");
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
line(stderr, `detect-stale-core-policy-mirror: ${mirrors.length} twin(s) found — ${orphaned.length} prunable orphan(s), ${diverged.length} diverged (need human classification)`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
if (mode === "prune") {
|
|
65
|
+
if (!mirrors.length) {
|
|
66
|
+
line(stdout, "detect-stale-core-policy-mirror: nothing to prune (no twins)");
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
for (const item of mirrors) {
|
|
70
|
+
if (item.status === "diverged")
|
|
71
|
+
line(stdout, `SKIPPED (diverged, needs human classification): ${item.core_path}`);
|
|
72
|
+
else {
|
|
73
|
+
fs.unlinkSync(path.join(root, item.core_path));
|
|
74
|
+
line(stdout, `pruned (${item.status}): ${item.core_path}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
line(stdout, `detect-stale-core-policy-mirror: pruned ${orphaned.length} orphan(s); left ${diverged.length} diverged twin(s) for review.`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
if (!mirrors.length) {
|
|
81
|
+
line(stdout, "detect-stale-core-policy-mirror: clean — no core/policies copies shadow a personal twin.");
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
line(stdout, `Stale personal→core policy mirror copies in core/policies/ (${mirrors.length} twin(s)):`);
|
|
85
|
+
line(stdout, "");
|
|
86
|
+
if (orphaned.length) {
|
|
87
|
+
line(stdout, "Orphaned mirror copies (behavior-neutral — remove with --prune-identical):");
|
|
88
|
+
for (const item of orphaned)
|
|
89
|
+
line(stdout, ` ${item.status.padEnd(10)} ${item.core_path}`);
|
|
90
|
+
line(stdout, "");
|
|
91
|
+
}
|
|
92
|
+
if (diverged.length) {
|
|
93
|
+
line(stdout, "Diverged copies (DRIFT MAY RUN EITHER DIRECTION — classify by hand, do NOT blind-sync):");
|
|
94
|
+
for (const item of diverged)
|
|
95
|
+
line(stdout, ` diverged ${item.core_path} (diff: git diff --no-index ${item.personal_path} ${item.core_path})`);
|
|
96
|
+
line(stdout, "");
|
|
97
|
+
}
|
|
98
|
+
line(stdout, `Summary: ${orphaned.length} orphan(s), ${diverged.length} diverged.`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=detect-stale-core-policy-mirror.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type UtilityIo } from "./common.js";
|
|
2
|
+
type HqStatusOptions = UtilityIo & {
|
|
3
|
+
cwd?: string;
|
|
4
|
+
root?: string;
|
|
5
|
+
};
|
|
6
|
+
/** Classify porcelain status records using the same path-prefix and baseline rules as the shell utility. */
|
|
7
|
+
export declare function summarizeHqStatus(args?: string[], options?: HqStatusOptions): number;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=hq-status-summary.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Native implementation of core/scripts/hq-status-summary.sh. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { command, ioFor, line } from "./common.js";
|
|
5
|
+
function jsonPaths(value) { try {
|
|
6
|
+
const input = JSON.parse(value);
|
|
7
|
+
return Array.isArray(input) ? input.flatMap((entry) => typeof entry === "string" ? [entry] : entry && typeof entry === "object" && typeof entry.path === "string" ? [entry.path] : []) : [];
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return [];
|
|
11
|
+
} }
|
|
12
|
+
function matchesBaseline(pathname, pattern) { const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*"); return new RegExp(`^${escaped}$`).test(pathname); }
|
|
13
|
+
function statusRoot(cwd, root) { if (root)
|
|
14
|
+
return root; if (process.env.HQ_ROOT)
|
|
15
|
+
return process.env.HQ_ROOT; const result = command("git", ["rev-parse", "--show-toplevel"], cwd); return result.status === 0 ? result.stdout.trim() : cwd; }
|
|
16
|
+
/** Classify porcelain status records using the same path-prefix and baseline rules as the shell utility. */
|
|
17
|
+
export function summarizeHqStatus(args = [], options = {}) {
|
|
18
|
+
const { stdout, stderr } = ioFor(options);
|
|
19
|
+
const cwd = options.cwd ?? process.cwd();
|
|
20
|
+
let porcelainFile = "", sessionJson = "[]", sessionFile = "", format = "json";
|
|
21
|
+
for (let index = 0; index < args.length;) {
|
|
22
|
+
const arg = args[index];
|
|
23
|
+
if (arg === "--porcelain-file") {
|
|
24
|
+
porcelainFile = args[index + 1] ?? "";
|
|
25
|
+
index += 2;
|
|
26
|
+
}
|
|
27
|
+
else if (arg === "--session-files-json") {
|
|
28
|
+
sessionJson = args[index + 1] ?? "";
|
|
29
|
+
index += 2;
|
|
30
|
+
}
|
|
31
|
+
else if (arg === "--session-files-file") {
|
|
32
|
+
sessionFile = args[index + 1] ?? "";
|
|
33
|
+
index += 2;
|
|
34
|
+
}
|
|
35
|
+
else if (arg === "--json") {
|
|
36
|
+
format = "json";
|
|
37
|
+
index++;
|
|
38
|
+
}
|
|
39
|
+
else if (arg === "--text") {
|
|
40
|
+
format = "text";
|
|
41
|
+
index++;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
line(stderr, `Unknown arg: ${arg}`);
|
|
45
|
+
return 2;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (sessionFile && !fs.existsSync(sessionFile)) {
|
|
49
|
+
line(stderr, `hq-status-summary: --session-files-file not found: ${sessionFile}`);
|
|
50
|
+
return 3;
|
|
51
|
+
}
|
|
52
|
+
const root = statusRoot(cwd, options.root);
|
|
53
|
+
const sessions = sessionFile ? jsonPaths(fs.readFileSync(sessionFile, "utf8")) : jsonPaths(sessionJson);
|
|
54
|
+
let patterns = ["companies/*", "workspace/*", "repos/*", "core/settings/*", ".hq/*"];
|
|
55
|
+
const baseline = path.join(root, "workspace/baseline/hq-local-baseline.json");
|
|
56
|
+
if (fs.existsSync(baseline)) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(fs.readFileSync(baseline, "utf8"));
|
|
59
|
+
patterns = (parsed.categories ?? []).flatMap((category) => (category.patterns ?? []).filter((item) => typeof item === "string"));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
patterns = [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const source = porcelainFile ? fs.readFileSync(path.resolve(cwd, porcelainFile), "utf8") : command("git", ["status", "--porcelain", "--ignored"], root).stdout;
|
|
66
|
+
const lists = { tracked_changes: [], session_touched_untracked: [], baseline_untracked: [], unrelated_untracked: [], ignored: [] };
|
|
67
|
+
for (const item of source.split(/\r?\n/)) {
|
|
68
|
+
if (!item)
|
|
69
|
+
continue;
|
|
70
|
+
const code = item.slice(0, 2);
|
|
71
|
+
let pathname = item.slice(3);
|
|
72
|
+
if (pathname.includes(" -> "))
|
|
73
|
+
pathname = pathname.slice(pathname.lastIndexOf(" -> ") + 4);
|
|
74
|
+
if (code === "!!")
|
|
75
|
+
lists.ignored.push(pathname);
|
|
76
|
+
else if (code === "??") {
|
|
77
|
+
if (sessions.some((candidate) => pathname === candidate || pathname.startsWith(`${candidate}/`) || candidate.startsWith(`${pathname}/`)))
|
|
78
|
+
lists.session_touched_untracked.push(pathname);
|
|
79
|
+
else if (patterns.some((pattern) => matchesBaseline(pathname, pattern)))
|
|
80
|
+
lists.baseline_untracked.push(pathname);
|
|
81
|
+
else
|
|
82
|
+
lists.unrelated_untracked.push(pathname);
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
lists.tracked_changes.push(pathname);
|
|
86
|
+
}
|
|
87
|
+
if (format === "text") {
|
|
88
|
+
for (const [key, values] of Object.entries(lists))
|
|
89
|
+
line(stdout, `${key === "session_touched_untracked" ? "session_untracked" : key}=${values.length}`);
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
const output = { counts: { tracked_changes: lists.tracked_changes.length, session_touched_untracked: lists.session_touched_untracked.length, baseline_untracked: lists.baseline_untracked.length, unrelated_untracked: lists.unrelated_untracked.length, ignored: lists.ignored.length, baseline_noise: lists.baseline_untracked.length + lists.unrelated_untracked.length }, files: lists };
|
|
93
|
+
line(stdout, JSON.stringify(output, null, 2));
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=hq-status-summary.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type UtilityIo } from "./common.js";
|
|
2
|
+
export type OntologyReadmeDriftOptions = UtilityIo & {
|
|
3
|
+
root?: string;
|
|
4
|
+
};
|
|
5
|
+
/** Compare the first README team table with ontology canonical names and aliases. */
|
|
6
|
+
export declare function ontologyReadmeDrift(args?: string[], options?: OntologyReadmeDriftOptions): number;
|
|
7
|
+
//# sourceMappingURL=ontology-readme-drift.d.ts.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Native implementation of core/scripts/ontology-readme-drift.sh. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { ioFor, line } from "./common.js";
|
|
5
|
+
const usage = "USAGE: ontology-readme-drift.sh <company-slug> [--root <HQ_ROOT>]";
|
|
6
|
+
const trim = (value) => value.trim();
|
|
7
|
+
const unquote = (value) => { const trimmed = trim(value); return (trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'")) ? trimmed.slice(1, -1) : trimmed; };
|
|
8
|
+
function teamNames(readme) {
|
|
9
|
+
const results = [];
|
|
10
|
+
let team = false, table = false, rows = 0;
|
|
11
|
+
for (const raw of fs.readFileSync(readme, "utf8").split(/\r?\n/)) {
|
|
12
|
+
if (/^##\s+Team\s*$/.test(raw)) {
|
|
13
|
+
team = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (team && /^##\s+/.test(raw))
|
|
17
|
+
break;
|
|
18
|
+
if (!team)
|
|
19
|
+
continue;
|
|
20
|
+
if (/^\s*\|/.test(raw)) {
|
|
21
|
+
table = true;
|
|
22
|
+
rows++;
|
|
23
|
+
if (rows <= 2)
|
|
24
|
+
continue;
|
|
25
|
+
const name = trim((raw.replace(/^\s*/, "").split("|")[2] ?? "").replace(/<!--[^>]*-->/g, ""));
|
|
26
|
+
if (name)
|
|
27
|
+
results.push(name);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (table)
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
return results;
|
|
34
|
+
}
|
|
35
|
+
function personNames(person) {
|
|
36
|
+
const out = [];
|
|
37
|
+
let markers = 0, frontmatter = false, aliases = false;
|
|
38
|
+
for (const raw of fs.readFileSync(person, "utf8").split(/\r?\n/)) {
|
|
39
|
+
if (/^---\s*$/.test(raw)) {
|
|
40
|
+
markers++;
|
|
41
|
+
if (markers === 1) {
|
|
42
|
+
frontmatter = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (markers === 2)
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (!frontmatter)
|
|
49
|
+
continue;
|
|
50
|
+
if (/^\s*aliases:\s*$/.test(raw)) {
|
|
51
|
+
aliases = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (aliases && /^\s*-\s*/.test(raw)) {
|
|
55
|
+
const value = unquote(raw.replace(/^\s*-\s*/, ""));
|
|
56
|
+
if (value)
|
|
57
|
+
out.push(value);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (aliases && !/^\s*(?:$|#)/.test(raw))
|
|
61
|
+
aliases = false;
|
|
62
|
+
if (/^\s*canonical_name\s*:/.test(raw)) {
|
|
63
|
+
const value = unquote(raw.replace(/^\s*canonical_name\s*:\s*/, ""));
|
|
64
|
+
if (value)
|
|
65
|
+
out.push(value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function normalize(name) { return name.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, "").replace(/\s+/g, " ").trim(); }
|
|
71
|
+
function matches(a, b) { if (a === b)
|
|
72
|
+
return true; if (!a || !b)
|
|
73
|
+
return false; const at = a.split(" "), bt = b.split(" "); const [smaller, larger] = at.length < bt.length || at.length === bt.length && a.length <= b.length ? [at, bt] : [bt, at]; return smaller.every((token) => larger.includes(token)); }
|
|
74
|
+
/** Compare the first README team table with ontology canonical names and aliases. */
|
|
75
|
+
export function ontologyReadmeDrift(args = [], options = {}) {
|
|
76
|
+
const { stdout, stderr } = ioFor(options);
|
|
77
|
+
let slug = "", rootArg = "";
|
|
78
|
+
for (let index = 0; index < args.length;) {
|
|
79
|
+
const arg = args[index];
|
|
80
|
+
if (arg === "--root") {
|
|
81
|
+
if (index + 1 >= args.length) {
|
|
82
|
+
line(stderr, usage);
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
rootArg = args[index + 1];
|
|
86
|
+
index += 2;
|
|
87
|
+
}
|
|
88
|
+
else if (arg.startsWith("-") || slug) {
|
|
89
|
+
line(stderr, usage);
|
|
90
|
+
return 2;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
slug = arg;
|
|
94
|
+
index++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!slug) {
|
|
98
|
+
line(stderr, usage);
|
|
99
|
+
return 2;
|
|
100
|
+
}
|
|
101
|
+
const root = rootArg || options.root || process.env.HQ_ROOT || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
102
|
+
const readme = path.join(root, "companies", slug, "README.md");
|
|
103
|
+
const persons = path.join(root, "companies", slug, "ontology/entities/person");
|
|
104
|
+
if (!fs.existsSync(readme)) {
|
|
105
|
+
line(stderr, `ERROR: README missing: companies/${slug}/README.md`);
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
if (!fs.existsSync(persons) || !fs.statSync(persons).isDirectory()) {
|
|
109
|
+
line(stderr, `ERROR: ontology person directory missing: companies/${slug}/ontology/entities/person`);
|
|
110
|
+
return 2;
|
|
111
|
+
}
|
|
112
|
+
const files = fs.readdirSync(persons).filter((name) => name.endsWith(".md") && fs.statSync(path.join(persons, name)).isFile());
|
|
113
|
+
if (!files.length) {
|
|
114
|
+
line(stderr, `ERROR: ontology person directory is empty: companies/${slug}/ontology/entities/person`);
|
|
115
|
+
return 2;
|
|
116
|
+
}
|
|
117
|
+
const ontology = files.flatMap((name) => personNames(path.join(persons, name))).map(normalize).filter(Boolean);
|
|
118
|
+
if (!ontology.length) {
|
|
119
|
+
line(stderr, `ERROR: ontology person directory has no canonical_name or aliases to compare: companies/${slug}/ontology/entities/person`);
|
|
120
|
+
return 2;
|
|
121
|
+
}
|
|
122
|
+
let drift = 0;
|
|
123
|
+
for (const name of teamNames(readme)) {
|
|
124
|
+
const normalized = normalize(name);
|
|
125
|
+
if (normalized && !ontology.some((candidate) => matches(normalized, candidate))) {
|
|
126
|
+
line(stdout, `DRIFT: "${name}" in companies/${slug} README team table has no matching active ontology person entity (possibly departed or misspelled)`);
|
|
127
|
+
drift++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!drift)
|
|
131
|
+
line(stdout, `ontology-readme-drift(${slug}): no drift`);
|
|
132
|
+
line(stdout, `ontology-readme-drift(${slug}): ${drift} drift(s)`);
|
|
133
|
+
return drift ? 1 : 0;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=ontology-readme-drift.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type UtilityIo } from "./common.js";
|
|
2
|
+
export interface ResizeScreenshotOptions extends UtilityIo {
|
|
3
|
+
run?: (binary: string, args: string[]) => {
|
|
4
|
+
status: number;
|
|
5
|
+
stdout: string;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** Resize a screenshot through the legacy sips/magick codec boundary. */
|
|
9
|
+
export declare function resizeScreenshot(args?: string[], options?: ResizeScreenshotOptions): number;
|
|
10
|
+
//# sourceMappingURL=resize-screenshot.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Native implementation of core/scripts/resize-screenshot.sh. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { command, ioFor, line } from "./common.js";
|
|
4
|
+
const MAX_DIM = 1800;
|
|
5
|
+
/** Resize a screenshot through the legacy sips/magick codec boundary. */
|
|
6
|
+
export function resizeScreenshot(args = [], options = {}) {
|
|
7
|
+
const { stdout, stderr } = ioFor(options);
|
|
8
|
+
const file = args[0] ?? "";
|
|
9
|
+
if (!file || !fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
10
|
+
line(stderr, `resize-screenshot: file not found: ${file}`);
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
13
|
+
const run = options.run ?? ((binary, commandArgs) => command(binary, commandArgs));
|
|
14
|
+
// The original always queries sips before checking the fallback. Preserve that
|
|
15
|
+
// ABI; magick is only a resize fallback, not a metadata reader.
|
|
16
|
+
let width = "", height = "";
|
|
17
|
+
try {
|
|
18
|
+
width = run("sips", ["-g", "pixelWidth", file]).stdout.split(/\r?\n/).find((entry) => entry.includes("pixelWidth"))?.trim().split(/\s+/)[1] ?? "";
|
|
19
|
+
height = run("sips", ["-g", "pixelHeight", file]).stdout.split(/\r?\n/).find((entry) => entry.includes("pixelHeight"))?.trim().split(/\s+/)[1] ?? "";
|
|
20
|
+
}
|
|
21
|
+
catch { /* missing sips intentionally maps to legacy dimension error */ }
|
|
22
|
+
const w = Number.parseInt(width, 10), h = Number.parseInt(height, 10);
|
|
23
|
+
if (!width || !height || Number.isNaN(w) || Number.isNaN(h)) {
|
|
24
|
+
line(stderr, "resize-screenshot: cannot read dimensions");
|
|
25
|
+
return 2;
|
|
26
|
+
}
|
|
27
|
+
if (w <= MAX_DIM && h <= MAX_DIM) {
|
|
28
|
+
line(stdout, `resize-screenshot: ${w}x${h} — within bounds, skipping`);
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
const [newW, newH] = w >= h ? [MAX_DIM, Math.trunc(h * MAX_DIM / w)] : [Math.trunc(w * MAX_DIM / h), MAX_DIM];
|
|
32
|
+
line(stdout, `resize-screenshot: ${w}x${h} → ${newW}x${newH} (${file})`);
|
|
33
|
+
try {
|
|
34
|
+
const sips = run("sips", ["--resampleHeightWidth", String(newH), String(newW), file, "--out", file]);
|
|
35
|
+
if (sips.status !== 0) {
|
|
36
|
+
const magick = run("magick", [file, "-resize", `${MAX_DIM}x${MAX_DIM}>`, file]);
|
|
37
|
+
if (magick.status !== 0) {
|
|
38
|
+
line(stderr, "resize-screenshot: no image tool available (sips/magick)");
|
|
39
|
+
return 2;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
line(stderr, "resize-screenshot: no image tool available (sips/magick)");
|
|
45
|
+
return 2;
|
|
46
|
+
}
|
|
47
|
+
line(stdout, "resize-screenshot: done");
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=resize-screenshot.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type UtilityIo } from "./common.js";
|
|
2
|
+
export type TokenUsageReportOptions = UtilityIo & {
|
|
3
|
+
projectDir?: string;
|
|
4
|
+
now?: Date;
|
|
5
|
+
};
|
|
6
|
+
/** Print token usage totals from Claude session JSONL files. */
|
|
7
|
+
export declare function tokenUsageReport(args?: string[], options?: TokenUsageReportOptions): number;
|
|
8
|
+
//# sourceMappingURL=token-usage-report.d.ts.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/** Native implementation of core/scripts/token-usage-report.sh. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { ioFor, line } from "./common.js";
|
|
6
|
+
const format = (value) => value.toLocaleString("en-US");
|
|
7
|
+
const dayOf = (date) => date.toISOString().slice(0, 10);
|
|
8
|
+
const parseJsonl = (file) => fs.readFileSync(file, "utf8").split(/\r?\n/).flatMap((raw) => { try {
|
|
9
|
+
return [JSON.parse(raw)];
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return [];
|
|
13
|
+
} });
|
|
14
|
+
const listJsonl = (dir) => { try {
|
|
15
|
+
return fs.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).sort();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return [];
|
|
19
|
+
} };
|
|
20
|
+
const readNumber = (value) => typeof value === "number" ? value : 0;
|
|
21
|
+
function firstUser(record) { const content = record.message?.content; if (typeof content === "string")
|
|
22
|
+
return content.slice(0, 120); if (Array.isArray(content) && content.length) {
|
|
23
|
+
const first = content[0];
|
|
24
|
+
if (first && typeof first === "object" && typeof first.text === "string")
|
|
25
|
+
return first.text.slice(0, 120);
|
|
26
|
+
} return ""; }
|
|
27
|
+
function comparison(projectDir, before, after, minHours, json, stdout) {
|
|
28
|
+
const parseRange = (range) => range.split(":").map((value) => new Date(`${value}T00:00:00Z`));
|
|
29
|
+
const [bStart, bEnd] = parseRange(before), [aStart, aEnd] = parseRange(after);
|
|
30
|
+
const collect = (start, end) => listJsonl(projectDir).flatMap((name) => {
|
|
31
|
+
const rows = parseJsonl(path.join(projectDir, name));
|
|
32
|
+
let cr = 0, first, last;
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
cr += readNumber(row.message?.usage?.cache_read_input_tokens);
|
|
35
|
+
if (row.timestamp) {
|
|
36
|
+
if (!first)
|
|
37
|
+
first = row.timestamp;
|
|
38
|
+
last = row.timestamp;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!first || !last)
|
|
42
|
+
return [];
|
|
43
|
+
const firstDate = new Date(first), lastDate = new Date(last), day = new Date(`${first.slice(0, 10)}T00:00:00Z`);
|
|
44
|
+
if (Number.isNaN(firstDate.getTime()) || Number.isNaN(lastDate.getTime()) || day < start || day > end)
|
|
45
|
+
return [];
|
|
46
|
+
const hours = Math.max((lastDate.getTime() - firstDate.getTime()) / 3_600_000, 0.01);
|
|
47
|
+
if (hours < minHours)
|
|
48
|
+
return [];
|
|
49
|
+
return [{ sid: name.replace(/\.jsonl$/, "").slice(0, 8), hours: Math.round(hours * 10) / 10, cr, cr_per_hour: Math.trunc(cr / hours) }];
|
|
50
|
+
});
|
|
51
|
+
const bRows = collect(bStart, bEnd), aRows = collect(aStart, aEnd);
|
|
52
|
+
const median = (rows) => { if (!rows.length)
|
|
53
|
+
return 0; const values = rows.map((row) => row.cr_per_hour).sort((a, b) => a - b); const mid = Math.floor(values.length / 2); return Math.trunc(values.length % 2 ? values[mid] : (values[mid - 1] + values[mid]) / 2); };
|
|
54
|
+
const bMed = median(bRows), aMed = median(aRows), delta = bMed ? (bMed - aMed) / bMed * 100 : 0;
|
|
55
|
+
if (json) {
|
|
56
|
+
line(stdout, JSON.stringify({ before: { window: before, sessions: bRows.length, median_cr_per_hour: bMed, rows: bRows }, after: { window: after, sessions: aRows.length, median_cr_per_hour: aMed, rows: aRows }, delta_pct: Math.round(delta * 10) / 10 }, null, 2));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
line(stdout, "");
|
|
60
|
+
line(stdout, `Cache_read/hour comparison (min ${minHours}h session duration)`);
|
|
61
|
+
line(stdout, "");
|
|
62
|
+
line(stdout, ` BEFORE ${before} — ${bRows.length} sessions`);
|
|
63
|
+
for (const row of bRows)
|
|
64
|
+
line(stdout, ` ${row.sid} ${row.hours.toFixed(1).padStart(5)}h cr_per_hour=${format(row.cr_per_hour).padStart(12)}`);
|
|
65
|
+
line(stdout, ` median: ${format(bMed)}/h`);
|
|
66
|
+
line(stdout, `\n AFTER ${after} — ${aRows.length} sessions`);
|
|
67
|
+
for (const row of aRows)
|
|
68
|
+
line(stdout, ` ${row.sid} ${row.hours.toFixed(1).padStart(5)}h cr_per_hour=${format(row.cr_per_hour).padStart(12)}`);
|
|
69
|
+
line(stdout, ` median: ${format(aMed)}/h`);
|
|
70
|
+
const verdict = delta > 0 ? "IMPROVED" : delta < 0 ? "WORSE" : "unchanged";
|
|
71
|
+
line(stdout, `\n Δ: ${delta >= 0 ? "+" : ""}${delta.toFixed(1)}% (${verdict})`);
|
|
72
|
+
}
|
|
73
|
+
/** Print token usage totals from Claude session JSONL files. */
|
|
74
|
+
export function tokenUsageReport(args = [], options = {}) {
|
|
75
|
+
const { stdout, stderr } = ioFor(options);
|
|
76
|
+
let lastDays = 7, since = "", json = false, before = "", after = "", minHours = 0;
|
|
77
|
+
for (let index = 0; index < args.length;) {
|
|
78
|
+
const arg = args[index];
|
|
79
|
+
if (arg === "--last") {
|
|
80
|
+
lastDays = Number.parseInt(args[index + 1] ?? "", 10);
|
|
81
|
+
index += 2;
|
|
82
|
+
}
|
|
83
|
+
else if (arg === "--since") {
|
|
84
|
+
since = args[index + 1] ?? "";
|
|
85
|
+
index += 2;
|
|
86
|
+
}
|
|
87
|
+
else if (arg === "--json") {
|
|
88
|
+
json = true;
|
|
89
|
+
index++;
|
|
90
|
+
}
|
|
91
|
+
else if (arg === "--compare-windows")
|
|
92
|
+
index++;
|
|
93
|
+
else if (arg === "--before") {
|
|
94
|
+
before = args[index + 1] ?? "";
|
|
95
|
+
index += 2;
|
|
96
|
+
}
|
|
97
|
+
else if (arg === "--after") {
|
|
98
|
+
after = args[index + 1] ?? "";
|
|
99
|
+
index += 2;
|
|
100
|
+
}
|
|
101
|
+
else if (arg === "--min-hours") {
|
|
102
|
+
minHours = Number.parseFloat(args[index + 1] ?? "");
|
|
103
|
+
index += 2;
|
|
104
|
+
}
|
|
105
|
+
else if (arg === "-h" || arg === "--help") {
|
|
106
|
+
stdout("token-usage-report.sh — Daily effective-token totals + top burners from Claude Code session JSONLs.\nUsage:\n scripts/token-usage-report.sh # last 7 days, table\n scripts/token-usage-report.sh --last 14 # last 14 days\n scripts/token-usage-report.sh --since 2026-05-01\n scripts/token-usage-report.sh --json # machine-readable\nEffective tokens weighting (matches weekly rate-limit volume):\n input + 5×output + 1.25×cache_creation + 0.1×cache_read\n--- Compare-windows mode (US-011) ---\nFormat: --before YYYY-MM-DD:YYYY-MM-DD --after YYYY-MM-DD:YYYY-MM-DD\n");
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
line(stderr, `Unknown arg: ${arg}`);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Preserve the bundled shell asset's historical parameter-expansion parsing:
|
|
115
|
+
// the `}` in `{your-name}` terminates `${CLAUDE_PROJECTS_DIR:-…}` early, so
|
|
116
|
+
// its literal `-Documents-HQ}` suffix remains even when the environment
|
|
117
|
+
// variable is set. `projectDir` is the explicit native test/integration seam.
|
|
118
|
+
const projectDir = options.projectDir ?? `${process.env.CLAUDE_PROJECTS_DIR ?? path.join(os.homedir(), ".claude/projects/-Users-{your-name")}-Documents-HQ}`;
|
|
119
|
+
if (before && after) {
|
|
120
|
+
comparison(projectDir, before, after, minHours, json, stdout);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
if (!fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory()) {
|
|
124
|
+
line(stderr, `Project dir not found: ${projectDir}`);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
const now = options.now ?? new Date();
|
|
128
|
+
const cutoff = since || dayOf(new Date(now.getTime() - (lastDays - 1) * 86_400_000));
|
|
129
|
+
const days = new Map(), sessions = new Map();
|
|
130
|
+
for (const name of listJsonl(projectDir)) {
|
|
131
|
+
const file = path.join(projectDir, name);
|
|
132
|
+
const stat = fs.statSync(file);
|
|
133
|
+
const mtime = dayOf(stat.mtime);
|
|
134
|
+
if (mtime < cutoff)
|
|
135
|
+
continue;
|
|
136
|
+
let inp = 0, out = 0, cc = 0, cr = 0, firstTs = "", lastTs = "", first = "";
|
|
137
|
+
for (const record of parseJsonl(file)) {
|
|
138
|
+
const usage = record.message?.usage;
|
|
139
|
+
inp += readNumber(usage?.input_tokens);
|
|
140
|
+
out += readNumber(usage?.output_tokens);
|
|
141
|
+
cc += readNumber(usage?.cache_creation_input_tokens);
|
|
142
|
+
cr += readNumber(usage?.cache_read_input_tokens);
|
|
143
|
+
if (record.timestamp) {
|
|
144
|
+
if (!firstTs)
|
|
145
|
+
firstTs = record.timestamp;
|
|
146
|
+
lastTs = record.timestamp;
|
|
147
|
+
}
|
|
148
|
+
if (!first && record.type === "user")
|
|
149
|
+
first = firstUser(record);
|
|
150
|
+
}
|
|
151
|
+
const sid = name.replace(/\.jsonl$/, "");
|
|
152
|
+
const subagents = (() => { try {
|
|
153
|
+
return fs.readdirSync(path.join(projectDir, sid, "subagents")).filter((entry) => entry.endsWith(".jsonl")).length;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return 0;
|
|
157
|
+
} })();
|
|
158
|
+
const day = (firstTs || lastTs || "").slice(0, 10) || mtime;
|
|
159
|
+
if (day < cutoff)
|
|
160
|
+
continue;
|
|
161
|
+
const current = days.get(day) ?? { sessions: new Set(), inp: 0, out: 0, cc: 0, cr: 0 };
|
|
162
|
+
current.sessions.add(sid);
|
|
163
|
+
current.inp += inp;
|
|
164
|
+
current.out += out;
|
|
165
|
+
current.cc += cc;
|
|
166
|
+
current.cr += cr;
|
|
167
|
+
days.set(day, current);
|
|
168
|
+
sessions.set(sid, { day, inp, out, cc, cr, eff: Math.trunc(inp + 5 * out + 1.25 * cc + .1 * cr), subagents, first_user: first.replaceAll("\n", " ") });
|
|
169
|
+
}
|
|
170
|
+
const sorted = [...days.keys()].sort();
|
|
171
|
+
const dayRows = sorted.map((day) => { const value = days.get(day); return { date: day, sessions: value.sessions.size, input: value.inp, output: value.out, cache_create: value.cc, cache_read: value.cr, effective: Math.trunc(value.inp + 5 * value.out + 1.25 * value.cc + .1 * value.cr) }; });
|
|
172
|
+
const recent = sorted.at(-1);
|
|
173
|
+
const top = recent ? [...sessions.values()].filter((session) => session.day === recent).sort((a, b) => b.eff - a.eff).slice(0, 3) : [];
|
|
174
|
+
if (json) {
|
|
175
|
+
line(stdout, JSON.stringify({ days: dayRows, top_sessions_recent_day: top }, null, 2));
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
line(stdout, `\nToken usage (last ${sorted.length} day(s), cutoff ${cutoff})`);
|
|
179
|
+
line(stdout, " Effective = input + 5×output + 1.25×cache_create + 0.1×cache_read\n");
|
|
180
|
+
line(stdout, ` ${"date".padEnd(12)} ${"sessions".padStart(8)} ${"output".padStart(12)} ${"cache_read".padStart(14)} ${"effective".padStart(14)}`);
|
|
181
|
+
line(stdout, ` ${"-".repeat(12)} ${"-".repeat(8)} ${"-".repeat(12)} ${"-".repeat(14)} ${"-".repeat(14)}`);
|
|
182
|
+
let total = 0;
|
|
183
|
+
for (const row of dayRows) {
|
|
184
|
+
total += row.effective;
|
|
185
|
+
line(stdout, ` ${row.date.padEnd(12)} ${String(row.sessions).padStart(8)} ${format(row.output).padStart(12)} ${format(row.cache_read).padStart(14)} ${format(row.effective).padStart(14)}`);
|
|
186
|
+
}
|
|
187
|
+
line(stdout, `\n total effective: ${format(total)}`);
|
|
188
|
+
if (recent && top.length) {
|
|
189
|
+
line(stdout, `\nTop sessions on ${recent}:`);
|
|
190
|
+
for (const row of top)
|
|
191
|
+
line(stdout, ` ${[...sessions.entries()].find(([, value]) => value === row)?.[0].slice(0, 8)} eff=${format(row.eff).padStart(11)} subagents=${String(row.subagents).padStart(3)} ${row.first_user.slice(0, 80)}`);
|
|
192
|
+
}
|
|
193
|
+
line(stdout, "");
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=token-usage-report.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type UtilityIo } from "./common.js";
|
|
2
|
+
export type WorktreeOptions = UtilityIo & {
|
|
3
|
+
cwd?: string;
|
|
4
|
+
hqRoot?: string;
|
|
5
|
+
run?: (args: string[], cwd?: string) => {
|
|
6
|
+
status: number;
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
/** Create an isolated branch worktree at the exact location established by the legacy shell tool. */
|
|
12
|
+
export declare function createWorktree(args?: string[], options?: WorktreeOptions): number;
|
|
13
|
+
//# sourceMappingURL=worktree.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** Native implementation of core/scripts/worktree.sh. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { command, ioFor, line } from "./common.js";
|
|
5
|
+
function git(options, args, cwd) { return (options.run ?? ((commandArgs, workingDirectory) => command("git", commandArgs, workingDirectory)))(args, cwd); }
|
|
6
|
+
function emit(result, stdout, stderr) { if (result.stdout)
|
|
7
|
+
stdout(result.stdout); if (result.stderr)
|
|
8
|
+
stderr(result.stderr); }
|
|
9
|
+
/** Create an isolated branch worktree at the exact location established by the legacy shell tool. */
|
|
10
|
+
export function createWorktree(args = [], options = {}) {
|
|
11
|
+
const { stdout, stderr } = ioFor(options);
|
|
12
|
+
const cwd = options.cwd ?? process.cwd();
|
|
13
|
+
let name = "", source = "", branch = "", base = "", noPull = false;
|
|
14
|
+
const usage = (status = 0) => { stdout("# Create a git worktree under {HQ_ROOT}/workspace/worktrees/{repo}/{name}/ on a\n# fresh branch pointing at origin/{default-branch}. The source repo's working\n# tree and local branch refs are left untouched.\n#\n# Usage:\n# worktree.sh --name <slug> [--source <repo-path>] [--branch <new-branch>]\n# [--base <branch-or-ref>] [--no-pull]\n"); return status; };
|
|
15
|
+
for (let index = 0; index < args.length;) {
|
|
16
|
+
const arg = args[index];
|
|
17
|
+
if (arg === "--name") {
|
|
18
|
+
name = args[index + 1] ?? "";
|
|
19
|
+
index += 2;
|
|
20
|
+
}
|
|
21
|
+
else if (arg === "--source") {
|
|
22
|
+
source = args[index + 1] ?? "";
|
|
23
|
+
index += 2;
|
|
24
|
+
}
|
|
25
|
+
else if (arg === "--branch") {
|
|
26
|
+
branch = args[index + 1] ?? "";
|
|
27
|
+
index += 2;
|
|
28
|
+
}
|
|
29
|
+
else if (arg === "--base") {
|
|
30
|
+
base = args[index + 1] ?? "";
|
|
31
|
+
index += 2;
|
|
32
|
+
}
|
|
33
|
+
else if (arg === "--no-pull") {
|
|
34
|
+
noPull = true;
|
|
35
|
+
index++;
|
|
36
|
+
}
|
|
37
|
+
else if (arg === "-h" || arg === "--help")
|
|
38
|
+
return usage();
|
|
39
|
+
else {
|
|
40
|
+
line(stderr, `unknown arg: ${arg}`);
|
|
41
|
+
return usage(2);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const die = (message) => { line(stderr, `ERROR: ${message}`); return 1; };
|
|
45
|
+
if (!name)
|
|
46
|
+
return die("--name is required (kebab-case slug, e.g. 'fix-login-bug')");
|
|
47
|
+
if (name.includes("/") || name.includes(" "))
|
|
48
|
+
return die(`invalid --name '${name}': no slashes or spaces; use kebab-case`);
|
|
49
|
+
if (!source) {
|
|
50
|
+
const result = git(options, ["rev-parse", "--show-toplevel"], cwd);
|
|
51
|
+
if (result.status === 0)
|
|
52
|
+
source = result.stdout.trim();
|
|
53
|
+
}
|
|
54
|
+
if (!source)
|
|
55
|
+
return die("not inside a git repo and --source not given");
|
|
56
|
+
if (!fs.existsSync(path.join(source, ".git")))
|
|
57
|
+
return die(`${source} is not a git repo`);
|
|
58
|
+
source = fs.realpathSync(source);
|
|
59
|
+
const repo = path.basename(source);
|
|
60
|
+
let hqRoot = process.env.HQ_HOME || options.hqRoot || process.env.HQ_ROOT || "";
|
|
61
|
+
if (!hqRoot)
|
|
62
|
+
hqRoot = path.resolve(cwd, "../..");
|
|
63
|
+
if (!fs.existsSync(path.join(hqRoot, "workspace")))
|
|
64
|
+
return die(`HQ_ROOT=${hqRoot} has no workspace/ directory`);
|
|
65
|
+
const hasOrigin = git(options, ["-C", source, "remote", "get-url", "origin"]).status === 0;
|
|
66
|
+
if (!base) {
|
|
67
|
+
if (hasOrigin) {
|
|
68
|
+
const remoteHead = git(options, ["-C", source, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
|
|
69
|
+
if (remoteHead.status === 0)
|
|
70
|
+
base = remoteHead.stdout.trim().replace(/^origin\//, "");
|
|
71
|
+
}
|
|
72
|
+
if (!base) {
|
|
73
|
+
if (git(options, ["-C", source, "show-ref", "--verify", "--quiet", "refs/heads/main"]).status === 0)
|
|
74
|
+
base = "main";
|
|
75
|
+
else if (git(options, ["-C", source, "show-ref", "--verify", "--quiet", "refs/heads/master"]).status === 0)
|
|
76
|
+
base = "master";
|
|
77
|
+
else
|
|
78
|
+
return die("cannot determine default branch; pass --base <branch>");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!noPull && hasOrigin) {
|
|
82
|
+
line(stdout, `→ fetching origin/${base} in ${source}`);
|
|
83
|
+
const result = git(options, ["-C", source, "fetch", "origin", base]);
|
|
84
|
+
emit(result, stdout, stderr);
|
|
85
|
+
if (result.status !== 0)
|
|
86
|
+
return die(`git fetch origin ${base} failed; rerun with --no-pull to skip, or fix network/auth`);
|
|
87
|
+
}
|
|
88
|
+
else if (!noPull)
|
|
89
|
+
line(stdout, `⚠ no 'origin' remote on ${source} — skipping fetch, basing off local ${base}`);
|
|
90
|
+
branch ||= `wt/${name}`;
|
|
91
|
+
let baseRef = base;
|
|
92
|
+
if (hasOrigin && git(options, ["-C", source, "rev-parse", "--verify", "--quiet", `origin/${base}`]).status === 0)
|
|
93
|
+
baseRef = `origin/${base}`;
|
|
94
|
+
const parent = path.join(hqRoot, "workspace/worktrees", repo), target = path.join(parent, name);
|
|
95
|
+
if (fs.existsSync(target))
|
|
96
|
+
return die(`${target} already exists. Pick a different --name, or remove it first:\n git -C ${source} worktree remove ${target}`);
|
|
97
|
+
if (git(options, ["-C", source, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0)
|
|
98
|
+
return die(`branch '${branch}' already exists in ${source}. Pass --branch <other-name>, or delete:\n git -C ${source} branch -D ${branch}`);
|
|
99
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
100
|
+
line(stdout, `→ creating worktree ${target} on branch ${branch} from ${baseRef}`);
|
|
101
|
+
const added = git(options, ["-C", source, "worktree", "add", target, "-b", branch, baseRef]);
|
|
102
|
+
emit(added, stdout, stderr);
|
|
103
|
+
if (added.status !== 0)
|
|
104
|
+
return added.status;
|
|
105
|
+
// Shell captures this command substitution, so its stdout is intentionally
|
|
106
|
+
// not forwarded to the caller.
|
|
107
|
+
const sha = git(options, ["-C", target, "rev-parse", "--short", "HEAD"]);
|
|
108
|
+
if (sha.status !== 0)
|
|
109
|
+
return sha.status;
|
|
110
|
+
stdout(`\n✓ worktree ready\n path: ${target}\n branch: ${branch}\n base: ${baseRef} (${sha.stdout.trim()})\n source: ${source}\n\nMain checkout untouched — working tree, local '${base}' ref, and stash are unchanged.\n\nCleanup when done:\n git -C ${source} worktree remove ${target}\n git -C ${source} branch -D ${branch} # optional, drops the branch ref\n`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=worktree.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.91.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
31
31
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
32
|
-
"@indigoai-us/hq-cloud": "^6.14.
|
|
32
|
+
"@indigoai-us/hq-cloud": "^6.14.47",
|
|
33
33
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
34
34
|
"@sentry/node": "^10.49.0",
|
|
35
35
|
"@tobilu/qmd": "1.0.7",
|