harnery 0.6.0 → 0.7.1

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 (137) hide show
  1. package/README.md +16 -6
  2. package/dist/commander.d.ts +19 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +2 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +51 -4
  7. package/dist/commands/deinit.d.ts.map +1 -1
  8. package/dist/commands/deinit.js +4 -0
  9. package/dist/commands/devtools.d.ts +4 -0
  10. package/dist/commands/devtools.d.ts.map +1 -0
  11. package/dist/commands/devtools.js +239 -0
  12. package/dist/commands/docs.d.ts.map +1 -1
  13. package/dist/commands/docs.js +69 -1
  14. package/dist/commands/doctor.js +12 -4
  15. package/dist/commands/env.d.ts.map +1 -1
  16. package/dist/commands/env.js +3 -63
  17. package/dist/commands/init.d.ts +1 -0
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +54 -14
  20. package/dist/commands/scratch.js +1 -1
  21. package/dist/commands/tunnel.d.ts.map +1 -1
  22. package/dist/commands/tunnel.js +273 -62
  23. package/dist/commands/web-fetch.js +1 -1
  24. package/dist/core/agents/cli.js +48 -0
  25. package/dist/core/agents/coord-client.d.ts.map +1 -1
  26. package/dist/core/agents/coord-client.js +32 -8
  27. package/dist/core/agents/events/emit.d.ts.map +1 -1
  28. package/dist/core/agents/events/emit.js +4 -0
  29. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  30. package/dist/core/agents/rules/claim-conflict.js +16 -5
  31. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  32. package/dist/core/agents/state/heartbeat-projector.js +10 -3
  33. package/dist/core/config.d.ts +10 -0
  34. package/dist/core/config.d.ts.map +1 -1
  35. package/dist/core/config.js +13 -0
  36. package/dist/core/hooks/cli.js +3 -3
  37. package/dist/core/hooks/effects/index.d.ts +11 -7
  38. package/dist/core/hooks/effects/index.d.ts.map +1 -1
  39. package/dist/core/hooks/effects/index.js +15 -18
  40. package/dist/core/hooks/events/emit.d.ts.map +1 -1
  41. package/dist/core/hooks/events/emit.js +4 -0
  42. package/dist/core/hooks/events/rotate.d.ts +43 -0
  43. package/dist/core/hooks/events/rotate.d.ts.map +1 -0
  44. package/dist/core/hooks/events/rotate.js +142 -0
  45. package/dist/core/hooks/harness/events.d.ts +11 -1
  46. package/dist/core/hooks/harness/events.d.ts.map +1 -1
  47. package/dist/core/hooks/harness/events.js +22 -3
  48. package/dist/core/hooks/harness/wiring.d.ts +8 -0
  49. package/dist/core/hooks/harness/wiring.d.ts.map +1 -1
  50. package/dist/core/hooks/harness/wiring.js +34 -5
  51. package/dist/core/scratch/index.d.ts.map +1 -0
  52. package/dist/{lib → core}/scratch/index.js +2 -2
  53. package/dist/lib/devtools.d.ts +178 -0
  54. package/dist/lib/devtools.d.ts.map +1 -0
  55. package/dist/lib/devtools.js +1328 -0
  56. package/dist/lib/docs-frontmatter-migrate.d.ts +33 -0
  57. package/dist/lib/docs-frontmatter-migrate.d.ts.map +1 -0
  58. package/dist/lib/docs-frontmatter-migrate.js +364 -0
  59. package/dist/lib/docs-frontmatter.d.ts +33 -0
  60. package/dist/lib/docs-frontmatter.d.ts.map +1 -0
  61. package/dist/lib/docs-frontmatter.js +130 -0
  62. package/dist/lib/docs-index.d.ts +1 -0
  63. package/dist/lib/docs-index.d.ts.map +1 -1
  64. package/dist/lib/docs-index.js +4 -5
  65. package/dist/lib/docs-lint.d.ts +2 -0
  66. package/dist/lib/docs-lint.d.ts.map +1 -1
  67. package/dist/lib/docs-lint.js +18 -12
  68. package/dist/lib/docs-meta.d.ts +14 -0
  69. package/dist/lib/docs-meta.d.ts.map +1 -0
  70. package/dist/lib/docs-meta.js +34 -0
  71. package/dist/lib/docs-sweep.d.ts +12 -0
  72. package/dist/lib/docs-sweep.d.ts.map +1 -1
  73. package/dist/lib/docs-sweep.js +98 -103
  74. package/dist/lib/format.js +2 -2
  75. package/dist/lib/http/index.d.ts +1 -0
  76. package/dist/lib/http/index.d.ts.map +1 -1
  77. package/dist/lib/http/index.js +1 -0
  78. package/dist/lib/http/request.d.ts +77 -0
  79. package/dist/lib/http/request.d.ts.map +1 -0
  80. package/dist/lib/http/request.js +105 -0
  81. package/dist/lib/instructions/apply.d.ts +63 -0
  82. package/dist/lib/instructions/apply.d.ts.map +1 -0
  83. package/dist/lib/instructions/apply.js +255 -0
  84. package/dist/lib/instructions/splice.d.ts +73 -0
  85. package/dist/lib/instructions/splice.d.ts.map +1 -0
  86. package/dist/lib/instructions/splice.js +118 -0
  87. package/dist/lib/instructions/templates.d.ts +45 -0
  88. package/dist/lib/instructions/templates.d.ts.map +1 -0
  89. package/dist/lib/instructions/templates.js +258 -0
  90. package/dist/lib/tunnel/gate.d.ts +1 -0
  91. package/dist/lib/tunnel/gate.d.ts.map +1 -1
  92. package/dist/lib/tunnel/gate.js +14 -9
  93. package/dist/lib/tunnel/state.d.ts +11 -1
  94. package/dist/lib/tunnel/state.d.ts.map +1 -1
  95. package/dist/lib/tunnel/state.js +8 -3
  96. package/package.json +7 -6
  97. package/src/commander.ts +23 -0
  98. package/src/commands/agents.ts +50 -3
  99. package/src/commands/deinit.ts +5 -0
  100. package/src/commands/devtools.ts +284 -0
  101. package/src/commands/docs.ts +81 -1
  102. package/src/commands/doctor.ts +13 -4
  103. package/src/commands/env.ts +11 -77
  104. package/src/commands/init.ts +66 -15
  105. package/src/commands/scratch.ts +1 -1
  106. package/src/commands/tunnel.ts +316 -65
  107. package/src/commands/web-fetch.ts +1 -1
  108. package/src/core/agents/cli.ts +55 -0
  109. package/src/core/agents/coord-client.ts +34 -7
  110. package/src/core/agents/events/emit.ts +5 -0
  111. package/src/core/agents/rules/claim-conflict.ts +17 -6
  112. package/src/core/agents/state/heartbeat-projector.ts +11 -3
  113. package/src/core/config.ts +14 -0
  114. package/src/core/hooks/cli.ts +3 -3
  115. package/src/core/hooks/effects/index.ts +23 -17
  116. package/src/core/hooks/events/emit.ts +5 -0
  117. package/src/core/hooks/events/rotate.ts +151 -0
  118. package/src/core/hooks/harness/events.ts +30 -3
  119. package/src/core/hooks/harness/wiring.ts +46 -5
  120. package/src/{lib → core}/scratch/index.ts +2 -2
  121. package/src/lib/devtools.ts +1653 -0
  122. package/src/lib/docs-frontmatter-migrate.ts +427 -0
  123. package/src/lib/docs-frontmatter.ts +151 -0
  124. package/src/lib/docs-index.ts +4 -5
  125. package/src/lib/docs-lint.ts +17 -11
  126. package/src/lib/docs-meta.ts +44 -0
  127. package/src/lib/docs-sweep.ts +104 -102
  128. package/src/lib/format.ts +2 -2
  129. package/src/lib/http/index.ts +1 -0
  130. package/src/lib/http/request.ts +154 -0
  131. package/src/lib/instructions/apply.ts +318 -0
  132. package/src/lib/instructions/splice.ts +148 -0
  133. package/src/lib/instructions/templates.ts +295 -0
  134. package/src/lib/tunnel/gate.ts +14 -9
  135. package/src/lib/tunnel/state.ts +19 -4
  136. package/dist/lib/scratch/index.d.ts.map +0 -1
  137. /package/dist/{lib → core}/scratch/index.d.ts +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../../src/lib/instructions/templates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,+DAA+D;AAC/D,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,sEAAsE;AACtE,eAAO,MAAM,aAAa,WAAW,CAAC;AAEtC,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,MAAM,EAAE,OAAO,CAAC;IAChB,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,WAA6C,GACpD,MAAM,CA+CR;AAID,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC;AAsKD,eAAO,MAAM,MAAM,EAAE,aAAa,EA4BjC,CAAC"}
