harnery 0.4.0 → 0.6.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.
Files changed (100) hide show
  1. package/dist/commander.d.ts +10 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +2 -0
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +43 -29
  6. package/dist/commands/browse-ai.js +1 -1
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +41 -9
  9. package/dist/commands/completion.d.ts.map +1 -1
  10. package/dist/commands/completion.js +48 -10
  11. package/dist/commands/cookies.js +1 -1
  12. package/dist/commands/decision.d.ts +4 -0
  13. package/dist/commands/decision.d.ts.map +1 -0
  14. package/dist/commands/decision.js +354 -0
  15. package/dist/commands/docs.d.ts.map +1 -1
  16. package/dist/commands/docs.js +5 -1
  17. package/dist/commands/fetch.js +1 -1
  18. package/dist/core/agents/events/consume.d.ts +25 -2
  19. package/dist/core/agents/events/consume.d.ts.map +1 -1
  20. package/dist/core/agents/events/consume.js +55 -7
  21. package/dist/core/agents/events/emit.d.ts +2 -1
  22. package/dist/core/agents/events/emit.d.ts.map +1 -1
  23. package/dist/core/agents/events/emit.js +2 -1
  24. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  25. package/dist/core/agents/rules/claim-conflict.js +10 -2
  26. package/dist/core/agents/state/scratch.d.ts +1 -1
  27. package/dist/core/agents/state/scratch.js +2 -2
  28. package/dist/core/hooks/cli.js +4 -10
  29. package/dist/core/hooks/effects/index.d.ts.map +1 -1
  30. package/dist/core/hooks/effects/index.js +2 -1
  31. package/dist/core/hooks/guard-path.d.ts +29 -0
  32. package/dist/core/hooks/guard-path.d.ts.map +1 -0
  33. package/dist/core/hooks/guard-path.js +38 -0
  34. package/dist/lib/agent-browser/client.js +1 -1
  35. package/dist/lib/browser/client.d.ts +14 -0
  36. package/dist/lib/browser/client.d.ts.map +1 -1
  37. package/dist/lib/browser/client.js +20 -0
  38. package/dist/lib/browser/index.d.ts +1 -0
  39. package/dist/lib/browser/index.d.ts.map +1 -1
  40. package/dist/lib/browser/runts.d.ts +44 -0
  41. package/dist/lib/browser/runts.d.ts.map +1 -0
  42. package/dist/lib/browser/runts.js +193 -0
  43. package/dist/lib/completion/bash.d.ts +15 -0
  44. package/dist/lib/completion/bash.d.ts.map +1 -1
  45. package/dist/lib/completion/bash.js +35 -0
  46. package/dist/lib/completion/fish.d.ts +10 -0
  47. package/dist/lib/completion/fish.d.ts.map +1 -1
  48. package/dist/lib/completion/fish.js +21 -0
  49. package/dist/lib/completion/index.d.ts +4 -3
  50. package/dist/lib/completion/index.d.ts.map +1 -1
  51. package/dist/lib/completion/index.js +4 -3
  52. package/dist/lib/completion/resolve.d.ts +52 -0
  53. package/dist/lib/completion/resolve.d.ts.map +1 -0
  54. package/dist/lib/completion/resolve.js +171 -0
  55. package/dist/lib/completion/walk.js +1 -1
  56. package/dist/lib/completion/zsh.d.ts +8 -0
  57. package/dist/lib/completion/zsh.d.ts.map +1 -1
  58. package/dist/lib/completion/zsh.js +33 -0
  59. package/dist/lib/cookies/client.d.ts +1 -1
  60. package/dist/lib/cookies/client.d.ts.map +1 -1
  61. package/dist/lib/cookies/client.js +1 -1
  62. package/dist/lib/decision/index.d.ts +212 -0
  63. package/dist/lib/decision/index.d.ts.map +1 -0
  64. package/dist/lib/decision/index.js +523 -0
  65. package/dist/lib/docs-lint.d.ts +1 -0
  66. package/dist/lib/docs-lint.d.ts.map +1 -1
  67. package/dist/lib/docs-lint.js +55 -0
  68. package/dist/lib/tunnel/gate.js +1 -1
  69. package/package.json +3 -1
  70. package/src/commander.ts +12 -0
  71. package/src/commands/agents.ts +47 -26
  72. package/src/commands/browse-ai.ts +1 -1
  73. package/src/commands/browse.ts +63 -8
  74. package/src/commands/completion.ts +62 -9
  75. package/src/commands/cookies.ts +1 -1
  76. package/src/commands/decision.ts +438 -0
  77. package/src/commands/docs.ts +5 -1
  78. package/src/commands/fetch.ts +1 -1
  79. package/src/core/agents/events/consume.ts +65 -7
  80. package/src/core/agents/events/emit.ts +2 -1
  81. package/src/core/agents/rules/claim-conflict.ts +10 -2
  82. package/src/core/agents/state/scratch.ts +2 -2
  83. package/src/core/config.ts +1 -1
  84. package/src/core/hooks/cli.ts +4 -8
  85. package/src/core/hooks/effects/index.ts +2 -1
  86. package/src/core/hooks/guard-path.ts +34 -0
  87. package/src/lib/agent-browser/client.ts +1 -1
  88. package/src/lib/browser/client.ts +28 -0
  89. package/src/lib/browser/index.ts +4 -0
  90. package/src/lib/browser/runts.ts +218 -0
  91. package/src/lib/completion/bash.ts +36 -0
  92. package/src/lib/completion/fish.ts +22 -0
  93. package/src/lib/completion/index.ts +12 -3
  94. package/src/lib/completion/resolve.ts +210 -0
  95. package/src/lib/completion/walk.ts +1 -1
  96. package/src/lib/completion/zsh.ts +34 -0
  97. package/src/lib/cookies/client.ts +2 -2
  98. package/src/lib/decision/index.ts +685 -0
  99. package/src/lib/docs-lint.ts +49 -0
  100. package/src/lib/tunnel/gate.ts +1 -1
