harnery 0.3.2 → 0.5.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 (68) hide show
  1. package/README.md +20 -7
  2. package/dist/commander.js +2 -2
  3. package/dist/commands/completion.d.ts.map +1 -1
  4. package/dist/commands/completion.js +48 -10
  5. package/dist/commands/deinit.d.ts +51 -0
  6. package/dist/commands/deinit.d.ts.map +1 -0
  7. package/dist/commands/{uninstall.js → deinit.js} +83 -14
  8. package/dist/commands/doctor.d.ts.map +1 -1
  9. package/dist/commands/doctor.js +47 -9
  10. package/dist/commands/init.d.ts +2 -21
  11. package/dist/commands/init.d.ts.map +1 -1
  12. package/dist/commands/init.js +3 -15
  13. package/dist/core/agents/render/session-context.d.ts +12 -2
  14. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  15. package/dist/core/agents/render/session-context.js +75 -36
  16. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  17. package/dist/core/agents/rules/claim-conflict.js +38 -12
  18. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  19. package/dist/core/agents/state/heartbeat-writer.js +7 -1
  20. package/dist/core/config.d.ts +7 -0
  21. package/dist/core/config.d.ts.map +1 -1
  22. package/dist/core/config.js +13 -0
  23. package/dist/core/hooks/cli.js +4 -10
  24. package/dist/core/hooks/guard-path.d.ts +29 -0
  25. package/dist/core/hooks/guard-path.d.ts.map +1 -0
  26. package/dist/core/hooks/guard-path.js +38 -0
  27. package/dist/core/hooks/harness/wiring.d.ts +85 -0
  28. package/dist/core/hooks/harness/wiring.d.ts.map +1 -0
  29. package/dist/core/hooks/harness/wiring.js +137 -0
  30. package/dist/lib/completion/bash.d.ts +15 -0
  31. package/dist/lib/completion/bash.d.ts.map +1 -1
  32. package/dist/lib/completion/bash.js +35 -0
  33. package/dist/lib/completion/fish.d.ts +10 -0
  34. package/dist/lib/completion/fish.d.ts.map +1 -1
  35. package/dist/lib/completion/fish.js +21 -0
  36. package/dist/lib/completion/index.d.ts +4 -3
  37. package/dist/lib/completion/index.d.ts.map +1 -1
  38. package/dist/lib/completion/index.js +4 -3
  39. package/dist/lib/completion/resolve.d.ts +52 -0
  40. package/dist/lib/completion/resolve.d.ts.map +1 -0
  41. package/dist/lib/completion/resolve.js +171 -0
  42. package/dist/lib/completion/zsh.d.ts +8 -0
  43. package/dist/lib/completion/zsh.d.ts.map +1 -1
  44. package/dist/lib/completion/zsh.js +33 -0
  45. package/dist/lib/docs-lint.d.ts.map +1 -1
  46. package/dist/lib/docs-lint.js +6 -0
  47. package/package.json +1 -1
  48. package/schemas/config.schema.json +4 -0
  49. package/src/commander.ts +2 -2
  50. package/src/commands/completion.ts +62 -9
  51. package/src/commands/{uninstall.ts → deinit.ts} +107 -15
  52. package/src/commands/doctor.ts +47 -9
  53. package/src/commands/init.ts +12 -39
  54. package/src/core/agents/render/session-context.ts +74 -34
  55. package/src/core/agents/rules/claim-conflict.ts +37 -12
  56. package/src/core/agents/state/heartbeat-writer.ts +8 -1
  57. package/src/core/config.ts +21 -0
  58. package/src/core/hooks/cli.ts +4 -8
  59. package/src/core/hooks/guard-path.ts +34 -0
  60. package/src/core/hooks/harness/wiring.ts +185 -0
  61. package/src/lib/completion/bash.ts +36 -0
  62. package/src/lib/completion/fish.ts +22 -0
  63. package/src/lib/completion/index.ts +12 -3
  64. package/src/lib/completion/resolve.ts +210 -0
  65. package/src/lib/completion/zsh.ts +34 -0
  66. package/src/lib/docs-lint.ts +5 -0
  67. package/dist/commands/uninstall.d.ts +0 -22
  68. package/dist/commands/uninstall.d.ts.map +0 -1