@@ -0,0 +1,258 @@
1
+ /**
2
+ * The agent-facing content harnery ships into a consumer: one orientation block
3
+ * for `AGENTS.md` and the generic skills. Everything here is engine mechanics
4
+ * only — no triage rubric, no escalation targets, no host doc-layout policy
5
+ * (those stay host-authored, per ADR 0007's portability split). Every command
6
+ * string renders through `binName`; a template that only reads for `harn` is a
7
+ * bug the portability guard exists to catch.
8
+ *
9
+ * Content, not paths: templates are TS string builders (not shipped `.md`
10
+ * files), so they compile into `dist/` and resolve identically under Bun and
11
+ * Node — no `files`-field copy or package-path guesswork.
12
+ */
13
+ import { buildOwnedSkill } from "./splice.js";
14
+ /** Managed-region name for the AGENTS.md orientation block. */
15
+ export const INSTRUCTIONS_REGION = "instructions";
16
+ /** Managed-region name for the CLAUDE.md `@AGENTS.md` import shim. */
17
+ export const IMPORT_REGION = "import";
18
+ /**
19
+ * The always-on orientation spliced into `AGENTS.md`. Target ≤ 80 rendered
20
+ * lines: it costs every agent context on every turn, so it states that each
21
+ * surface *exists* and gives one line of *when* — the *how* lives in the skills
22
+ * and each command's `--help`. Skill names are fixed (`harn-decide`,
23
+ * `harn-council`) even for a renamed bin; only command strings track `binName`.
24
+ *
25
+ * The block only points at a skill that actually exists here: a host that
26
+ * excludes one via `skills.exclude`, or a harness with no skill primitive
27
+ * (cursor/codex get the block but no skill files), gets a `--help` pointer
28
+ * instead of a dangling reference to a skill it doesn't have.
29
+ */
30
+ export function renderInstructionsBlock(binName, skills = { decide: true, council: true }) {
31
+ const b = binName;
32
+ const named = [skills.decide && "`harn-decide`", skills.council && "`harn-council`"].filter(Boolean);
33
+ const deeper = named.length > 0
34
+ ? `Procedures for the deeper flows live in the ${named.join(" and ")} skill${named.length > 1 ? "s" : ""}.`
35
+ : `See \`${b} decision --help\` and \`${b} council --help\` for the deeper procedures.`;
36
+ const decidePointer = skills.decide
37
+ ? "The `harn-decide` skill has the file / claim / resolve-with-evidence procedure."
38
+ : `See \`${b} decision --help\` for the file / claim / resolve-with-evidence procedure.`;
39
+ const councilPointer = skills.council
40
+ ? "The `harn-council` skill has the steward and member flow."
41
+ : `See \`${b} council --help\` for the steward and member flow.`;
42
+ return `## harnery coordination
43
+
44
+ This project runs [harnery](https://harnery.com) for multi-agent coordination.
45
+ You share this checkout with other agents; the surfaces below keep you oriented
46
+ and out of each other's way. Run \`${b} <command> --help\` for any command's full
47
+ surface. ${deeper}
48
+
49
+ **Identity + peers.** You are one of several agents in this repo.
50
+ \`${b} agents whoami\` is you; \`${b} agents status\` shows your session plus the
51
+ active peers and the files they've claimed; \`${b} agents set-task "<focus>"\`
52
+ declares your current focus so peers can see it. Check for peers before editing
53
+ widely-shared files.
54
+
55
+ **Declare intent on shell commands.** Every command you run is captured to the
56
+ coordination ledger. Lead a shell command with a \`# intent: <why>\` comment (or set
57
+ the tool's description) so the recorded event carries a reason instead of
58
+ \`(no intent)\`.
59
+
60
+ **Scratch journal.** \`${b} scratch add <category> "<text>"\` (category = note, plan,
61
+ decision, blocker, or handoff) leaves breadcrumbs that survive context compaction;
62
+ \`${b} scratch read\` reads yours, \`${b} scratch read --name <peer>\` reads a peer's.
63
+ Use it for anything future-you or a peer will need to pick up your thread.
64
+
65
+ **Decision docket.** When you would otherwise stop to ask a human a decision you
66
+ can't resolve from the repo, file it instead. \`${b} decision file "<question>"\`
67
+ records it and lets you proceed on a stated default; \`${b} decision search "<terms>"\`
68
+ surfaces prior decisions, so check for precedent before re-deciding. ${decidePointer}
69
+
70
+ **Councils.** For a hard or contested decision, convene a council of agents.
71
+ \`${b} council create "<objective>"\` runs structured rounds toward a decision. ${councilPointer}`;
72
+ }
73
+ function decideBody(b) {
74
+ return `The decision docket is a persistent queue for decisions you would otherwise
75
+ route to a human. It's built on the \`${b} decision\` engine. This skill is the
76
+ mechanics — file, find precedent, claim, and resolve with evidence. *When* a
77
+ decision needs a human at all (versus one you settle yourself) is host policy;
78
+ if this project defines that rubric, follow it.
79
+
80
+ ## Modes
81
+
82
+ - **A decision you're facing (default)** → capture: record it and proceed.
83
+ - **\`resolve <id>\`** → pick up an open decision, research it, resolve it.
84
+ - **\`review\`** → surface resolved-but-unreviewed decisions for a human to skim.
85
+
86
+ ## Capture (default)
87
+
88
+ 1. **Check precedent first.** \`${b} decision search "<key terms>"\`. If a resolved
89
+ decision already answers this, cite it — don't re-litigate.
90
+ 2. **File it** when the choice has a second consumer (a human will want to see it,
91
+ or a future agent will face it again) or reversal is expensive. Skip the
92
+ docket for pure local mechanics (a variable name, one of two equivalent idioms).
93
+
94
+ \`\`\`bash
95
+ ${b} decision file "<the decision as a clear question>" \\
96
+ --context "why it matters / what's blocked / the options you see" \\
97
+ --default-taken "<what you're proceeding with>"
98
+ \`\`\`
99
+
100
+ For a decision with real substance, write a brief to a file and pass
101
+ \`--brief <path>\` so the reviewer sees options + evidence, not a cold prompt.
102
+ 3. **Proceed on your default.** Filing does not mean blocking — note the id in your
103
+ reply and keep working.
104
+
105
+ ## Resolve (\`resolve <id>\`)
106
+
107
+ \`\`\`bash
108
+ ${b} decision show <id> # read the question + context + any brief
109
+ ${b} decision claim <id> # mark it deliberating (claimed by you)
110
+ \`\`\`
111
+
112
+ Research it for real — run the queries, read the files, compute the costs. Then
113
+ resolve with **cited evidence** (the engine rejects an evidence-free resolution):
114
+
115
+ \`\`\`bash
116
+ ${b} decision resolve <id> \\
117
+ --recommendation "<the call>" \\
118
+ --evidence "<a fact you established: a query run, a file read, a cost computed>" \\
119
+ --evidence "<another>" \\
120
+ --reversal-cost "<cost to undo if wrong>" \\
121
+ --wrong-if "<what would make this wrong>" \\
122
+ --revisit-when "<trigger to revisit>"
123
+ \`\`\`
124
+
125
+ If the decision is genuinely hard or contested, escalate to a council
126
+ (\`${b} council create "<objective>"\`) and link it rather than forcing a thin
127
+ single-agent resolution.
128
+
129
+ ## Review (\`review\`)
130
+
131
+ \`\`\`bash
132
+ ${b} decision list --status resolved
133
+ \`\`\`
134
+
135
+ For each one a human reacts to, record the verdict so triage self-corrects:
136
+
137
+ \`\`\`bash
138
+ ${b} decision review <id> --verdict ratified # agreed; no action
139
+ ${b} decision review <id> --verdict overridden --note "…" # disagreed
140
+ ${b} decision review <id> --verdict wrong-tier-low # "didn't need to see this"
141
+ ${b} decision review <id> --verdict wrong-tier-high # "should have seen it sooner"
142
+ \`\`\``;
143
+ }
144
+ function councilBody(b) {
145
+ return `A council convenes several agents to deliberate a hard or contested decision
146
+ over structured rounds. This skill wraps the \`${b} council\` surface with the
147
+ guardrails a router needs when passing prompts between agents. It's asymmetric on
148
+ purpose: it does the most for **contributors** (refusing a misrouted prompt) and
149
+ the least for **stewards**.
150
+
151
+ Every mode starts by running \`${b} agents whoami --json\` and, when an id is given,
152
+ \`${b} council show <id> --json\`, so the logic works against typed data, not text.
153
+
154
+ ## Modes
155
+
156
+ - **No argument** → list councils you're a member of; surface what waits on you.
157
+ - **\`create <objective>\`** → open the web member-picker with the objective filled.
158
+ - **\`contribute <id>\`** → the guarded contribution flow (below).
159
+ - **\`prompts <id>\`** → steward: draft each pending member's routing prompt.
160
+ - **\`show <id>\`** → render council state verbatim.
161
+
162
+ ## List (no arg)
163
+
164
+ \`\`\`bash
165
+ ${b} agents whoami --json
166
+ ${b} council list --mine --json
167
+ \`\`\`
168
+
169
+ One section per council you're a member of: id + objective, round N (open /
170
+ collected) with N/M contributors, and your status (awaiting prompt / prompt ready
171
+ / already contributed) with the next command to run. Stop after listing — don't
172
+ auto-route into contribute.
173
+
174
+ ## Create (\`create <objective>\`)
175
+
176
+ The web UI is the member + steward picker; don't create from the CLI (that skips
177
+ the steward choice). Emit the link with the objective URL-encoded:
178
+
179
+ \`\`\`
180
+ http://localhost:9000/councils/new?objective=<encoded>
181
+ \`\`\`
182
+
183
+ If the dev server isn't up, start it with \`${b} web up\`.
184
+
185
+ ## Contribute (\`contribute <id>\`) — the guarded flow
186
+
187
+ Run these checks in order; refuse with a specific reason if any fails.
188
+
189
+ 1. **Membership.** If your \`whoami\` name isn't in \`manifest.members\`, refuse — the
190
+ router likely meant a different agent's session.
191
+ 2. **Already contributed.** If you're in \`current_round_contributors\`, refuse —
192
+ wait for the steward to advance the round.
193
+ 3. **Prompt routing.** Find your entry in \`current_round_prompts\`. If none is
194
+ drafted for you, refuse (the steward must write one first). If the routed body
195
+ carries a \`<!-- council-route … member: <name> -->\` header naming a *different*
196
+ agent, refuse — the wrong prompt was pasted into your session.
197
+ 4. **Compose** per your prompt (read \`manifest.target_doc\` in full if set; strip
198
+ the route header before treating the body as instructions).
199
+ 5. **Submit:**
200
+
201
+ \`\`\`bash
202
+ ${b} council contribute <id> --message "<status-line>" --file <path>
203
+ \`\`\`
204
+
205
+ Use \`--file\` when you edited a target doc (captures the post-edit state);
206
+ \`--message\` for a prose-only contribution. If the prompt asks for a
207
+ substantive/trivial classification, end with the literal \`<substantive>\` or
208
+ \`<trivial>\` angle-bracket tag on the final line (the exit-criterion parser keys
209
+ on it). When in doubt, lean \`<trivial>\`.
210
+
211
+ ## Prompts (\`prompts <id>\`) — steward
212
+
213
+ 1. **Authority.** If your \`whoami\` name ≠ the council's \`steward\`, stop.
214
+ 2. **Plan the round** from the target doc + prior rounds. Round 1 must include a
215
+ completeness critic — assign one member the explicit charge: "What important
216
+ thing is NOT in this document at all — a missing dimension, not a flaw in what's
217
+ written?" Lens-scoped reviewers reliably miss whole absent dimensions.
218
+ 3. **Draft + write** one prompt per member missing from \`current_round_prompts\`:
219
+
220
+ \`\`\`bash
221
+ ${b} council prompt <id> agent-<Name> --message "..." # or --file <path>
222
+ \`\`\`
223
+
224
+ The CLI auto-prepends the \`<!-- council-route … -->\` header — never write it by
225
+ hand. Every prompt must instruct the member to end with the literal
226
+ \`<substantive>\` / \`<trivial>\` tag.
227
+
228
+ ## Refusal style
229
+
230
+ Lead with what you are versus what the council expects, cite the structural fact
231
+ that triggered the refusal (member list, contributors, prompt absence, route
232
+ mismatch), and offer the right next step. Don't propose a workaround that bypasses
233
+ the guard.`;
234
+ }
235
+ export const SKILLS = [
236
+ {
237
+ id: "harn-decide",
238
+ relPath: "harn-decide/SKILL.md",
239
+ render: (binName) => buildOwnedSkill({
240
+ name: "harn-decide",
241
+ description: "File a decision into the docket instead of blocking on a human — search precedent, file it, and proceed on a reversible default; or pick up and resolve an open decision with cited evidence. Use whenever you're about to ask a human a decision-shaped question you could resolve yourself.",
242
+ argumentHint: "[<the decision / question you're facing> | resolve <id> | review]",
243
+ binName,
244
+ body: decideBody(binName),
245
+ }),
246
+ },
247
+ {
248
+ id: "harn-council",
249
+ relPath: "harn-council/SKILL.md",
250
+ render: (binName) => buildOwnedSkill({
251
+ name: "harn-council",
252
+ description: "Interact with the multi-agent council system: list / create / show / prompts (steward) / contribute (member). Guards against misrouting — refuses to contribute when you aren't a member, have already contributed, or weren't routed a prompt.",
253
+ argumentHint: "[<id-or-fragment> | create <objective> | contribute <id> | prompts <id> | show <id>]",
254
+ binName,
255
+ body: councilBody(binName),
256
+ }),
257
+ },
258
+ ];
@@ -3,6 +3,7 @@ declare const ALLOW: Set<string>;
3
3
  declare const TARGET: string;