@@ -28,6 +28,7 @@ import { projectHeartbeats } from "../agents/state/heartbeat-projector.js";
28
28
  import { shellMutationPaths } from "../agents/state/shell-mutation.js";
29
29
  import { captureImages, detectPresence, imageJanitor, playSound, resetSoundCounters, runTurnSummary, scratchArchive, scratchJanitor, scratchRecoveryCue, soundForEvent, syncClaudeSessions, } from "./effects/index.js";
30
30
  import { emit } from "./events/emit.js";
31
+ import { canonicalize } from "./guard-path.js";
31
32
  import { detectHarness } from "./harness/detect.js";
32
33
  import { extractBashCommand, extractToolDescription, normalizeEventName, parsePayload, } from "./harness/parse.js";
33
34
  import { parsePsChainLine, selectAnchorPid } from "./resolve/anchor.js";
@@ -688,7 +689,9 @@ async function main() {
688
689
  }
689
690
  async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, harness) {
690
691
  const toolName = data.tool_name ?? "";
691
- const targets = collectGuardTargets(toolName, data).map((p) => canonicalize(coordRoot, p));
692
+ const targets = collectGuardTargets(toolName, data)
693
+ .map((p) => canonicalize(coordRoot, p))
694
+ .filter((p) => p !== null);
692
695
  if (targets.length === 0)
693
696
  return;
694
697
  const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
@@ -737,15 +740,6 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, harnes
737
740
  /** Canonicalize a path to monorepo-relative form. Absolute paths under
738
741
  * coordRoot get the prefix stripped; relative paths pass through (assumed
739
742
  * already canonical). */
740
- function canonicalize(coordRoot, p) {
741
- if (!p)
742
- return p;
743
- if (p.startsWith(`${coordRoot}/`))
744
- return p.slice(coordRoot.length + 1);
745
- if (p === coordRoot)
746
- return ".";
747
- return p;
748
- }
749
743
  /** Pull the candidate path(s) out of a write-tool payload. Empty array when
750
744
  * the tool isn't a write or no path could be derived. */
751
745
  function collectGuardTargets(toolName, data) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/effects/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEjE;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,SAAI,GACX,IAAI,CAYN;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAW3F;AAOD,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAYrD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAa3D;AAED,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAYpE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAkBzE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,IAAI,CAqBN;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAa1D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAOnD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/hooks/effects/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEjE;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,SAAI,GACX,IAAI,CAYN;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAW3F;AAOD,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAYrD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAa3D;AAED,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAYpE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAkBzE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,IAAI,CAqBN;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAa1D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAOnD"}
@@ -16,6 +16,7 @@ import { existsSync, readdirSync, rmSync } from "node:fs";
16
16
  import os from "node:os";
17
17
  import { join } from "node:path";
18
18
  import { applyDetection } from "../../../lib/presence.js";
19
+ import { resolveBinName } from "../../config.js";
19
20
  export { captureImages, imageJanitor } from "./image-capture.js";
20
21
  /**
21
22
  * Play a notification sound via the cross-platform utility
@@ -119,7 +120,7 @@ export function scratchArchive(repoRoot, owner) {
119
120
  */
120
121
  export function syncClaudeSessions(repoRoot, force) {
121
122
  try {
122
- const bin = join(repoRoot, "bin", "bp");
123
+ const bin = join(repoRoot, "bin", resolveBinName(repoRoot));
123
124
  if (!existsSync(bin))
124
125
  return;
125
126
  const env = {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Canonicalize a write-tool target path for the claim guard.
3
+ *
4
+ * Returns the monorepo-relative path, or `null` when the target lies OUTSIDE the
5
+ * repo (an absolute path not under coordRoot, e.g. a `/tmp` scratchpad or other
6
+ * session-temp file).
7
+ *
8
+ * The claim system is intentionally repo-scoped: it coordinates monorepo files,
9
+ * not arbitrary absolute paths, so the guard skips out-of-repo targets. Skipping
10
+ * them is right on two counts. First, it keeps non-repo paths out of a
11
+ * heartbeat's `files_touched`. Second, the ordering rule compares raw path
12
+ * strings, and an absolute `/tmp/…` sorts before every repo-relative path
13
+ * (`/` = 0x2F < any letter), so without this a scratchpad write would spuriously
14
+ * "block" a legitimately-held repo file. Returning null keeps such paths out of
15
+ * the claim system entirely.
16
+ *
17
+ * Accepted tradeoff: this also means shared out-of-repo files (a user-level
18
+ * memory or plans directory) are not coordinated across agents. The alternative,
19
+ * normalizing every path to one consistent key so those stay coordinated, was
20
+ * rejected as gold-plating a rare, merge-disciplined race in a deadlock-critical
21
+ * path. Coordinate shared state by keeping it in the repo, not out of it.
22
+ *
23
+ * Relative inputs are assumed already-repo-relative (Codex `apply_patch` emits
24
+ * cwd-relative paths). The in-repo check requires the `<root>/` separator, so a
25
+ * sibling dir that merely shares a prefix (`/repo-other` vs `/repo`) is treated
26
+ * as out-of-repo, not stripped.
27
+ */
28
+ export declare function canonicalize(coordRoot: string, p: string): string | null;
29
+ //# sourceMappingURL=guard-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard-path.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/guard-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMxE"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Canonicalize a write-tool target path for the claim guard.
3
+ *
4
+ * Returns the monorepo-relative path, or `null` when the target lies OUTSIDE the
5
+ * repo (an absolute path not under coordRoot, e.g. a `/tmp` scratchpad or other
6
+ * session-temp file).
7
+ *
8
+ * The claim system is intentionally repo-scoped: it coordinates monorepo files,
9
+ * not arbitrary absolute paths, so the guard skips out-of-repo targets. Skipping
10
+ * them is right on two counts. First, it keeps non-repo paths out of a
11
+ * heartbeat's `files_touched`. Second, the ordering rule compares raw path
12
+ * strings, and an absolute `/tmp/…` sorts before every repo-relative path
13
+ * (`/` = 0x2F < any letter), so without this a scratchpad write would spuriously
14
+ * "block" a legitimately-held repo file. Returning null keeps such paths out of
15
+ * the claim system entirely.
16
+ *
17
+ * Accepted tradeoff: this also means shared out-of-repo files (a user-level
18
+ * memory or plans directory) are not coordinated across agents. The alternative,
19
+ * normalizing every path to one consistent key so those stay coordinated, was
20
+ * rejected as gold-plating a rare, merge-disciplined race in a deadlock-critical
21
+ * path. Coordinate shared state by keeping it in the repo, not out of it.
22
+ *
23
+ * Relative inputs are assumed already-repo-relative (Codex `apply_patch` emits
24
+ * cwd-relative paths). The in-repo check requires the `<root>/` separator, so a
25
+ * sibling dir that merely shares a prefix (`/repo-other` vs `/repo`) is treated
26
+ * as out-of-repo, not stripped.
27
+ */
28
+ export function canonicalize(coordRoot, p) {
29
+ if (!p)
30
+ return null;
31
+ if (p === coordRoot)
32
+ return ".";
33
+ if (p.startsWith(`${coordRoot}/`))
34
+ return p.slice(coordRoot.length + 1);
35
+ if (p.startsWith("/"))
36
+ return null; // absolute + not under coordRoot → out-of-repo
37
+ return p; // relative → treat as repo-relative
38
+ }
@@ -66,7 +66,7 @@ export class AgentBrowser {
66
66
  this.cookiesSeeded = true;
67
67
  return;
68
68
  }
69
- const stateFile = this.opts.stateFilePath ?? `/tmp/bp-agent-browser-state-${process.pid}.json`;
69
+ const stateFile = this.opts.stateFilePath ?? `/tmp/harn-agent-browser-state-${process.pid}.json`;
70
70
  writeFileSync(stateFile, JSON.stringify(jarStore, null, 2));
71
71
  this.exec(["state", "load", stateFile], 10_000);
72
72
  this.cookiesSeeded = true;
@@ -1,6 +1,7 @@
1
1
  import { type Page } from "playwright";
2
2
  import type { CookieJar } from "../cookies/index.js";
3
3
  import { type OverflowResult, type WidthResult } from "./layout.js";
4
+ import { type RuntsResult } from "./runts.js";
4
5
  import { type CheckVisibilityOptions, type VisibilityResult } from "./visibility.js";
5
6
  /**
6
7
  * Headless-Chromium wrapper for the `browse` command.
@@ -168,6 +169,19 @@ export declare class Browser {
168
169
  checkOverflow(opts?: {
169
170
  sampleLimit?: number;
170
171
  }): Promise<OverflowResult>;
172
+ /**
173
+ * Scan text blocks for runts — a single word alone on a block's last
174
+ * visual line. Word-count per line via per-word Range rects (the width
175
+ * of the last line is deliberately NOT the signal; see runts.ts).
176
+ */
177
+ checkRunts(opts?: {
178
+ scope?: string | null;
179
+ minChars?: number;
180
+ }): Promise<RuntsResult>;
181
+ /** Inject annotation overlays for runt hits. Used before screenshot. */
182
+ annotateRunts(result: RuntsResult): Promise<void>;
183
+ /** Remove runt annotation overlays. */
184
+ clearRuntsAnnotations(): Promise<void>;
171
185
  /** Inject annotation overlays for width + overflow results. Used before screenshot. */
172
186
  annotateLayout(args: {
173
187
  widths: WidthResult[];
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/client.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { resolve } from "node:path";
4
4
  import { chromium, } from "playwright";
5
5
  import { buildClearLayoutAnnotationsScript, buildLayoutAnnotateScript, buildOverflowCheck, buildWidthCheck, } from "./layout.js";
6
+ import { buildClearRuntsAnnotationsScript, buildRuntsAnnotateScript, buildRuntsCheck, } from "./runts.js";
6
7
  import { buildAnnotateScript, buildClearAnnotationsScript, buildVisibilityCheck, } from "./visibility.js";
7
8
  const DEFAULT_PROFILE = resolve(homedir(), ".cache", "harnery", "browser-profile");
8
9
  export class Browser {
@@ -237,6 +238,25 @@ export class Browser {
237
238
  sampleLimit: opts.sampleLimit ?? 5,
238
239
  });
239
240
  }
241
+ /**
242
+ * Scan text blocks for runts — a single word alone on a block's last
243
+ * visual line. Word-count per line via per-word Range rects (the width
244
+ * of the last line is deliberately NOT the signal; see runts.ts).
245
+ */
246
+ async checkRunts(opts = {}) {
247
+ return await this.currentPage.evaluate(buildRuntsCheck(), {
248
+ scope: opts.scope ?? null,
249
+ minChars: opts.minChars ?? 40,
250
+ });
251
+ }
252
+ /** Inject annotation overlays for runt hits. Used before screenshot. */
253
+ async annotateRunts(result) {
254
+ await this.currentPage.evaluate(buildRuntsAnnotateScript(), { runts: result.runts });
255
+ }
256
+ /** Remove runt annotation overlays. */
257
+ async clearRuntsAnnotations() {
258
+ await this.currentPage.evaluate(buildClearRuntsAnnotationsScript());
259
+ }
240
260
  /** Inject annotation overlays for width + overflow results. Used before screenshot. */
241
261
  async annotateLayout(args) {
242
262
  await this.currentPage.evaluate(buildLayoutAnnotateScript(), args);
@@ -1,5 +1,6 @@
1
1
  export { Browser, type BrowserOptions, type ConsoleEvent, type Diagnostics, type FailedRequest, type NavigateResult, type PageErrorEvent, } from "./client.js";
2
2
  export { captureDevOverlay, type DevOverlayError, type DevOverlayResult, } from "./dev-overlay.js";
3
3
  export type { OverflowElement, OverflowResult, WidthResult, } from "./layout.js";
4
+ export type { RuntHit, RuntsResult, } from "./runts.js";
4
5
  export type { CheckVisibilityOptions, VisibilityResult, VisibilitySample, } from "./visibility.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,eAAe,EACf,cAAc,EACd,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,eAAe,EACf,cAAc,EACd,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,OAAO,EACP,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,44 @@
1
+ export interface RuntHit {
2
+ /** Human label for the owning block: `tag#id` / `tag.class` / `tag`. */
3
+ block: string;
4
+ /** The lone word on the last visual line (trailing punctuation included). */
5
+ word: string;
6
+ /** First ~80 chars of the block's text, for locating it. */
7
+ snippet: string;
8
+ /** Document-relative rect of the runt word (scroll offsets applied). */
9
+ rect: {
10
+ x: number;
11
+ y: number;
12
+ width: number;
13
+ height: number;
14
+ };
15
+ /** Number of visual lines in the block. */
16
+ lines: number;
17
+ }
18
+ export interface RuntsResult {
19
+ /** Text blocks scanned (post length/inline-children filters). */
20
+ scannedBlocks: number;
21
+ /** True when the block sweep hit the internal cap (very large page). */
22
+ truncated: boolean;
23
+ runts: RuntHit[];
24
+ }
25
+ /**
26
+ * Build the JS function passed to `page.evaluate`. `scope` narrows the sweep
27
+ * to one container (null = whole body); `minChars` filters out tiny labels
28
+ * that can't meaningfully wrap (default 40).
29
+ */
30
+ export declare function buildRuntsCheck(): (args: {
31
+ scope: string | null;
32
+ minChars: number;
33
+ }) => RuntsResult;
34
+ /**
35
+ * Annotate runt hits onto the live page before the screenshot. Uses a
36
+ * document-absolute root (not fixed-inset like the other check overlays)
37
+ * because runts are usually below the fold and the rects are document
38
+ * coordinates — this keeps boxes aligned on full-page captures.
39
+ */
40
+ export declare function buildRuntsAnnotateScript(): (args: {
41
+ runts: RuntHit[];
42
+ }) => void;
43
+ export declare function buildClearRuntsAnnotationsScript(): () => void;
44
+ //# sourceMappingURL=runts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runts.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/runts.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,OAAO;IACtB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,CAAC,IAAI,EAAE;IACxC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAK,WAAW,CAkHhB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,OAAO,EAAE,CAAA;CAAE,KAAK,IAAI,CAyC/E;AAED,wBAAgB,gCAAgC,IAAI,MAAM,IAAI,CAI7D"}
@@ -0,0 +1,193 @@
1
+ // Runt detection for `harn browse` — a runt (a.k.a. "widow word") is a single
2
+ // short word sitting alone on the LAST visual line of a text block. Not a
3
+ // page-break issue; it happens mid-page and just looks sloppy.
4
+ //
5
+ // Detection is by COUNTING WORDS on the last visual line (== 1 word), NOT by
6
+ // relative width: a width threshold misses runts in narrow columns (how-to
7
+ // steps, a right-aligned contact box) where one short word still fills a big
8
+ // slice of the skinny column. "Text block" = element with real text whose
9
+ // element children are all inline — a <p>-only pass silently skips
10
+ // pull-quotes, blurbs and captions that ship as <div>/<blockquote>.
11
+ //
12
+ // Atomic tokens (URLs, emails, phone numbers) are excluded — they can't be
13
+ // rebalanced and usually sit on their own line by design. The detector can't
14
+ // catch a runt that is itself one long word; eyeball as a final pass.
15
+ //
16
+ // The word-level line mapping (per-word Range rects grouped by line top) was
17
+ // first built for the paged.js print-book lint; this is the generalized
18
+ // live-page port.
19
+ /**
20
+ * Build the JS function passed to `page.evaluate`. `scope` narrows the sweep
21
+ * to one container (null = whole body); `minChars` filters out tiny labels
22
+ * that can't meaningfully wrap (default 40).
23
+ */
24
+ export function buildRuntsCheck() {
25
+ return ({ scope, minChars }) => {
26
+ const INLINE = new Set([
27
+ "B",
28
+ "I",
29
+ "EM",
30
+ "STRONG",
31
+ "SPAN",
32
+ "A",
33
+ "BR",
34
+ "SMALL",
35
+ "SUP",
36
+ "SUB",
37
+ "U",
38
+ "MARK",
39
+ "CODE",
40
+ "ABBR",
41
+ "TIME",
42
+ "WBR",
43
+ ]);
44
+ const ATOMIC = /@|https?:|\.(com|org|net|io|co)\b|\d-\d{3}/;
45
+ const BLOCK_CAP = 5000;
46
+ const RUNT_CAP = 50;
47
+ const root = scope ? document.querySelector(scope) : document.body;
48
+ if (!root)
49
+ return { scannedBlocks: 0, truncated: false, runts: [] };
50
+ const sx = window.scrollX;
51
+ const sy = window.scrollY;
52
+ const label = (el) => {
53
+ const tag = el.tagName.toLowerCase();
54
+ if (el.id)
55
+ return `${tag}#${el.id}`;
56
+ const cls = typeof el.className === "string" ? el.className.trim() : "";
57
+ if (cls)
58
+ return `${tag}.${cls.split(/\s+/).slice(0, 2).join(".")}`;
59
+ return tag;
60
+ };
61
+ const all = root.querySelectorAll("*");
62
+ const runts = [];
63
+ let scanned = 0;
64
+ let truncated = false;
65
+ for (let i = 0; i < all.length; i++) {
66
+ const el = all[i];
67
+ if (!el)
68
+ continue;
69
+ const text = el.textContent ?? "";
70
+ if (text.trim().length < minChars)
71
+ continue;
72
+ let allInline = true;
73
+ for (const c of el.children) {
74
+ if (!INLINE.has(c.tagName)) {
75
+ allInline = false;
76
+ break;
77
+ }
78
+ }
79
+ if (!allInline)
80
+ continue;
81
+ const elRect = el.getBoundingClientRect();
82
+ if (elRect.width <= 0 || elRect.height <= 0)
83
+ continue;
84
+ if (scanned >= BLOCK_CAP) {
85
+ truncated = true;
86
+ break;
87
+ }
88
+ scanned++;
89
+ // Map every word to its visual line by rounding the Range rect top.
90
+ const lines = new Map();
91
+ const tw = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
92
+ for (let n = tw.nextNode(); n; n = tw.nextNode()) {
93
+ const value = n.nodeValue ?? "";
94
+ for (const m of value.matchAll(/\S+/g)) {
95
+ const r = document.createRange();
96
+ r.setStart(n, m.index);
97
+ r.setEnd(n, m.index + m[0].length);
98
+ const rect = r.getBoundingClientRect();
99
+ if (!rect.width)
100
+ continue;
101
+ const top = Math.round(rect.top);
102
+ const entry = lines.get(top);
103
+ if (entry) {
104
+ entry.words.push(m[0]);
105
+ entry.last = rect;
106
+ }
107
+ else {
108
+ lines.set(top, { words: [m[0]], last: rect });
109
+ }
110
+ }
111
+ }
112
+ const tops = [...lines.keys()].sort((a, b) => a - b);
113
+ if (tops.length < 2)
114
+ continue; // single line, no runt risk
115
+ const lastTop = tops[tops.length - 1];
116
+ const last = lastTop === undefined ? undefined : lines.get(lastTop);
117
+ if (last?.words.length !== 1)
118
+ continue;
119
+ const word = last.words[0] ?? "";
120
+ if (ATOMIC.test(word))
121
+ continue;
122
+ runts.push({
123
+ block: label(el),
124
+ word,
125
+ snippet: text.replace(/\s+/g, " ").trim().slice(0, 80),
126
+ rect: {
127
+ x: Math.round(last.last.x + sx),
128
+ y: Math.round(last.last.y + sy),
129
+ width: Math.round(last.last.width),
130
+ height: Math.round(last.last.height),
131
+ },
132
+ lines: tops.length,
133
+ });
134
+ if (runts.length >= RUNT_CAP) {
135
+ truncated = true;
136
+ break;
137
+ }
138
+ }
139
+ return { scannedBlocks: scanned, truncated, runts };
140
+ };
141
+ }
142
+ /**
143
+ * Annotate runt hits onto the live page before the screenshot. Uses a
144
+ * document-absolute root (not fixed-inset like the other check overlays)
145
+ * because runts are usually below the fold and the rects are document
146
+ * coordinates — this keeps boxes aligned on full-page captures.
147
+ */
148
+ export function buildRuntsAnnotateScript() {
149
+ return ({ runts }) => {
150
+ const ROOT_ID = "__bp-check-runt-annotations__";
151
+ document.getElementById(ROOT_ID)?.remove();
152
+ const root = document.createElement("div");
153
+ root.id = ROOT_ID;
154
+ root.style.cssText =
155
+ "position: absolute; top: 0; left: 0; width: 100%; height: 0; overflow: visible; pointer-events: none; z-index: 2147483646;";
156
+ document.body.appendChild(root);
157
+ for (const hit of runts) {
158
+ const box = document.createElement("div");
159
+ box.style.cssText = `
160
+ position: absolute;
161
+ left: ${hit.rect.x - 3}px;
162
+ top: ${hit.rect.y - 3}px;
163
+ width: ${hit.rect.width + 6}px;
164
+ height: ${hit.rect.height + 6}px;
165
+ border: 2px solid #d946ef;
166
+ box-sizing: border-box;
167
+ background: #d946ef1a;
168
+ pointer-events: none;
169
+ `;
170
+ const tag = document.createElement("div");
171
+ tag.textContent = `runt: …${hit.word} (${hit.block})`;
172
+ tag.style.cssText = `
173
+ position: absolute;
174
+ left: 0;
175
+ top: -18px;
176
+ background: #d946ef;
177
+ color: #fff;
178
+ font: 12px/1.4 -apple-system, system-ui, sans-serif;
179
+ padding: 1px 6px;
180
+ border-radius: 3px;
181
+ white-space: nowrap;
182
+ pointer-events: none;
183
+ `;
184
+ box.appendChild(tag);
185
+ root.appendChild(box);
186
+ }
187
+ };
188
+ }
189
+ export function buildClearRuntsAnnotationsScript() {
190
+ return () => {
191
+ document.getElementById("__bp-check-runt-annotations__")?.remove();
192
+ };
193
+ }
@@ -22,4 +22,19 @@
22
22
  */
23
23
  import type { CommandSpec } from "./walk.js";
24
24
  export declare function generateBash(root: CommandSpec, binName: string): string;
25
+ /**
26
+ * The driver: the parts of the completion that don't depend on the command
27
+ * tree. Walks COMP_WORDS to determine the current command path, then dispatches
28
+ * to subcommand / option / value completion. `fn` is the function-name prefix
29
+ * and `binName` is the CLI name used for the `__complete` callback.
30
+ */
31
+ /**
32
+ * DYNAMIC bash completion: a thin, tree-independent shim. Instead of baking the
33
+ * command tree into case-tables (which go stale on any command/flag change),
34
+ * it hands the live command line to `<bin> __complete-line` on every <Tab> and
35
+ * lets the binary — which always knows its own current tree — answer. Install
36
+ * once; never regenerate. The trailing `\x1f:<n>` line carries the directive
37
+ * bitmask (bit 0 = fall back to file completion).
38
+ */
39
+ export declare function generateBashDynamic(binName: string): string;
25
40
  //# sourceMappingURL=bash.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/bash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAA8B,MAAM,WAAW,CAAC;AAuBzE,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2FvE"}
1
+ {"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/bash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAA8B,MAAM,WAAW,CAAC;AAuBzE,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2FvE;AAqBD;;;;;GAKG;AACH;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0B3D"}
@@ -155,6 +155,41 @@ function bashEscape(s) {
155
155
  * to subcommand / option / value completion. `fn` is the function-name prefix
156
156
  * and `binName` is the CLI name used for the `__complete` callback.
157
157
  */
158
+ /**
159
+ * DYNAMIC bash completion: a thin, tree-independent shim. Instead of baking the
160
+ * command tree into case-tables (which go stale on any command/flag change),
161
+ * it hands the live command line to `<bin> __complete-line` on every <Tab> and
162
+ * lets the binary — which always knows its own current tree — answer. Install
163
+ * once; never regenerate. The trailing `\x1f:<n>` line carries the directive
164
+ * bitmask (bit 0 = fall back to file completion).
165
+ */
166
+ export function generateBashDynamic(binName) {
167
+ const fn = fnPrefix(binName);
168
+ return `# ${binName} bash completion (dynamic; install once, never stale). Do not edit.
169
+ # Source via: eval "$(${binName} completion bash --dynamic)"
170
+ ${fn}() {
171
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
172
+ local raw line directive=0
173
+ local -a cands=()
174
+ raw="$(${binName} __complete-line "$COMP_CWORD" -- "\${COMP_WORDS[@]}" 2>/dev/null)" || return
175
+ while IFS= read -r line; do
176
+ [ -z "$line" ] && continue
177
+ if [ "\${line:0:2}" = $'\\x1f:' ]; then
178
+ directive="\${line:2}"
179
+ else
180
+ cands+=( "\${line%%$'\\t'*}" )
181
+ fi
182
+ done <<< "$raw"
183
+ if (( directive & 1 )); then
184
+ COMPREPLY=( $(compgen -f -- "$cur") )
185
+ return
186
+ fi
187
+ local IFS=$'\\n'
188
+ COMPREPLY=( $(compgen -W "\${cands[*]}" -- "$cur") )
189
+ }
190
+ complete -F ${fn} ${binName}
191
+ `;
192
+ }
158
193
  function bashDriver(fn, binName) {
159
194
  return `${fn}() {
160
195
  local cur prev words cword
@@ -12,5 +12,15 @@
12
12
  * giving us per-tab callbacks for free.
13
13
  */
14
14
  import type { CommandSpec } from "./walk.js";
15
+ /**
16
+ * DYNAMIC fish completion: a thin, tree-independent shim that calls
17
+ * `<bin> __complete-line` on every <Tab> (see bash.ts generateBashDynamic for
18
+ * the rationale). Fish renders `value\tdescription` lines natively, so the
19
+ * helper just strips the trailing `\x1f:<n>` directive line. Note: fish's
20
+ * declarative `complete -a` cannot switch to file completion from inside the
21
+ * helper, so the File directive is a no-op here — use static fish completion
22
+ * (`completion fish`) if you need file fallback on a path-valued positional.
23
+ */
24
+ export declare function generateFishDynamic(binName: string): string;
15
25
  export declare function generateFish(root: CommandSpec, binName: string): string;
16
26
  //# sourceMappingURL=fish.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fish.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/fish.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAuD7C,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAgEvE"}
1
+ {"version":3,"file":"fish.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/fish.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAuD7C;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAW3D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAgEvE"}
@@ -55,6 +55,27 @@ function valueAction(opt, binName) {
55
55
  }
56
56
  return { flag: "-r", arg: "" };
57
57
  }
58
+ /**
59
+ * DYNAMIC fish completion: a thin, tree-independent shim that calls
60
+ * `<bin> __complete-line` on every <Tab> (see bash.ts generateBashDynamic for
61
+ * the rationale). Fish renders `value\tdescription` lines natively, so the
62
+ * helper just strips the trailing `\x1f:<n>` directive line. Note: fish's
63
+ * declarative `complete -a` cannot switch to file completion from inside the
64
+ * helper, so the File directive is a no-op here — use static fish completion
65
+ * (`completion fish`) if you need file fallback on a path-valued positional.
66
+ */
67
+ export function generateFishDynamic(binName) {
68
+ const fn = `__${binName.replace(/[^a-zA-Z0-9_]/g, "_")}_complete`;
69
+ return `# ${binName} fish completion (dynamic; install once, never stale). Do not edit.
70
+ # Source via: ${binName} completion fish --dynamic | source
71
+ function ${fn}
72
+ set -l toks (commandline -opc)
73
+ set -l cur (commandline -ct)
74
+ ${binName} __complete-line (count $toks) -- $toks $cur 2>/dev/null | string match -rv '^\\x1f:'
75
+ end
76
+ complete -c ${binName} -f -a '(${fn})'
77
+ `;
78
+ }
58
79
  export function generateFish(root, binName) {
59
80
  const out = [];
60
81
  out.push(`# ${binName} fish completion, generated by \`${binName} completion fish\`. Do not edit.`);
@@ -1,5 +1,6 @@
1
- export { generateBash } from "./bash.js";
2
- export { generateFish } from "./fish.js";
1
+ export { generateBash, generateBashDynamic } from "./bash.js";
2
+ export { generateFish, generateFishDynamic } from "./fish.js";
3
+ export { type Candidate, type CompletionProviderRunner, type CompletionResult, DIRECTIVE_PREFIX, Directive, encodeResult, resolveCompletions, } from "./resolve.js";
3
4
  export { type CommandSpec, type CompletionContextLookup, type OptionSpec, type PositionalSpec, walkProgram, } from "./walk.js";
4
- export { generateZsh } from "./zsh.js";
5
+ export { generateZsh, generateZshDynamic } from "./zsh.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,uBAAuB,EAC5B,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,WAAW,GACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EACL,KAAK,SAAS,EACd,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,EACrB,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,uBAAuB,EAC5B,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,WAAW,GACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC"}
@@ -1,4 +1,5 @@
1
- export { generateBash } from "./bash.js";
2
- export { generateFish } from "./fish.js";
1
+ export { generateBash, generateBashDynamic } from "./bash.js";
2
+ export { generateFish, generateFishDynamic } from "./fish.js";
3
+ export { DIRECTIVE_PREFIX, Directive, encodeResult, resolveCompletions, } from "./resolve.js";
3
4
  export { walkProgram, } from "./walk.js";
4
- export { generateZsh } from "./zsh.js";
5
+ export { generateZsh, generateZshDynamic } from "./zsh.js";