@@ -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";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Runtime completion resolver — the authoritative, in-process answer to
3
+ * "what should I suggest at this cursor position?".
4
+ *
5
+ * The static bash/zsh/fish generators bake the whole command tree into shell
6
+ * case-tables, which go stale the moment a command/flag is added (the script
7
+ * must be regenerated + reinstalled). The DYNAMIC completion path instead
8
+ * installs a thin, tree-independent shim that, on every <Tab>, hands the live
9
+ * command line back to the binary and calls THIS function. Because the binary
10
+ * always knows its own current command tree, a dynamic shim never goes stale —
11
+ * install once, ever.
12
+ *
13
+ * This mirrors the path-walking the static bash driver does, but in one place
14
+ * and in TypeScript, so all three shells share identical behavior. It is
15
+ * deliberately decoupled from Commander internals via the CommandSpec tree
16
+ * (walkProgram), exactly like the static generators.
17
+ */
18
+ import type { Command } from "commander";
19
+ import { type CompletionContextLookup } from "./walk.js";
20
+ /** Bitmask of post-processing hints for the shell shim (Cobra-style). */
21
+ export declare const Directive: {
22
+ /** Use the returned candidates as completion values. */
23
+ readonly Default: 0;
24
+ /** No candidates apply here — the shell should fall back to file completion. */
25
+ readonly File: 1;
26
+ };
27
+ export interface Candidate {
28
+ value: string;
29
+ description?: string;
30
+ }
31
+ export interface CompletionResult {
32
+ candidates: Candidate[];
33
+ directive: number;
34
+ }
35
+ /** Provider runner injected by the host CLI (same contract as `__complete`). */
36
+ export type CompletionProviderRunner = (provider: string, partial: string) => Promise<string[]>;
37
+ /**
38
+ * Compute completion candidates for `words` with the cursor at `cword`.
39
+ * `words[0]` is the bin name; `words[cword]` is the (possibly empty) token
40
+ * being completed. The shell shim does the final prefix-filtering against that
41
+ * token, so this returns the full candidate set for the slot.
42
+ */
43
+ export declare function resolveCompletions(program: Command, words: string[], cword: number, lookup: CompletionContextLookup, runProvider: CompletionProviderRunner): Promise<CompletionResult>;
44
+ /** Sentinel prefix for the trailing directive line in the wire protocol. */
45
+ export declare const DIRECTIVE_PREFIX = "\u001F:";
46
+ /**
47
+ * Serialize a result for the shell shim: one `value\tdescription` line per
48
+ * candidate, then a final `\x1f:<directive>` line. The \x1f (unit separator)
49
+ * prefix makes the directive line unambiguous against real candidate values.
50
+ */
51
+ export declare function encodeResult(result: CompletionResult): string;
52
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAoB,KAAK,uBAAuB,EAAe,MAAM,WAAW,CAAC;AAExF,yEAAyE;AACzE,eAAO,MAAM,SAAS;IACpB,wDAAwD;;IAExD,gFAAgF;;CAExE,CAAC;AAEX,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,gFAAgF;AAChF,MAAM,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAiGhG;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EAAE,EACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,uBAAuB,EAC/B,WAAW,EAAE,wBAAwB,GACpC,OAAO,CAAC,gBAAgB,CAAC,CA4C3B;AAED,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,YAAU,CAAC;AAExC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAM7D"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Runtime completion resolver — the authoritative, in-process answer to
3
+ * "what should I suggest at this cursor position?".
4
+ *
5
+ * The static bash/zsh/fish generators bake the whole command tree into shell
6
+ * case-tables, which go stale the moment a command/flag is added (the script
7
+ * must be regenerated + reinstalled). The DYNAMIC completion path instead
8
+ * installs a thin, tree-independent shim that, on every <Tab>, hands the live
9
+ * command line back to the binary and calls THIS function. Because the binary
10
+ * always knows its own current command tree, a dynamic shim never goes stale —
11
+ * install once, ever.
12
+ *
13
+ * This mirrors the path-walking the static bash driver does, but in one place
14
+ * and in TypeScript, so all three shells share identical behavior. It is
15
+ * deliberately decoupled from Commander internals via the CommandSpec tree
16
+ * (walkProgram), exactly like the static generators.
17
+ */
18
+ import { walkProgram } from "./walk.js";
19
+ /** Bitmask of post-processing hints for the shell shim (Cobra-style). */
20
+ export const Directive = {
21
+ /** Use the returned candidates as completion values. */
22
+ Default: 0,
23
+ /** No candidates apply here — the shell should fall back to file completion. */
24
+ File: 1,
25
+ };
26
+ function buildTables(root) {
27
+ const subcommandsByPath = new Map();
28
+ const optionsByPath = new Map();
29
+ const positionalsByPath = new Map();
30
+ const walk = (s) => {
31
+ subcommandsByPath.set(s.path, s.subcommands);
32
+ optionsByPath.set(s.path, s.options);
33
+ positionalsByPath.set(s.path, s.positionals);
34
+ for (const sub of s.subcommands)
35
+ walk(sub);
36
+ };
37
+ walk(root);
38
+ return { subcommandsByPath, optionsByPath, positionalsByPath };
39
+ }
40
+ function optionAt(tables, path, flag) {
41
+ const opts = tables.optionsByPath.get(path) ?? [];
42
+ return opts.find((o) => o.long === flag || o.short === flag);
43
+ }
44
+ function isKnownSubcommand(tables, path, name) {
45
+ return (tables.subcommandsByPath.get(path) ?? []).some((c) => c.name === name);
46
+ }
47
+ /**
48
+ * Walk the words before the cursor to determine the current command path,
49
+ * skipping options and the values they consume (mirrors the static driver).
50
+ * Returns the resolved command path ("" = root).
51
+ */
52
+ function resolvePath(tables, words, cword) {
53
+ let path = "";
54
+ let i = 1; // words[0] is the bin name
55
+ while (i < cword) {
56
+ const w = words[i] ?? "";
57
+ if (w.startsWith("-")) {
58
+ const opt = optionAt(tables, path, w);
59
+ if (opt?.takesValue)
60
+ i += 1; // skip the option's value
61
+ }
62
+ else if (isKnownSubcommand(tables, path, w)) {
63
+ path = path ? `${path} ${w}` : w;
64
+ }
65
+ i += 1;
66
+ }
67
+ return path;
68
+ }
69
+ /** Count positional args consumed at the resolved path (non-option, non-subcommand words). */
70
+ function positionalIndex(tables, words, cword) {
71
+ let seen = "";
72
+ let after = 0;
73
+ let j = 1;
74
+ while (j < cword) {
75
+ const w = words[j] ?? "";
76
+ if (w.startsWith("-")) {
77
+ const opt = optionAt(tables, seen, w);
78
+ if (opt?.takesValue)
79
+ j += 1;
80
+ }
81
+ else if (isKnownSubcommand(tables, seen, w)) {
82
+ seen = seen ? `${seen} ${w}` : w;
83
+ }
84
+ else {
85
+ after += 1;
86
+ }
87
+ j += 1;
88
+ }
89
+ return after;
90
+ }
91
+ /** Resolve the value source (enum / dynamic-provider / file) for a value slot. */
92
+ async function valueCandidates(source, partial, runProvider) {
93
+ if (source.dynamicProvider) {
94
+ try {
95
+ const values = await runProvider(source.dynamicProvider, partial);
96
+ return { candidates: values.map((value) => ({ value })), directive: Directive.Default };
97
+ }
98
+ catch {
99
+ return { candidates: [], directive: Directive.File };
100
+ }
101
+ }
102
+ if (source.valueChoices && source.valueChoices.length > 0) {
103
+ return {
104
+ candidates: source.valueChoices.map((value) => ({ value })),
105
+ directive: Directive.Default,
106
+ };
107
+ }
108
+ // A value is expected but we have nothing to suggest → let the shell try files.
109
+ return { candidates: [], directive: Directive.File };
110
+ }
111
+ /**
112
+ * Compute completion candidates for `words` with the cursor at `cword`.
113
+ * `words[0]` is the bin name; `words[cword]` is the (possibly empty) token
114
+ * being completed. The shell shim does the final prefix-filtering against that
115
+ * token, so this returns the full candidate set for the slot.
116
+ */
117
+ export async function resolveCompletions(program, words, cword, lookup, runProvider) {
118
+ const tables = buildTables(walkProgram(program, lookup));
119
+ const path = resolvePath(tables, words, cword);
120
+ const cur = words[cword] ?? "";
121
+ const prev = cword > 0 ? (words[cword - 1] ?? "") : "";
122
+ // Case 1: previous word is an option that takes a value → complete the value.
123
+ if (prev.startsWith("-")) {
124
+ const opt = optionAt(tables, path, prev);
125
+ if (opt?.takesValue)
126
+ return valueCandidates(opt, cur, runProvider);
127
+ }
128
+ // Case 2: completing an option name (cur starts with "-").
129
+ if (cur.startsWith("-")) {
130
+ const opts = tables.optionsByPath.get(path) ?? [];
131
+ const candidates = [];
132
+ for (const o of opts) {
133
+ if (o.long)
134
+ candidates.push({ value: o.long, description: o.description });
135
+ if (o.short)
136
+ candidates.push({ value: o.short, description: o.description });
137
+ }
138
+ return { candidates, directive: Directive.Default };
139
+ }
140
+ // Case 3: subcommands available at this path → suggest them.
141
+ const subs = tables.subcommandsByPath.get(path) ?? [];
142
+ if (subs.length > 0) {
143
+ return {
144
+ candidates: subs.map((c) => ({ value: c.name, description: c.description })),
145
+ directive: Directive.Default,
146
+ };
147
+ }
148
+ // Case 4: positional value slot.
149
+ const positionals = tables.positionalsByPath.get(path) ?? [];
150
+ const idx = positionalIndex(tables, words, cword);
151
+ const pos = positionals[idx] ??
152
+ (positionals[positionals.length - 1]?.variadic
153
+ ? positionals[positionals.length - 1]
154
+ : undefined);
155
+ if (pos)
156
+ return valueCandidates(pos, cur, runProvider);
157
+ // Nothing structured to suggest → file completion.
158
+ return { candidates: [], directive: Directive.File };
159
+ }
160
+ /** Sentinel prefix for the trailing directive line in the wire protocol. */
161
+ export const DIRECTIVE_PREFIX = "\x1f:";
162
+ /**
163
+ * Serialize a result for the shell shim: one `value\tdescription` line per
164
+ * candidate, then a final `\x1f:<directive>` line. The \x1f (unit separator)
165
+ * prefix makes the directive line unambiguous against real candidate values.
166
+ */
167
+ export function encodeResult(result) {
168
+ const lines = result.candidates.map((c) => c.description ? `${c.value}\t${c.description}` : c.value);
169
+ lines.push(`${DIRECTIVE_PREFIX}${result.directive}`);
170
+ return `${lines.join("\n")}\n`;
171
+ }
@@ -10,4 +10,12 @@
10
10
  */