4
4
  declare const VHOST: string;
5
5
  declare const PORT: number;
6
+ declare const ACCESS: string;
6
7
  declare const UPSTREAM_HTTP: string;
7
8
  declare const UPSTREAM_WS: string;
8
9
  interface WsData {
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/gate.ts"],"names":[],"mappings":"AAgBA,iBAAS,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGlD;AAED,QAAA,MAAM,KAAK,aAKV,CAAC;AACF,QAAA,MAAM,MAAM,QAAwD,CAAC;AACrE,QAAA,MAAM,KAAK,QAAkD,CAAC;AAC9D,QAAA,MAAM,IAAI,QAA0E,CAAC;AAErF,QAAA,MAAM,aAAa,QAAqB,CAAC;AACzC,QAAA,MAAM,WAAW,QAAmB,CAAC;AAErC,UAAU,MAAM;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,QAAA,MAAM,MAAM,oBAoEV,CAAC"}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/gate.ts"],"names":[],"mappings":"AAiBA,iBAAS,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGlD;AAED,QAAA,MAAM,KAAK,aAKV,CAAC;AACF,QAAA,MAAM,MAAM,QAAwD,CAAC;AACrE,QAAA,MAAM,KAAK,QAAkD,CAAC;AAC9D,QAAA,MAAM,IAAI,QAA0E,CAAC;AACrF,QAAA,MAAM,MAAM,QAA8D,CAAC;AAE3E,QAAA,MAAM,aAAa,QAAqB,CAAC;AACzC,QAAA,MAAM,WAAW,QAAmB,CAAC;AAErC,UAAU,MAAM;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,QAAA,MAAM,MAAM,oBAsEV,CAAC"}
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
- // Reverse-proxy worker spawned by `tunnel up`. Listens on 127.0.0.1:<port>,
3
- // checks CF-Connecting-IP against an allowlist passed via env, and forwards
4
- // HTTP + WebSocket requests to an upstream with a Host header rewrite and
5
- // Content-Encoding stripped (Bun's fetch auto-decompresses the body but
6
- // retains the encoding header, which breaks browser decoding downstream).
2
+ // Reverse-proxy worker spawned by `tunnel up`. Listens on 127.0.0.1:<port>
3
+ // and forwards HTTP + WebSocket requests to an upstream with a Host header
4
+ // rewrite and Content-Encoding stripped (Bun's fetch auto-decompresses the
5
+ // body but retains the encoding header, which breaks browser decoding
6
+ // downstream). Cloudflare provider mode checks CF-Connecting-IP against an
7
+ // allowlist; trusted local proxy mode lets the local exposer own access.
7
8
  //
8
9
  // Runs detached, outside the CLI command framework; no command context is
9
10
  // available; stdout/stderr is captured into .cache/tunnel/gate.log by the
@@ -25,6 +26,7 @@ const ALLOW = new Set((process.env.HARNERY_TUNNEL_ALLOW ?? "")
25
26
  const TARGET = process.env.HARNERY_TUNNEL_TARGET ?? "127.0.0.1:8001";
26
27
  const VHOST = process.env.HARNERY_TUNNEL_VHOST ?? "localhost";
27
28
  const PORT = Number(process.env.HARNERY_TUNNEL_PORT ?? argvFlag("--port") ?? "9001");
29
+ const ACCESS = process.env.HARNERY_TUNNEL_ACCESS ?? "cloudflare-allowlist";
28
30
  const UPSTREAM_HTTP = `http://${TARGET}`;
29
31
  const UPSTREAM_WS = `ws://${TARGET}`;
30
32
  const server = Bun.serve({
@@ -36,9 +38,11 @@ const server = Bun.serve({
36
38
  // Bun's max, and beyond any realistic single-turn delay.
37
39
  idleTimeout: 255,
38
40
  async fetch(req, server) {
39
- const ip = req.headers.get("cf-connecting-ip") ?? "";
40
- if (!ALLOW.has(ip)) {
41
- return new Response("403 Forbidden\n", { status: 403 });
41
+ if (ACCESS === "cloudflare-allowlist") {
42
+ const ip = req.headers.get("cf-connecting-ip") ?? "";
43
+ if (!ALLOW.has(ip)) {
44
+ return new Response("403 Forbidden\n", { status: 403 });
45
+ }
42
46
  }
43
47
  const url = new URL(req.url);
44
48
  if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
@@ -98,4 +102,5 @@ const server = Bun.serve({
98
102
  // framework, so no AsyncLocalStorage context is available. stdout/stderr is
99
103
  // captured into .cache/tunnel/gate.log by the spawning command.
100
104
  console.log(`harn-tunnel-gate :${server.port} -> ${UPSTREAM_HTTP} (Host: ${VHOST})`); // lint-ok-emission: detached worker, see file note above
101
- console.log(`allow: ${[...ALLOW].join(", ") || "(empty, denies all)"}`); // lint-ok-emission: detached worker, see file note above
105
+ console.log(`access: ${ACCESS}`); // lint-ok-emission: detached worker, see file note above
106
+ console.log(`allow: ${[...ALLOW].join(", ") || "(empty, not used outside allowlist mode)"}`); // lint-ok-emission: detached worker, see file note above
@@ -2,18 +2,28 @@
2
2
  export declare const DEFAULT_INSTANCE = "default";
3
3
  export declare function gateLogFile(name: string): string;
4
4
  export declare function cfdLogFile(name: string): string;
5
+ export declare function providerLogFile(name: string, provider: TunnelProvider): string;
5
6
  export interface TunnelConfig {
6
7
  allowed_ips: string[];
7
8
  }
9
+ export type TunnelProvider = "cloudflare" | "tailscale";
10
+ export type TailscaleMode = "serve" | "funnel";
8
11
  export interface TunnelState {
9
12
  name: string;
13
+ provider: TunnelProvider;
10
14
  url: string;
11
15
  gate_pid: number;
12
- cloudflared_pid: number;
16
+ /** Present for Cloudflare quick tunnels; absent for Tailscale Serve/Funnel. */
17
+ cloudflared_pid?: number;
18
+ /** Optional provider-side process when a provider owns one. */
19
+ provider_pid?: number;
13
20
  started_at: string;
14
21
  target: string;
15
22
  vhost: string;
16
23
  gate_port: number;
24
+ tailscale_mode?: TailscaleMode;
25
+ tailscale_path?: string;
26
+ tailscale_https_port?: number;
17
27
  }
18
28
  export declare function readConfig(): TunnelConfig;
19
29
  export declare function writeConfig(cfg: TunnelConfig): void;
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/state.ts"],"names":[],"mappings":"AAuBA,0DAA0D;AAC1D,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAW1C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAgBD,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAOD,wBAAgB,UAAU,IAAI,YAAY,CAWzC;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAEnD;AAED,wBAAgB,SAAS,CAAC,IAAI,GAAE,MAAyB,GAAG,WAAW,GAAG,IAAI,CAQ7E;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAEnD;AAED,wBAAgB,UAAU,CAAC,IAAI,GAAE,MAAyB,GAAG,IAAI,CAGhE;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,WAAW,EAAE,CAW1C;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAuB1C"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/lib/tunnel/state.ts"],"names":[],"mappings":"AAuBA,0DAA0D;AAC1D,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAW1C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,MAAM,CAG9E;AAgBD,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,WAAW,CAAC;AACxD,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,aAAa,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAOD,wBAAgB,UAAU,IAAI,YAAY,CAWzC;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAEnD;AAED,wBAAgB,SAAS,CAAC,IAAI,GAAE,MAAyB,GAAG,WAAW,GAAG,IAAI,CAQ7E;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAEnD;AAED,wBAAgB,UAAU,CAAC,IAAI,GAAE,MAAyB,GAAG,IAAI,CAGhE;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,WAAW,EAAE,CAW1C;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAuB1C"}
@@ -1,5 +1,5 @@
1
- // Tunnel config + state persistence + cloudflared-install helper. Lives
2
- // under <cwd>/.cache/tunnel/; gitignored, so the allowlist is per-machine.
1
+ // Tunnel config + state persistence + provider helpers. Lives under
2
+ // <cwd>/.cache/tunnel/; gitignored, so the allowlist is per-machine.
3
3
  import { execSync } from "node:child_process";
4
4
  import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
5
5
  import { resolve } from "node:path";
@@ -27,6 +27,11 @@ export function gateLogFile(name) {
27
27
  export function cfdLogFile(name) {
28
28
  return name === DEFAULT_INSTANCE ? "cloudflared.log" : `cloudflared-${name}.log`;
29
29
  }
30
+ export function providerLogFile(name, provider) {
31
+ if (provider === "cloudflare")
32
+ return cfdLogFile(name);
33
+ return name === DEFAULT_INSTANCE ? "tailscale.log" : `tailscale-${name}.log`;
34
+ }
30
35
  /** Map a state filename back to its instance name (inverse of stateFile). */
31
36
  function nameFromStateFile(file) {
32
37
  if (file === "state.json")
@@ -42,7 +47,7 @@ const DEFAULT_CONFIG = {
42
47
  };
43
48
  /** Normalize a parsed state blob; supply `name` for pre-multi-instance files. */
44
49
  function normalizeState(raw, fallbackName) {
45
- return { ...raw, name: raw.name ?? fallbackName };
50
+ return { ...raw, name: raw.name ?? fallbackName, provider: raw.provider ?? "cloudflare" };
46
51
  }
47
52
  export function readConfig() {
48
53
  const p = cachePath("tunnel", CONFIG_FILE);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.6.0",
4
- "description": "Multi-agent coordination + harness adapters + portable CLI utilities for Claude Code / Cursor / Codex.",
3
+ "version": "0.7.1",
4
+ "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
7
7
  "homepage": "https://harnery.com",
@@ -87,10 +87,10 @@
87
87
  "types": "./dist/lib/readability/index.d.ts",
88
88
  "import": "./dist/lib/readability/index.js"
89
89
  },
90
- "./lib/scratch": {
91
- "bun": "./src/lib/scratch/index.ts",
92
- "types": "./dist/lib/scratch/index.d.ts",
93
- "import": "./dist/lib/scratch/index.js"
90
+ "./core/scratch": {
91
+ "bun": "./src/core/scratch/index.ts",
92
+ "types": "./dist/core/scratch/index.d.ts",
93
+ "import": "./dist/core/scratch/index.js"
94
94
  },
95
95
  "./lib/tunnel/state": {
96
96
  "bun": "./src/lib/tunnel/state.ts",
@@ -129,6 +129,7 @@
129
129
  "typecheck": "bun x tsc --noEmit",
130
130
  "lint": "bun x @biomejs/biome check src",
131
131
  "check:portability": "bun run scripts/check-portability.ts",
132
+ "check:layering": "bun run scripts/check-layering.ts",
132
133
  "lint:fix": "bun x @biomejs/biome check --write src",
133
134
  "format": "bun x @biomejs/biome format --write src",
134
135
  "test": "bun test src tests",
package/src/commander.ts CHANGED
@@ -29,6 +29,7 @@ import { registerContextCommand } from "./commands/context.ts";
29
29
  import { registerCookiesCommand } from "./commands/cookies.ts";
30
30
  import { registerDecisionCommand } from "./commands/decision.ts";
31
31
  import { registerDeinitCommand } from "./commands/deinit.ts";
32
+ import { registerDevtoolsCommand } from "./commands/devtools.ts";
32
33
  import { registerDocsCommand } from "./commands/docs.ts";
33
34
  import { registerDoctorCommand } from "./commands/doctor.ts";
34
35
  import { registerEditBatchCommand } from "./commands/edit-batch.ts";
@@ -81,6 +82,20 @@ export interface HarneryContextOpts {
81
82
  skipCommands?: readonly string[];
82
83
  }
83
84
 
85
+ /** One row in a `harn env` section report. */
86
+ export interface EnvCheck {
87
+ label: string;
88
+ value: string;
89
+ status?: "ok" | "missing" | "warn" | "info";
90
+ }
91
+
92
+ /**
93
+ * A `harn env` section: an async probe returning its rows. Hosts register extra
94
+ * sections (e.g. cloud-provider connectivity) via `context.envSections`; harnery
95
+ * core ships only the generic sections (runtimes, docker, git).
96
+ */
97
+ export type EnvSection = () => Promise<EnvCheck[]>;
98
+
84
99
  export interface HarneryProgramContext {
85
100
  /** Project name (e.g., "my-monorepo"). Used in user-facing log lines + telemetry tags. */
86
101
  projectName?: string;
@@ -98,6 +113,13 @@ export interface HarneryProgramContext {
98
113
  * submodule row.
99
114
  */
100
115
  submodules?: readonly string[];
116
+ /**
117
+ * Extra `harn env` sections keyed by name (e.g. `{ gcp, bq }`). Merged into
118
+ * the built-in generic sections (runtimes, docker, git), so a host can expose
119
+ * `harn env <name>` for its own environment probes without harnery core
120
+ * carrying provider-specific checks. harn standalone ships none.
121
+ */
122
+ envSections?: Record<string, EnvSection>;
101
123
  /**
102
124
  * Optional callback that returns extra HTTP headers to attach to outbound
103
125
  * `fetch` calls based on the target URL. Useful for consumers that need
@@ -237,6 +259,7 @@ export function createHarneryProgram(opts: HarneryContextOpts = {}): Command {
237
259
  registerContextCommand(program, emit, opts.context);
238
260
  registerScratchCommand(program, emit);
239
261
  registerDecisionCommand(program, emit);
262
+ registerDevtoolsCommand(program, emit);
240
263
  registerTunnelCommand(program, emit, opts.context);
241
264
  registerDocsCommand(program, emit, opts.context);
242
265
  registerAgentsCommand(program, emit);
@@ -36,6 +36,7 @@ import {
36
36
  normalizeHarness,
37
37
  readHeartbeat,
38
38
  resolveOwner,
39
+ resolveOwnerBySessionEnv,
39
40
  resolveOwnerWithSource,
40
41
  } from "../core/agents/index.ts";
41
42
  import { resolveBinName } from "../core/config.ts";
@@ -46,6 +47,7 @@ import { resolveBinName } from "../core/config.ts";
46
47
  const STREAM_SCAN_CAP_BYTES = 128 * 1024 * 1024; // 128 MiB
47
48
 
48
49
  import { parsePsChainLine } from "../core/hooks/resolve/anchor.ts";
50
+ import { appendEntry, resolveOwnerByName } from "../core/scratch/index.ts";
49
51
  import {
50
52
  buildCouncilId,
51
53
  buildInviteMarkdown,
@@ -80,7 +82,6 @@ import {
80
82
  lookupById as lookupIdentityById,
81
83
  lookupByName as lookupIdentityByName,
82
84
  } from "../lib/identities/index.ts";
83
- import { appendEntry, resolveOwnerByName } from "../lib/scratch/index.ts";
84
85
 
85
86
  const FRESHNESS_SECS = 600; // 10-minute heartbeat-freshness window.
86
87
 
@@ -166,8 +167,7 @@ export function registerAgentsCommand(program: Command, emitParam: EmitContext):
166
167
  cmd
167
168
  .command("show <name>")
168
169
  .description(
169
- "Deep-dive on one peer agent: registry state (files held, last tool, task) " +
170
- "plus claude-sessions history (latest title, recent prompts, recent tools, tool-usage tallies). " +
170
+ "Deep-dive on one peer agent: registry state (files held, last tool, task, turn summary). " +
171
171
  "Disambiguates name → instance_id via prefix match.",
172
172
  )
173
173
  .option("--json", "JSON envelope output")
@@ -694,6 +694,7 @@ function runWhoami(opts: { json?: boolean }): void {
694
694
  process.exit(1);
695
695
  }
696
696
 
697
+ ensureCursorSession(root);
697
698
  const resolved = resolveOwnerWithSource();
698
699
  const myOwner = resolved.owner;
699
700
  if (!myOwner) {
@@ -1172,6 +1173,50 @@ function formatLocalShort(iso: string): string {
1172
1173
  }).format(d);
1173
1174
  }
1174
1175
 
1176
+ function cursorEnvSessionId(): string | null {
1177
+ const raw = process.env.CURSOR_SESSION_ID?.trim() || process.env.CURSOR_CONVERSATION_ID?.trim();
1178
+ if (!raw) return null;
1179
+ return raw.startsWith("bc-") && raw.length > 3 ? raw.slice(3) : raw;
1180
+ }
1181
+
1182
+ function shouldBootstrapCursorSession(): boolean {
1183
+ return process.env.CURSOR_AGENT === "1" && cursorEnvSessionId() !== null;
1184
+ }
1185
+
1186
+ function ensureCursorSession(root: string): void {
1187
+ if (!shouldBootstrapCursorSession()) return;
1188
+ if (resolveOwnerBySessionEnv(root)) return;
1189
+
1190
+ const sessionId = cursorEnvSessionId();
1191
+ if (!sessionId) return;
1192
+ const agentHook = resolve(root, "harnery", "bin", "agent-hook");
1193
+ if (!existsSync(agentHook)) return;
1194
+
1195
+ const payload = JSON.stringify({
1196
+ conversation_id: sessionId,
1197
+ session_id: sessionId,
1198
+ hook_event_name: "sessionStart",
1199
+ workspace_roots: [root],
1200
+ cwd: root,
1201
+ composer_mode: "agent",
1202
+ is_background_agent: false,
1203
+ });
1204
+
1205
+ spawnSync("bash", [agentHook, "session-start", "--harness", "cursor"], {
1206
+ input: payload,
1207
+ cwd: root,
1208
+ encoding: "utf8",
1209
+ timeout: 3000,
1210
+ env: {
1211
+ ...process.env,
1212
+ HARNERY_AGENT_COORD_PLATFORM: "cursor",
1213
+ HARNERY_COORD_ROOT_OVERRIDE: root,
1214
+ CURSOR_SESSION_ID: sessionId,
1215
+ CURSOR_CONVERSATION_ID: sessionId,
1216
+ },
1217
+ });
1218
+ }
1219
+
1175
1220
  function runReleaseClaim(path: string): void {
1176
1221
  const root = monorepoRoot();
1177
1222
  if (!root) {
@@ -1220,6 +1265,7 @@ function runSetTask(task: string, opts?: { sessionId?: string }): void {
1220
1265
  // Identity: prefer explicit --session-id (the ppid-walk-free escape hatch,
1221
1266
  // mirrors `status`), fall back to the ppid walk. Cursor shell tool calls
1222
1267
  // don't descend from a pid-map-registered anchor, so the walk can miss there.
1268
+ if (!opts?.sessionId) ensureCursorSession(root);
1223
1269
  const myOwner = opts?.sessionId ?? resolveOwner();
1224
1270
  if (!myOwner) {
1225
1271
  emit.error({
@@ -1272,6 +1318,7 @@ function runStatus(opts: { json?: boolean; sessionId?: string }): void {
1272
1318
 
1273
1319
  // Identity resolution: prefer explicit --session-id (hook-friendly), fall
1274
1320
  // back to ppid walk for interactive shell usage.
1321
+ if (!opts.sessionId) ensureCursorSession(root);
1275
1322
  const myOwner = opts.sessionId ?? resolveOwner();
1276
1323
  if (!myOwner) {
1277
1324
  emit.error({
@@ -34,6 +34,7 @@ import type { Command } from "commander";
34
34
  import type { EmitContext } from "../commander.ts";
35
35
  import { DEFAULT_BIN_NAME } from "../core/config.ts";
36
36
  import { HARNESS_SPECS, type HarnessId } from "../core/hooks/harness/events.ts";
37
+ import { removeInstructions } from "../lib/instructions/apply.ts";
37
38
  import { type SettingsFile, unwireHooks } from "./init.ts";
38
39
 
39
40
  interface DeinitOpts {
@@ -133,6 +134,10 @@ export function registerDeinitCommand(program: Command, emit: EmitContext, binNa
133
134
  }
134
135
  }
135
136
 
137
+ // ── 1b. agent-facing instructions block + skills ───────────────────────
138
+ const removed = removeInstructions(projectRoot, { harness, dryRun });
139
+ actions.push(...removed.actions, ...removed.warnings.map((w) => `! ${w}`));
140
+
136
141
  // ── 2. coord root (opt-in; destructive) ────────────────────────────────
137
142
  if (purgeState) {
138
143
  if (!coordExists) {