11
11
  import type { CommandSpec } from "./walk.js";
12
12
  export declare function generateZsh(root: CommandSpec, binName: string): string;
13
+ /**
14
+ * DYNAMIC zsh completion: a thin, tree-independent shim that calls
15
+ * `<bin> __complete-line` on every <Tab> (see bash.ts generateBashDynamic for
16
+ * the rationale). Candidate lines are `value\tdescription`; the shim converts
17
+ * the tab to `:` so `_describe` renders descriptions. The trailing `\x1f:<n>`
18
+ * line carries the directive (bit 0 = file fallback → `_files`).
19
+ */
20
+ export declare function generateZshDynamic(binName: string): string;
13
21
  //# sourceMappingURL=zsh.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"zsh.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/zsh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAA8B,MAAM,WAAW,CAAC;AAwBzE,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAsGtE"}
1
+ {"version":3,"file":"zsh.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/zsh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAA8B,MAAM,WAAW,CAAC;AAwBzE,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAsGtE;AAOD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAyB1D"}
@@ -129,6 +129,39 @@ function truncate(s, n) {
129
129
  return "";
130
130
  return s.length > n ? `${s.slice(0, n - 1)}…` : s;
131
131
  }
132
+ /**
133
+ * DYNAMIC zsh completion: a thin, tree-independent shim that calls
134
+ * `<bin> __complete-line` on every <Tab> (see bash.ts generateBashDynamic for
135
+ * the rationale). Candidate lines are `value\tdescription`; the shim converts
136
+ * the tab to `:` so `_describe` renders descriptions. The trailing `\x1f:<n>`
137
+ * line carries the directive (bit 0 = file fallback → `_files`).
138
+ */
139
+ export function generateZshDynamic(binName) {
140
+ const fn = fnPrefix(binName);
141
+ return `#compdef ${binName}
142
+ # ${binName} zsh completion (dynamic; install once, never stale). Do not edit.
143
+ ${fn}() {
144
+ emulate -L zsh
145
+ local line directive=0
146
+ local -a raw display
147
+ raw=( "\${(@f)$(${binName} __complete-line $((CURRENT-1)) -- "\${words[@]}" 2>/dev/null)}" )
148
+ for line in $raw; do
149
+ [ -z "$line" ] && continue
150
+ if [[ "$line" == $'\\x1f:'* ]]; then
151
+ directive="\${line#$'\\x1f:'}"
152
+ else
153
+ display+=( "\${line//$'\\t'/:}" )
154
+ fi
155
+ done
156
+ if (( directive & 1 )); then
157
+ _files
158
+ return
159
+ fi
160
+ _describe -t values 'completions' display
161
+ }
162
+ compdef ${fn} ${binName}
163
+ `;
164
+ }
132
165
  /**
133
166
  * The driver walks `$words` to determine the current command path, then
134
167
  * dispatches to subcommand / option / value completion. `fn` is the
@@ -1 +1 @@
1
- {"version":3,"file":"docs-lint.d.ts","sourceRoot":"","sources":["../../src/lib/docs-lint.ts"],"names":[],"mappings":"AAYA,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C,GAAG,IAAI,CAIP;AAaD;;;;;;GAMG;AAEH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3C,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAkVD,wBAAsB,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAoBlE"}
1
+ {"version":3,"file":"docs-lint.d.ts","sourceRoot":"","sources":["../../src/lib/docs-lint.ts"],"names":[],"mappings":"AAYA,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C,GAAG,IAAI,CAIP;AAaD;;;;;;GAMG;AAEH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3C,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAuVD,wBAAsB,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAoBlE"}
@@ -204,6 +204,12 @@ function checkNamingConvention(repoName, _repoPath, files) {
204
204
  // Allowlisted names
205
205
  if (ROOT_FILE_ALLOWLIST.has(name))
206
206
  continue;
207
+ // Leading-underscore files are deliberate templates / meta files
208
+ // (e.g. _template.md), not content. The underscore is a convention
209
+ // marking "copy me, don't read me as a real doc", so exempt them
210
+ // from naming discipline rather than forcing kebab-case.
211
+ if (name.startsWith("_"))
212
+ continue;
207
213
  // Dated files (audits/issues)
208
214
  if (DATED_FILE_PATTERN.test(name))
209
215
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "Multi-agent coordination + harness adapters + portable CLI utilities for Claude Code / Cursor / Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -19,6 +19,10 @@
19
19
  "default": "harn",
20
20
  "description": "Host CLI bin name used in agent-facing strings (council prompts, end-of-turn nudges, command help). The coord binaries + web UI run as harnery and read this back since they can't see a consumer CLI's name. Stamped by `harn init` for a consumer. Resolution: HARNERY_BIN env -> this field -> 'harn'."
21
21
  },
22
+ "hooksSetupHint": {
23
+ "type": "string",
24
+ "description": "Host-specific command that (re)installs the project's git hooks (e.g. 'scripts/setup-hooks.sh'). Surfaced verbatim in the SessionStart 'commit guard not wired' nudge. harnery doesn't own git-hook installation (each host wires its own pre-commit to invoke `agent-coord verdict`) and the path/command is host-specific, so the host declares it here. Unset -> a generic, host-agnostic hint."
25
+ },
22
26
  "coord": {
23
27
  "type": "object",
24
28
  "additionalProperties": false,
package/src/commander.ts CHANGED
@@ -27,6 +27,7 @@ import { registerCompletionCommand } from "./commands/completion.ts";
27
27
  import { registerConfigGetCommand } from "./commands/config-get.ts";
28
28
  import { registerContextCommand } from "./commands/context.ts";
29
29
  import { registerCookiesCommand } from "./commands/cookies.ts";
30
+ import { registerDeinitCommand } from "./commands/deinit.ts";
30
31
  import { registerDocsCommand } from "./commands/docs.ts";
31
32
  import { registerDoctorCommand } from "./commands/doctor.ts";
32
33
  import { registerEditBatchCommand } from "./commands/edit-batch.ts";
@@ -45,7 +46,6 @@ import { registerSyncCommand } from "./commands/sync.ts";
45
46
  import { registerSectionCommand, registerTocCommand } from "./commands/toc.ts";
46
47
  import { registerTokensCommand } from "./commands/tokens.ts";
47
48
  import { registerTunnelCommand } from "./commands/tunnel.ts";
48
- import { registerUninstallCommand } from "./commands/uninstall.ts";
49
49
  import { registerWebCommand } from "./commands/web.ts";
50
50
 
51
51
  export interface HarneryContextOpts {
@@ -230,7 +230,7 @@ export function createHarneryProgram(opts: HarneryContextOpts = {}): Command {
230
230
  registerAgentsCommand(program, emit);
231
231
  registerDoctorCommand(program, emit);
232
232
  registerInitCommand(program, emit, opts.binName);
233
- registerUninstallCommand(program, emit);
233
+ registerDeinitCommand(program, emit, opts.binName);
234
234
  registerBackupCommand(program, emit);
235
235
  registerSyncCommand(program, emit);
236
236
  if (include("web")) registerWebCommand(program, emit);
@@ -4,9 +4,15 @@ import type { Command } from "commander";
4
4
  import type { EmitContext, HarneryProgramContext } from "../commander.ts";
5
5
  import {
6
6
  type CompletionContextLookup,
7
+ type CompletionProviderRunner,
8
+ encodeResult,
7
9
  generateBash,
10
+ generateBashDynamic,
8
11
  generateFish,
12
+ generateFishDynamic,
9
13
  generateZsh,
14
+ generateZshDynamic,
15
+ resolveCompletions,
10
16
  walkProgram,
11
17
  } from "../lib/completion/index.ts";
12
18
 
@@ -39,27 +45,39 @@ export function registerCompletionCommand(
39
45
  "Shell tab-completion. Emit a script per shell or install to the standard location.",
40
46
  );
41
47
 
48
+ const dynamicHint =
49
+ "Emit a thin shim that calls the binary at tab-time (never goes stale; install once)";
50
+
42
51
  root
43
52
  .command("bash")
44
53
  .description("Emit bash completion script to stdout")
45
- .action(() => {
46
- const out = generateBash(walkProgram(program, lookup), program.name());
54
+ .option("--dynamic", dynamicHint)
55
+ .action((opts: { dynamic?: boolean }) => {
56
+ const out = opts.dynamic
57
+ ? generateBashDynamic(program.name())
58
+ : generateBash(walkProgram(program, lookup), program.name());
47
59
  process.stdout.write(out); // raw bytes: shell completion scripts must be unframed (consumer evals stdout).
48
60
  });
49
61
 
50
62
  root
51
63
  .command("zsh")
52
64
  .description("Emit zsh completion script to stdout")
53
- .action(() => {
54
- const out = generateZsh(walkProgram(program, lookup), program.name());
65
+ .option("--dynamic", dynamicHint)
66
+ .action((opts: { dynamic?: boolean }) => {
67
+ const out = opts.dynamic
68
+ ? generateZshDynamic(program.name())
69
+ : generateZsh(walkProgram(program, lookup), program.name());
55
70
  process.stdout.write(out); // raw bytes: shell completion scripts must be unframed (consumer evals stdout).
56
71
  });
57
72
 
58
73
  root
59
74
  .command("fish")
60
75
  .description("Emit fish completion script to stdout")
61
- .action(() => {
62
- const out = generateFish(walkProgram(program, lookup), program.name());
76
+ .option("--dynamic", dynamicHint)
77
+ .action((opts: { dynamic?: boolean }) => {
78
+ const out = opts.dynamic
79
+ ? generateFishDynamic(program.name())
80
+ : generateFish(walkProgram(program, lookup), program.name());
63
81
  process.stdout.write(out); // raw bytes: shell completion scripts must be unframed (consumer evals stdout).
64
82
  });
65
83
 
@@ -69,6 +87,7 @@ export function registerCompletionCommand(
69
87
  .option("--shell <name>", "bash | zsh | fish (default: auto-detect from $SHELL)")
70
88
  .option("--path <file>", "Override destination path")
71
89
  .option("--print-path", "Print the destination path and exit (no write)")
90
+ .option("--dynamic", `${dynamicHint} (recommended)`)
72
91
  .action(async (opts: InstallOpts) => {
73
92
  await installCompletion(program, opts, lookup);
74
93
  });
@@ -92,12 +111,39 @@ export function registerCompletionCommand(
92
111
  });
93
112
  // Keep TS happy that the variable is used.
94
113
  void hidden;
114
+
115
+ // Hidden internal entry for DYNAMIC completion: the thin shim passes the live
116
+ // command line (cursor index + all words after `--`) and we compute the full
117
+ // candidate set from the live command tree. `--` stops option parsing so
118
+ // words like `-h` reach the variadic instead of being read as our flags.
119
+ const hiddenLine = program
120
+ .command("__complete-line <cword> [words...]", { hidden: true })
121
+ .description("Internal: full-line completion callback for the dynamic shell shim")
122
+ .allowUnknownOption(true)
123
+ .allowExcessArguments(true)
124
+ .action(async (cword: string, words: string[] | undefined) => {
125
+ try {
126
+ const result = await resolveCompletions(
127
+ program,
128
+ words ?? [],
129
+ Number.parseInt(cword, 10) || 0,
130
+ lookup,
131
+ runProvider as CompletionProviderRunner,
132
+ );
133
+ process.stdout.write(encodeResult(result)); // lint-ok-emission: shell callback; the encoded candidate/directive stream is the contract with the shim.
134
+ } catch {
135
+ // Never break the user's tab: emit just the file-fallback directive.
136
+ process.stdout.write("\x1f:1\n"); // lint-ok-emission: shell callback fallback directive.
137
+ }
138
+ });
139
+ void hiddenLine;
95
140
  }
96
141
 
97
142
  interface InstallOpts {
98
143
  shell?: string;
99
144
  path?: string;
100
145
  printPath?: boolean;
146
+ dynamic?: boolean;
101
147
  }
102
148
 
103
149
  async function installCompletion(
@@ -117,16 +163,23 @@ async function installCompletion(
117
163
  return;
118
164
  }
119
165
 
166
+ const name = program.name();
120
167
  let content: string;
121
168
  switch (shell) {
122
169
  case "bash":
123
- content = generateBash(walkProgram(program, lookup), program.name());
170
+ content = opts.dynamic
171
+ ? generateBashDynamic(name)
172
+ : generateBash(walkProgram(program, lookup), name);
124
173
  break;
125
174
  case "zsh":
126
- content = generateZsh(walkProgram(program, lookup), program.name());
175
+ content = opts.dynamic
176
+ ? generateZshDynamic(name)
177
+ : generateZsh(walkProgram(program, lookup), name);
127
178
  break;
128
179
  case "fish":
129
- content = generateFish(walkProgram(program, lookup), program.name());
180
+ content = opts.dynamic
181
+ ? generateFishDynamic(name)
182
+ : generateFish(walkProgram(program, lookup), name);
130
183
  break;
131
184
  default:
132
185
  process.stderr.write(`Unknown shell: ${shell}\n`); // lint-ok-emission: install-time error, see above.