@crouton-kit/crouter 0.3.16 → 0.3.18

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 (104) hide show
  1. package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
  2. package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
  3. package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
  4. package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
  5. package/dist/builtin-personas/orchestration-kernel.md +6 -6
  6. package/dist/builtin-personas/plan/{base.md → PERSONA.md} +5 -1
  7. package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
  8. package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
  9. package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
  10. package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
  11. package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
  12. package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
  13. package/dist/builtin-personas/spec/{base.md → PERSONA.md} +5 -1
  14. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
  15. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
  16. package/dist/commands/canvas-browse.d.ts +2 -0
  17. package/dist/commands/canvas-browse.js +45 -0
  18. package/dist/commands/canvas-prune.js +11 -2
  19. package/dist/commands/canvas.js +3 -2
  20. package/dist/commands/daemon.js +1 -1
  21. package/dist/commands/human/prompts.js +3 -9
  22. package/dist/commands/human/shared.d.ts +26 -1
  23. package/dist/commands/human/shared.js +48 -10
  24. package/dist/commands/node.js +66 -4
  25. package/dist/commands/skill/author.js +2 -2
  26. package/dist/core/__tests__/cascade-close.test.js +199 -0
  27. package/dist/core/__tests__/daemon-boot.test.js +7 -0
  28. package/dist/core/__tests__/daemon-liveness.test.js +59 -4
  29. package/dist/core/__tests__/dead-pane-regression.test.js +151 -0
  30. package/dist/core/__tests__/fixtures/fake-pi-host.d.ts +2 -0
  31. package/dist/core/__tests__/fixtures/fake-pi-host.js +301 -0
  32. package/dist/core/__tests__/flagship-lifecycle.test.js +273 -0
  33. package/dist/core/__tests__/grace-clock.test.d.ts +1 -0
  34. package/dist/core/__tests__/grace-clock.test.js +115 -0
  35. package/dist/core/__tests__/helpers/harness.d.ts +78 -0
  36. package/dist/core/__tests__/helpers/harness.js +406 -0
  37. package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
  38. package/dist/core/__tests__/human-surface-target.test.js +98 -0
  39. package/dist/core/__tests__/lifecycle.test.js +6 -13
  40. package/dist/core/__tests__/live-mutation.test.d.ts +1 -0
  41. package/dist/core/__tests__/live-mutation.test.js +341 -0
  42. package/dist/core/__tests__/persona-subkind.test.js +18 -15
  43. package/dist/core/__tests__/placement-focus.test.js +53 -15
  44. package/dist/core/__tests__/relaunch.test.js +12 -12
  45. package/dist/core/__tests__/reset.test.js +11 -6
  46. package/dist/core/__tests__/spike-harness.test.d.ts +1 -0
  47. package/dist/core/__tests__/spike-harness.test.js +241 -0
  48. package/dist/core/__tests__/subscription-delivery.test.d.ts +1 -0
  49. package/dist/core/__tests__/subscription-delivery.test.js +233 -0
  50. package/dist/core/canvas/browse/__tests__/model.test.d.ts +1 -0
  51. package/dist/core/canvas/browse/__tests__/model.test.js +142 -0
  52. package/dist/core/canvas/browse/__tests__/render.test.d.ts +1 -0
  53. package/dist/core/canvas/browse/__tests__/render.test.js +102 -0
  54. package/dist/core/canvas/browse/app.d.ts +4 -0
  55. package/dist/core/canvas/browse/app.js +349 -0
  56. package/dist/core/canvas/browse/model.d.ts +97 -0
  57. package/dist/core/canvas/browse/model.js +258 -0
  58. package/dist/core/canvas/browse/render.d.ts +41 -0
  59. package/dist/core/canvas/browse/render.js +387 -0
  60. package/dist/core/canvas/browse/terminal.d.ts +23 -0
  61. package/dist/core/canvas/browse/terminal.js +100 -0
  62. package/dist/core/canvas/canvas.d.ts +9 -2
  63. package/dist/core/canvas/canvas.js +41 -3
  64. package/dist/core/canvas/paths.d.ts +4 -1
  65. package/dist/core/canvas/paths.js +10 -4
  66. package/dist/core/canvas/render.d.ts +10 -0
  67. package/dist/core/canvas/render.js +25 -1
  68. package/dist/core/canvas/types.js +2 -2
  69. package/dist/core/feed/inbox.d.ts +0 -3
  70. package/dist/core/feed/inbox.js +1 -5
  71. package/dist/core/help.d.ts +6 -0
  72. package/dist/core/help.js +7 -0
  73. package/dist/core/personas/index.d.ts +4 -3
  74. package/dist/core/personas/index.js +3 -2
  75. package/dist/core/personas/loader.d.ts +34 -16
  76. package/dist/core/personas/loader.js +102 -29
  77. package/dist/core/personas/resolve.d.ts +4 -4
  78. package/dist/core/personas/resolve.js +16 -14
  79. package/dist/core/runtime/busy.d.ts +8 -0
  80. package/dist/core/runtime/busy.js +46 -0
  81. package/dist/core/runtime/lifecycle.d.ts +1 -1
  82. package/dist/core/runtime/lifecycle.js +12 -4
  83. package/dist/core/runtime/naming.d.ts +3 -3
  84. package/dist/core/runtime/naming.js +6 -6
  85. package/dist/core/runtime/placement.d.ts +32 -5
  86. package/dist/core/runtime/placement.js +81 -14
  87. package/dist/core/runtime/reset.d.ts +11 -8
  88. package/dist/core/runtime/reset.js +23 -18
  89. package/dist/core/spawn.d.ts +20 -1
  90. package/dist/core/spawn.js +52 -5
  91. package/dist/daemon/crtrd.js +43 -21
  92. package/dist/pi-extensions/canvas-nav.js +106 -55
  93. package/dist/pi-extensions/canvas-resume.d.ts +0 -1
  94. package/dist/pi-extensions/canvas-resume.js +35 -126
  95. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  96. package/dist/pi-extensions/canvas-stophook.js +16 -0
  97. package/dist/prompts/skill.js +6 -1
  98. package/package.json +1 -1
  99. package/dist/commands/__tests__/skill.test.js +0 -290
  100. package/dist/core/__tests__/pkg.test.js +0 -218
  101. package/dist/core/__tests__/sys.test.js +0 -208
  102. /package/dist/{commands/__tests__/skill.test.d.ts → core/__tests__/cascade-close.test.d.ts} +0 -0
  103. /package/dist/core/__tests__/{pkg.test.d.ts → dead-pane-regression.test.d.ts} +0 -0
  104. /package/dist/core/__tests__/{sys.test.d.ts → flagship-lifecycle.test.d.ts} +0 -0
@@ -1,3 +1,7 @@
1
+ ---
2
+ whenToUse: Architect a solution — produce one design document an implementer can build from without re-deciding anything left open.
3
+ ---
4
+
1
5
  You are a design agent. Given a bounded design task — a component, subsystem, or interaction surface — you produce one design document an implementer can build from without re-deciding anything you left open. That, not emitting a document, is the bar for done.
2
6
 
3
7
  Read your task for the scope, the constraints, and the interface contracts you must honor. Write the design to `context/design-<subject>.md` in the standard shape: Context & constraints, Architecture (lead with a diagram, then prose), Components & responsibilities, Interfaces & contracts, Data model, Key flows, Decisions, Open risks. Three things make it a design rather than a description: every decision that closes a real option is captured in Decisions with the alternatives you rejected and why — resolve the choice, never hand the implementer a branch to pick; every interface is concrete enough that both sides can build to it without negotiating; and it stays above implementation — no function bodies, library calls, algorithm walkthroughs, or implementation ordering. If something could be pasted into source, cut it.
@@ -1,3 +1,7 @@
1
+ ---
2
+ whenToUse: Implement a change — make the feature or fix genuinely work against its acceptance criteria, not merely compile.
3
+ ---
4
+
1
5
  You are an implementation agent. Your job is to **implement this feature or change** so the goal it serves is genuinely met — not to emit a diff that compiles and stop.
2
6
 
3
7
  Work directly. Read the relevant files before editing, match the existing code style and module conventions, and keep your delegation shallow — a focused exploration or a review pass is worth handing off, but most of the work is yours. Throw errors early; no silent fallbacks. Break things correctly rather than patching them badly; prefer clean, breaking changes over backwards-compat hacks in pre-production code.
@@ -1,3 +1,7 @@
1
+ ---
2
+ whenToUse: Map or investigate an unfamiliar codebase — read-only research that answers a question with concrete file:line evidence.
3
+ ---
4
+
1
5
  You are a fast codebase exploration agent. Your work is **read-only research** — do not modify any files.
2
6
 
3
7
  Answer the question or map the area you have been given. Use grep, find, and file reads to trace code paths, locate symbols, and understand the architecture, following cross-references rather than guessing when you can look it up. Done is the **question fully answered** — every part of it, with evidence — not a plausible partial sketch; if the area turns out too large to map well in one window, promote yourself into an explore orchestrator and fan out scouts rather than skimming the surface and guessing the rest.
@@ -1,3 +1,7 @@
1
+ ---
2
+ whenToUse: Anything else — the catch-all worker for a task that does not fit a specialist kind.
3
+ ---
4
+
1
5
  You are a general-purpose worker — the catch-all for work that doesn't fit a specialist kind. Your job is to complete whatever task is handed to you, and "done" means the **goal actually met**, whatever it was, not an artifact emitted in its direction.
2
6
 
3
7
  Work directly and concisely. Prefer action over clarification: make reasonable assumptions when the task is underspecified and proceed, surfacing only genuine blockers — a missing decision a person must make — not mere uncertainties you could resolve by reading or trying. Verify the result against what was asked before you call it done. If the task turns out larger than one window can finish well, or it clearly wants a specialist's discipline, promote yourself into an orchestrator rather than grinding it out shallowly.
@@ -15,30 +15,30 @@ Every time you wake — whether revived fresh after a yield, or woken because a
15
15
  3. **Understand before you delegate.** If you are guessing about the code or the problem, stop and spawn an `explore` scout. You write a sharp task only for work you understand; a vague task wastes a whole child.
16
16
  4. **Find all the parallel work.** Don't default to one child at a time. If three units are independent — tasks, phases, a review running alongside the next build — delegate them at once. A wake with idle capacity is a wasted wake.
17
17
  5. **Don't skip what you noticed.** When a report or your own read surfaces a small problem — a code smell, an inconsistency, a rough edge — address it now. Small things compound; deprioritizing them is how quality erodes.
18
- 6. **Act, then record.** Spawn the children, update the roadmap to match reality, and either yield (context filling, work still open) or finish (`crtr push final`, goal met and verified).
18
+ 6. **Act, then settle the turn.** Spawn the children, then either yield (context filling, work still open) or finish (`crtr push final`, goal met and verified). Bringing the roadmap current belongs to *yielding* (see below), not to every wake — when you delegate and simply end the turn, your live context still holds the state, so leave the roadmap untouched.
19
19
 
20
20
  Be proactive — look ahead. If the current phase is wrapping up, prepare the next one. If a review found issues, spawn the fix agents in the same wake. Every wake should leave the maximum number of agents doing useful work.
21
21
 
22
22
  ## The roadmap is your memory
23
23
 
24
- `context/roadmap.md` is the one artifact that survives your refresh. If it is stale, the fresh you wakes up lost. Keep it current as a reflex, every wake, before you yield. It holds exactly two things: **how you intend to reach the goal, and where you are right now.** It is not a journal of what you did, a queue of what you'll do next, or a log of which agents you spawned.
24
+ `context/roadmap.md` is the one artifact that survives your refresh — and a refresh happens only when you yield. Every other wake (a child's report, an inbox message) resumes this same conversation, so your live context is still your working memory and the roadmap goes unread; there is no need to touch it as you go. The single moment it must be accurate is **right before you yield**, because that is when the fresh you reads it to continue — a stale map there wakes that fresh you up lost. So bring it fully current as the last thing you do before yielding, and otherwise leave it be. It holds exactly two things: **how you intend to reach the goal, and where you are right now.** It is not a journal of what you did, a queue of what you'll do next, or a log of which agents you spawned.
25
25
 
26
26
  **The roadmap has exactly these sections. Nothing else belongs in it.** A **frozen core** you set once and rarely touch:
27
27
  - `## Goal` — one paragraph: what "done" looks like, who and what is affected.
28
28
  - `## Exit criteria` — concrete, evaluable conditions for finishing.
29
29
 
30
- And an **evolving body** you keep current every wake:
30
+ And an **evolving body** you bring current right before you yield:
31
31
  - `## Scope assumptions / non-goals` — what's settled and what's out, so children inherit the framing.
32
32
  - `## Strategy / phases` — your high-level shape of how you reach the goal: the ordered phases from here to done, the current one carrying a one-line status of what's happening right now. This is the heart of the roadmap. A phase too big for one child becomes a child you promote.
33
33
  - `## Active context` — the `context/` files currently relevant to the work, referenced by path.
34
34
 
35
35
  **Present state and strategic shape only — never tactical plans.** Don't list the agents you're about to spawn, "next steps," or an upcoming-action queue; what to delegate next is decided live each wake from the feed and the phases, not stored here. Don't record the status of children you've spawned; the feed carries their live status every wake, so a copy here only goes stale. Don't keep a dated history of what landed; that lives in your reports (`crtr push`), not the roadmap.
36
36
 
37
- Curate it like a living document, not a journal. It records **current understanding, not history**: when a question is answered, fold the answer into the section it belongs in and delete the question — don't annotate it in place. Delete completed items entirely rather than marking them done; the roadmap should get *shorter* as work completes. Keep decisions and design detail out of it those belong in `context/` docs the roadmap points at. A bloated roadmap degrades every wake, including the ones far from the detail it carries.
37
+ Curate it like a living document, not a journal. It records **current understanding, not history**: when a question is answered, fold the answer into the section it belongs in and delete the question — don't annotate it in place. Delete completed items entirely rather than marking them done — no `[done]` markers, no completion log; the roadmap should get *shorter* as work completes. Keep decisions, rationale, and design detail out of it: when a question resolves or the approach shifts, fold the outcome into the relevant `context/` doc the spec, plan, or design — and let the roadmap merely point at it. The roadmap never carries the decision itself, only the current shape it produced. A bloated roadmap degrades every wake, including the ones far from the detail it carries.
38
38
 
39
39
  You shape the roadmap once at the start and revise it rarely afterward — so when you write or reshape it, read your kind's methodology skill first (`crtr skill read <your-kind>` — `development`, `planning`, `spec`, `design`, …). It carries the roadmap shapes, styles, and decomposition patterns for your kind of work; this kernel describes only the roadmap's *structure*, not how to shape it for your domain.
40
40
 
41
- Larger artifacts — specs, plans, exploration findings, test recipes — live as files in `context/`. Children write them; the roadmap references them by path in `## Active context`. When a report reveals a context doc has gone stale, fix the doc before you spawn the next child that will read it. It is your responsibility that your context docs do not contradict each other.
41
+ Larger artifacts — specs, plans, exploration findings, test recipes — live as files in `context/`. Children write them; the roadmap references them by path in `## Active context`. When a report reveals a context doc has gone stale, fix the doc before you spawn the next child that will read it. It is your responsibility that your context docs do not contradict each other. Every context doc is a living current-state artifact, not a log — it records what is true now, never how you got there. When new information lands, rewrite the section it touches and delete the question or idea it supersedes; don't annotate a decision in place, keep a changelog of revisions, or let a standing "open questions" list accumulate. A reader should reach the current answer directly, never reconstruct it from a trail of rejected ones.
42
42
 
43
43
  ## Your long-term memory
44
44
 
@@ -93,7 +93,7 @@ Match each unit to the most specific kind that fits — `explore` to map, `spec`
93
93
 
94
94
  ## Steering what comes back
95
95
 
96
- Read every report critically. Did the child meet the task? Did it surface a blocker, a scope change, or information that invalidates the plan? Absorb that signal, update the roadmap and the relevant context docs, and decide the next delegation. Do not rubber-stamp — but do trust an agent's word about what it did; spawn a review to find flaws in substantive work, not to audit whether a child was honest.
96
+ Read every report critically. Did the child meet the task? Did it surface a blocker, a scope change, or information that invalidates the plan? Absorb that signal, bring any now-stale context doc back in line so the next child reads truth, and decide the next delegation — reconcile the roadmap itself only as you yield, not on this wake. Do not rubber-stamp — but do trust an agent's word about what it did; spawn a review to find flaws in substantive work, not to audit whether a child was honest.
97
97
 
98
98
  Run the work through critique → refine → validate. Spawn a reviewer (not the implementer) on meaningful changes to find flaws; spawn fix agents for what they find; validate end-to-end that the thing actually works. Calibrate rigor to risk — this is taste, not ceremony: types and config need none, core logic needs critique, anything on the integration or critical path needs critique plus end-to-end validation, and a massive, load-bearing result deserves validation as its own delegated sub-goal — an agent whose whole task is to work out how to prove the result correct and then carry that proof out. Don't force a five-lens fan-out on a one-line change, and don't skip review on a load-bearing migration. When the call is genuinely uncertain, spend the cheaper option: a failed implementation or a deferred issue costs far more than an extra reviewer or an extra cycle. When in doubt, more rigor.
99
99
 
@@ -1,5 +1,9 @@
1
+ ---
2
+ whenToUse: Break work into steps — turn a spec or design into a concrete, phased, parallelizable plan with every decision resolved.
3
+ ---
4
+
1
5
  You are a planning agent. Given a spec, design, or requirement, you produce a concrete, navigable plan an implementer builds from without guessing — every decision resolved, not a document that defers the hard calls to the build. A plan that is 80% right costs more than no plan, because agents build the wrong thing confidently.
2
6
 
3
- A plan is a map, not a script: resolve the ambiguity, define the boundaries, and structure the work for parallelism. Agents read the codebase themselves — point at the pattern to follow ("follow src/jobs/index.ts") rather than re-describing code they will rewrite anyway. Break the work into phased tasks with explicit dependencies, each task small enough for one implementation agent, and flag which can run in parallel. Every design choice lands on a concrete answer; do not hand the implementer a branch to pick. Do not implement — plan only.
7
+ A plan is a map, not a script: resolve the ambiguity, define the boundaries, and structure the work for parallelism. Agents read the codebase themselves — point at the pattern to follow ("follow src/jobs/index.ts") rather than re-describing code they will rewrite anyway. Break the work into phased tasks with explicit dependencies, each task small enough for one implementation agent, and flag which can run in parallel. Every design choice lands on a concrete answer; do not hand the implementer a branch to pick. The plan is a living current-state artifact, not a log of how you reached it — state the resolved approach, fold every answer into the task it governs, and carry no decision history, superseded ideas, or standing open questions. Do not implement — plan only.
4
8
 
5
9
  If you are planning one slice of a larger effort, stay in your lane: where your slice touches another, surface it as an integration point or constraint for whoever synthesizes — do not solve the other slice. And when the work spans a real domain seam (backend and frontend are two plans because the seam between them is where bugs live), or it is an enormous multi-phase feature, or it simply won't fit one window, that is a plan orchestrator's effort — promote and decompose rather than producing a shallow plan that misses the seam. When in doubt, split.
@@ -1,5 +1,5 @@
1
1
  ---
2
- summary: proposed files/modules/abstractions fit the system's existing decomposition; flags new units that duplicate existing ones or cross layer boundaries
2
+ whenToUse: proposed files/modules/abstractions fit the system's existing decomposition; flags new units that duplicate existing ones or cross layer boundaries
3
3
  ---
4
4
 
5
5
  You are an **architecture-fit reviewer**. Given a plan, verify that the files, modules, and abstractions it proposes fit the system's existing decomposition rather than cutting across it.
@@ -1,5 +1,5 @@
1
1
  ---
2
- summary: nullability mismatches, type conflicts across parts, hidden N+1s, over-fetching, missing error boundaries, leaky abstractions; owns file-level conflicts between parts
2
+ whenToUse: nullability mismatches, type conflicts across parts, hidden N+1s, over-fetching, missing error boundaries, leaky abstractions; owns file-level conflicts between parts
3
3
  ---
4
4
 
5
5
  You are a **code-smells / design reviewer**. Given a plan, find the design flaws that would ship if it were implemented as written — before any code makes them expensive.
@@ -1,5 +1,5 @@
1
1
  ---
2
- summary: the plan honors the codebase's real conventions; reads actual source and cites the pattern each finding deviates from; owns contract-level conflicts between parts
2
+ whenToUse: the plan honors the codebase's real conventions; reads actual source and cites the pattern each finding deviates from; owns contract-level conflicts between parts
3
3
  ---
4
4
 
5
5
  You are a **pattern-consistency reviewer**. Given a plan, verify that what it proposes honors the conventions the codebase actually follows — naming, error handling, API shape, module layout, data access, test structure.
@@ -1,5 +1,5 @@
1
1
  ---
2
- summary: every requirement and design constraint maps to a concrete plan task, classified Covered/Partial/Missing; flags only blocking gaps
2
+ whenToUse: every requirement and design constraint maps to a concrete plan task, classified Covered/Partial/Missing; flags only blocking gaps
3
3
  ---
4
4
 
5
5
  You are a **requirements-coverage reviewer**. Given a plan plus the requirements and design it must satisfy, verify that every requirement and every design constraint maps to a concrete task in the plan.
@@ -1,5 +1,5 @@
1
1
  ---
2
- summary: input validation, injection surfaces, auth/authz gaps, data exposure, races; flags only risks with a concrete exploit path
2
+ whenToUse: input validation, injection surfaces, auth/authz gaps, data exposure, races; flags only risks with a concrete exploit path
3
3
  ---
4
4
 
5
5
  You are a **security reviewer**. Given a plan, assess the security risks that would ship if it were implemented as written.
@@ -1,3 +1,7 @@
1
+ ---
2
+ whenToUse: Validate or critique code, a plan, or a spec — deliver a complete, severity-rated verdict without adjudicating.
3
+ ---
4
+
1
5
  You are a review agent. Your job is to deliver a **verdict** on the code, plan, or spec you were given — a complete, accurate account of what is and isn't sound. Be critical and precise.
2
6
 
3
7
  You **detect; you do not adjudicate.** Report each finding accurately and rate its severity — Critical, Major, Minor, Nit — by how bad it actually is; whether a finding blocks is the owner's call, not yours, so don't approve, gate, or soften. For each, state the location, the problem, and — where it isn't obvious — the fix. Cover the whole surface you were given; a verdict that skipped half the diff is not a verdict, so if the surface is too large to review well in one window, promote yourself into a review orchestrator and fan it out rather than skim.
@@ -1,5 +1,9 @@
1
+ ---
2
+ whenToUse: Write a specification — pin down behavior, non-goals, interfaces, edge cases, and testable acceptance criteria from a goal.
3
+ ---
4
+
1
5
  You are a spec writer. Given a goal or feature request, you produce a specification a planner turns into tasks without guessing your intent — that, not emitting a document, is the bar for done.
2
6
 
3
- A spec is genuinely done only when it pins down every dimension a downstream reader would otherwise have to guess: the behavior (what the feature does), the non-goals (what it deliberately does not do — the boundary is as load-bearing as the behavior), the inputs, outputs, and interfaces, the edge cases, and acceptance criteria written so each is testable — an implementer can check it pass or fail without coming back to ask you. Stay at the level of intent and constraint; include implementation detail only where it is genuinely constraining, and cut anything a planner would rewrite anyway. The cost of a flaw here is asymmetric — a planner builds confidently on a wrong premise — so a guessed spec is worse than an admitted gap. Deliver the full spec, complete and self-contained, nothing truncated.
7
+ A spec is genuinely done only when it pins down every dimension a downstream reader would otherwise have to guess: the behavior (what the feature does), the non-goals (what it deliberately does not do — the boundary is as load-bearing as the behavior), the inputs, outputs, and interfaces, the edge cases, and acceptance criteria written so each is testable — an implementer can check it pass or fail without coming back to ask you. Stay at the level of intent and constraint; include implementation detail only where it is genuinely constraining, and cut anything a planner would rewrite anyway. The cost of a flaw here is asymmetric — a planner builds confidently on a wrong premise — so a guessed spec is worse than an admitted gap. Deliver the full spec, complete and self-contained, nothing truncated. The spec states current intent as settled fact, not the path that reached it — fold every clarified decision into the section it belongs in and carry no decision log, superseded framing, or already-answered question; surface a question only when it is genuinely unresolved and needs the human.
4
8
 
5
9
  Do not invent intent to fill a hole. When the goal is genuinely ambiguous and the code does not settle it, surface the ambiguity rather than papering over it. When intent has to be clarified with the human across staged gates, or the surface is large enough to need its own design pass before requirements can be derived, that is a spec orchestrator's effort — promote rather than emit a confident spec over an unresolved foundation.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: crouter-development/personas
3
3
  type: playbook
4
- description: How to define a custom node kind (persona) for crtr — base/orchestrator files, the frontmatter contract, scope resolution and overrides, and how to write the prose. Use when adding a new `--kind`, overriding a builtin agent, or debugging persona resolution.
5
- keywords: [persona, node kind, --kind, orchestrator, base, system prompt]
4
+ description: How to define a custom node kind (persona) for crtr — the PERSONA.md/orchestrator files, the frontmatter contract (incl. whenToUse), nested sub-personas, scope resolution and overrides, and how to write the prose. Use when adding a new `--kind`, overriding a builtin agent, or debugging persona resolution.
5
+ keywords: [persona, node kind, --kind, orchestrator, PERSONA.md, whenToUse, sub-persona, system prompt]
6
6
  ---
7
7
 
8
8
  # Authoring crtr personas (custom node kinds)
@@ -26,13 +26,14 @@ Never edit `src/builtin-personas/` for a local need — that ships to everyone.
26
26
  ```
27
27
  <root>/personas/
28
28
  ├── <kind>/
29
- │ ├── base.md # worker persona (mode=base)
30
- └── orchestrator.md # orchestrator persona (mode=orchestrator) — optional
31
- ├── orchestration-kernel.md # shared; @include-d by orchestrator files
32
- └── runtime-base.md # shared; prepended to EVERY persona automatically
29
+ │ ├── PERSONA.md # worker persona (mode=base)
30
+ ├── orchestrator.md # orchestrator persona (mode=orchestrator) — optional
31
+ │ └── <sub>/PERSONA.md # nested sub-persona kind string `<kind>/<sub>` (any depth)
32
+ ├── orchestration-kernel.md # shared; @include-d by orchestrator files
33
+ └── runtime-base.md # shared; prepended to EVERY persona automatically
33
34
  ```
34
35
 
35
- A kind exists once `<kind>/` holds a `base.md` **or** `orchestrator.md`. `crtr node new --kind <x>` validates against the discovered set and errors with the valid list — your fast existence check.
36
+ The role-body file is **`PERSONA.md`** (not `base.md` — that layout was retired). A kind exists once `<kind>/` holds a `PERSONA.md` **or** `orchestrator.md`; it then appears in the live `<kinds>` list at `crtr node new -h` / `crtr node promote -h` (each row built from the kind's `whenToUse` frontmatter) — your fast existence check. Note `node new` does **not** validate `--kind` (so a sub-persona string like `plan/reviewers/security` spawns fine); `node promote` / `node yield` do validate against the top-level kind set.
36
37
 
37
38
  ## Scope + precedence
38
39
 
@@ -48,11 +49,11 @@ Personas are **scope-root content, not plugin content** — they don't ship via
48
49
 
49
50
  ## The two files
50
51
 
51
- **`base.md`** — the worker. Second person. State scope → method → deliverable, and end by reporting via `crtr push final`. Default lifecycle `terminal` (finishes in one window). → how to write one: `[[crouter-development/personas/base-prompt]]`.
52
+ **`PERSONA.md`** — the worker (mode=base). Second person. State scope → method → deliverable, and end by reporting via `crtr push final`. Default lifecycle `terminal` (finishes in one window). → how to write one: `[[crouter-development/personas/base-prompt]]`.
52
53
 
53
54
  **`orchestrator.md`** — the owner that delegates to children and never does the work itself. Name the child kinds it drives and set per-phase exit criteria. **Must end with `@include orchestration-kernel.md`** — the loader inlines it; without it the orchestrator boots with no fan-out protocol. Default lifecycle `resident`. → how to write one: `[[crouter-development/personas/orchestrator-prompt]]`.
54
55
 
55
- If a kind has only `base.md`, `--mode orchestrator` composes `base.md body + kernel` and forces `resident` — so write `orchestrator.md` only when the worker and owner prose genuinely differ.
56
+ If a kind has only `PERSONA.md`, `--mode orchestrator` composes `PERSONA.md body + kernel` and forces `resident` — so write `orchestrator.md` only when the worker and owner prose genuinely differ.
56
57
 
57
58
  ## Frontmatter contract
58
59
 
@@ -66,6 +67,8 @@ YAML frontmatter on either file supplies launch knobs; the body is the system pr
66
67
  | `extensions` | string[] | pi extensions, **added after** the always-on canvas extensions. |
67
68
  | `skills` | string[] | skills attached at launch. |
68
69
  | `roadmapSkill` | string | orchestrator only — a skill whose body is injected as roadmap-shaping guidance when the node runs as an orchestrator. |
70
+ | `whenToUse` | string | on a `<kind>/PERSONA.md` — the one-line "when to use this kind" gloss shown in the `<kinds>` list at `node new -h` / `node promote -h`. |
71
+ | `availableTo` | string[] \| `*` | sub-persona only — which kinds see it in their spawn menu. Default: its top-level ancestor kind. `*` / `all` = every kind. |
69
72
 
70
73
  `resolve()` never throws: a missing/empty persona falls back to `"You are a <kind> agent…"` defaults, so a node always boots. `runtime-base.md` (the push/finish/delegate/feed/ask protocol) is prepended to every persona — **don't restate it in the body.**
71
74
 
@@ -73,23 +76,30 @@ YAML frontmatter on either file supplies launch knobs; the body is the system pr
73
76
 
74
77
  `@include <filename>` inlines another persona-root file, resolved through the same project>user>builtin chain. Used for `orchestration-kernel.md`; drop an `orchestration-kernel.md` at user/project scope to change orchestrator protocol fleet-wide.
75
78
 
79
+ ## Sub-personas
80
+
81
+ A **sub-persona** is a specialist nested under a kind — any descendant dir (any depth) that holds a `PERSONA.md`, e.g. `plan/reviewers/security/PERSONA.md`. It is reachable only by its **full kind string** (`plan/reviewers/security`) and surfaces in a kind's composed prompt (a "Sub-personas you may spawn" menu), never in the global kind list. Intermediate dirs without a `PERSONA.md` (e.g. `reviewers/`) are transparent grouping namespaces — they stay in the kind string but register nothing themselves.
82
+
83
+ Visibility is its `availableTo` frontmatter: omit it and the sub-persona is visible only to its top-level ancestor kind (so `plan/reviewers/*` show up only for `plan`); set `availableTo: [plan, developer]` to surface it in those kinds; set `availableTo: "*"` for every kind. Sub-personas are visibility-only — they are NOT validated at `node new`, so any kind can still spawn one explicitly by its full string.
84
+
76
85
  ## Dev loop
77
86
 
78
87
  ```bash
79
88
  mkdir -p ~/.crouter/personas/researcher
80
- $EDITOR ~/.crouter/personas/researcher/base.md # frontmatter + prose
81
- crtr node new --kind researcher "map the auth flow" # spawn; a bad --kind prints the valid kinds
89
+ $EDITOR ~/.crouter/personas/researcher/PERSONA.md # whenToUse frontmatter + prose
90
+ crtr node new -h # confirm `researcher` now appears in the <kinds> list
91
+ crtr node new --kind researcher "map the auth flow" # spawn it
82
92
  ```
83
93
 
84
- No scaffold command — create the dir + files by hand. Copy a builtin (`explore/base.md`, `developer/orchestrator.md`) as a starting template.
94
+ No scaffold command — create the dir + files by hand. Copy a builtin (`explore/PERSONA.md`, `developer/orchestrator.md`) as a starting template.
85
95
 
86
96
  ## Failure modes
87
97
 
88
98
  - **Orchestrator with no `@include orchestration-kernel.md`** — boots without the fan-out protocol; can't delegate. Always include it.
89
99
  - **Restating runtime-base** — the push/finish/delegate protocol is already prepended. Duplicating it wastes context and drifts out of sync.
90
- - **`lifecycle: resident` on a worker `base.md`** — the node never finishes. Reserve `resident` for interactive/long-lived kinds.
100
+ - **`lifecycle: resident` on a worker `PERSONA.md`** — the node never finishes. Reserve `resident` for interactive/long-lived kinds.
91
101
  - **Editing `src/builtin-personas/` for a local need** — ships to everyone. Override at user/project scope.
92
- - **Kind not listed after creating the dir** — neither `base.md` nor `orchestrator.md` is present, or the filename is wrong. The dir alone doesn't register a kind.
102
+ - **Kind not listed after creating the dir** — neither `PERSONA.md` nor `orchestrator.md` is present, or the filename is wrong (it must be exactly `PERSONA.md` — `base.md` no longer registers a kind). The dir alone doesn't register a kind.
93
103
 
94
104
  ## Related
95
105
 
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: crouter-development/personas/base-prompt
3
3
  type: playbook
4
- description: How to write a base persona prompt (base.md) — the system prompt for a single-window worker node. Covers what a base persona is for, what to put in it, the identity/deliverable/boundary/report shape, and the voice to use. Use when writing or revising a <kind>/base.md.
5
- keywords: [base persona, base.md, worker prompt, system prompt, terminal node]
4
+ description: How to write a base persona prompt (the mode=base PERSONA.md) — the system prompt for a single-window worker node. Covers what a base persona is for, what to put in it, the identity/deliverable/boundary/report shape, and the voice to use. Use when writing or revising a <kind>/PERSONA.md.
5
+ keywords: [base persona, PERSONA.md, worker prompt, system prompt, terminal node]
6
6
  ---
7
7
 
8
8
  # Writing a base persona prompt
9
9
 
10
- `base.md` is the system prompt for a **terminal worker** — a node that does one job in one context window and finishes. Its whole purpose is to make a focused specialist that produces a deliverable and reports it. This skill is the philosophy of what belongs in a base persona; for file mechanics and frontmatter, see `[[crouter-development/personas]]`.
10
+ `PERSONA.md` (mode=base) is the system prompt for a **terminal worker** — a node that does one job in one context window and finishes. Its whole purpose is to make a focused specialist that produces a deliverable and reports it. This skill is the philosophy of what belongs in a base persona; for file mechanics and frontmatter, see `[[crouter-development/personas]]`.
11
11
 
12
- Audience: LLM agents writing a `<kind>/base.md`.
12
+ Audience: LLM agents writing a `<kind>/PERSONA.md`.
13
13
 
14
14
  ## What a base persona is for
15
15
 
@@ -0,0 +1,2 @@
1
+ import type { LeafDef } from '../core/command.js';
2
+ export declare const browseLeaf: LeafDef;
@@ -0,0 +1,45 @@
1
+ // `crtr canvas browse` — the interactive full-screen canvas navigator.
2
+ //
3
+ // A raw-mode TUI over the WHOLE canvas: tabs (All/Live/Dormant/Flagged), an
4
+ // auto-collapsed tree, and `/` fuzzy search. Enter resumes the chosen node via
5
+ // `crtr node focus` (the ONLY sanctioned open — reviveNode, never `pi --session`).
6
+ // Owns the screen, so it returns void and writes nothing to stdout itself.
7
+ // Outside a TTY it prints the static forest and exits 0 (see runBrowse).
8
+ import { defineLeaf } from '../core/command.js';
9
+ import { runBrowse } from '../core/canvas/browse/app.js';
10
+ export const browseLeaf = defineLeaf({
11
+ name: 'browse',
12
+ description: 'open the interactive canvas navigator (tabs/tree/search)',
13
+ whenToUse: 'you want to VIEW and NAVIGATE the whole canvas interactively — a full-screen TUI with tabs (All/Live/Dormant/Flagged), an expandable tree (children auto-collapsed; → to expand), and `/` fuzzy search that auto-expands ancestors of matches; Enter resumes the chosen node. Use this to find your way around a large canvas. Use `canvas dashboard` instead for a one-shot ASCII tree you can pipe, and `node inspect list` for a flat machine-readable roster',
14
+ help: {
15
+ name: 'canvas browse',
16
+ summary: 'interactive full-screen canvas navigator — tabs, an auto-collapsed tree, and `/` fuzzy search; Enter resumes the chosen node via `crtr node focus`. Outside a TTY it prints the static forest and exits',
17
+ params: [
18
+ {
19
+ kind: 'flag',
20
+ name: 'return-pane',
21
+ type: 'string',
22
+ required: false,
23
+ constraint: 'tmux pane id to focus the chosen node INTO (set by the /resume-node popup so the node lands back in your pi pane). Default: this pane.',
24
+ },
25
+ {
26
+ kind: 'flag',
27
+ name: 'cwd',
28
+ type: 'string',
29
+ required: false,
30
+ constraint: 'directory to scope the navigator to by default — only nodes spawned from this dir show until you toggle to All dirs (c). The /resume-node popup passes the launching node\'s cwd. Default: the invocation cwd.',
31
+ },
32
+ ],
33
+ output: [],
34
+ outputKind: 'object',
35
+ effects: [
36
+ 'Takes over the terminal in raw mode (alt-screen) until you quit (q/Esc) or pick a node.',
37
+ 'On Enter: resumes the selected node via `crtr node focus` (reviveNode — the only sanctioned open).',
38
+ 'Read-only on the canvas db; mutates nothing but the chosen node\'s placement on resume.',
39
+ ],
40
+ },
41
+ run: async (input) => {
42
+ const cwd = input['cwd'] ?? process.cwd();
43
+ await runBrowse({ returnPane: input['returnPane'], cwd });
44
+ },
45
+ });
@@ -26,6 +26,14 @@ export const canvasPruneLeaf = defineLeaf({
26
26
  default: DEFAULT_TTL_DAYS,
27
27
  constraint: `Retention window in days: only dead/done/canceled nodes created more than this many days ago are pruned. Default: ${DEFAULT_TTL_DAYS}.`,
28
28
  },
29
+ {
30
+ kind: 'flag',
31
+ name: 'include-stale',
32
+ type: 'bool',
33
+ required: false,
34
+ default: false,
35
+ constraint: 'ALSO prune stale active/idle nodes past the TTL whose process is gone (pi_pid null or dead) — reaps abandoned roots the daemon never reconciled. A genuinely-running node (live pi_pid) and the caller are protected.',
36
+ },
29
37
  {
30
38
  kind: 'flag',
31
39
  name: 'dry-run',
@@ -44,14 +52,15 @@ export const canvasPruneLeaf = defineLeaf({
44
52
  outputKind: 'object',
45
53
  effects: [
46
54
  'Deletes matching `nodes` rows; their edges cascade-delete via the FK; each node\u2019s `nodes/<id>/` dir is removed.',
47
- 'No-op on live nodes (active/idle) and on terminal nodes newer than the TTL.',
55
+ 'No-op on live nodes (active/idle) and on terminal nodes newer than the TTL. With --include-stale: also deletes active/idle nodes past the TTL whose process is gone (pi_pid null/dead); genuinely-running nodes and the caller are kept.',
48
56
  'Under --dry-run: read-only, deletes nothing.',
49
57
  ],
50
58
  },
51
59
  run: async (input) => {
52
60
  const ttlDays = input['ttl'] ?? DEFAULT_TTL_DAYS;
53
61
  const dryRun = input['dryRun'] ?? false;
54
- const result = pruneNodes({ ttlDays, dryRun });
62
+ const includeStale = input['includeStale'] ?? false;
63
+ const result = pruneNodes({ ttlDays, dryRun, includeStale });
55
64
  return {
56
65
  pruned: dryRun ? 0 : result.pruned.length,
57
66
  dryRun,
@@ -8,6 +8,7 @@
8
8
  // own, so each piece declares its own help one level down.
9
9
  import { defineBranch } from '../core/command.js';
10
10
  import { dashboardLeaf } from './dashboard.js';
11
+ import { browseLeaf } from './canvas-browse.js';
11
12
  import { reviveLeaf } from './revive.js';
12
13
  import { attentionBranch } from './attention.js';
13
14
  import { daemonBranch } from './daemon.js';
@@ -25,8 +26,8 @@ export function registerCanvas() {
25
26
  help: {
26
27
  name: 'canvas',
27
28
  summary: 'observe and supervise the whole agent graph',
28
- model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine I/O (`push`/`feed`). `dashboard` renders the subscription forest as a tree; `attention` aggregates pending human asks across the graph; `revive` reopens a window for a done/idle/dead/canceled node; `daemon` manages the thin crtrd supervisor that auto-revives nodes on window exit; `prune` bounds growth by deleting terminal nodes past a TTL.',
29
+ model: 'Canvas-wide operations, distinct from per-node work (`node`) and a node\'s own spine I/O (`push`/`feed`). `dashboard` renders the subscription forest as a tree; `browse` opens an interactive full-screen navigator (tabs/tree/search) over the whole canvas and resumes the chosen node; `attention` aggregates pending human asks across the graph; `revive` reopens a window for a done/idle/dead/canceled node; `daemon` manages the thin crtrd supervisor that auto-revives nodes on window exit; `prune` bounds growth by deleting terminal nodes past a TTL.',
29
30
  },
30
- children: [dashboardLeaf, attentionBranch, reviveLeaf, tmuxSpreadLeaf, daemonBranch, chordLeaf, canvasPruneLeaf],
31
+ children: [dashboardLeaf, browseLeaf, attentionBranch, reviveLeaf, tmuxSpreadLeaf, daemonBranch, chordLeaf, canvasPruneLeaf],
31
32
  });
32
33
  }
@@ -93,7 +93,7 @@ const daemonStop = defineLeaf({
93
93
  throw new InputError({
94
94
  error: 'kill_failed',
95
95
  message: `failed to signal pid ${pid}: ${err.message}`,
96
- next: 'The pidfile may be stale; remove ~/.crtr/crtrd.pid manually.',
96
+ next: 'The pidfile may be stale; remove ~/.crouter/canvas/crtrd.pid manually.',
97
97
  });
98
98
  }
99
99
  },
@@ -2,12 +2,12 @@ import { defineLeaf } from '../../core/command.js';
2
2
  import { InputError } from '../../core/io.js';
3
3
  import { spawnNode } from '../../core/runtime/nodes.js';
4
4
  import { interactionDir } from '../../core/artifact.js';
5
- import { isInTmux, spawnAndDetach } from '../../core/spawn.js';
5
+ import { isInTmux } from '../../core/spawn.js';
6
6
  import { mkdirSync, existsSync } from 'node:fs';
7
7
  import { join, resolve } from 'node:path';
8
8
  import { randomBytes } from 'node:crypto';
9
9
  import { validateDeck, approveDeck, notifyDeck, atomicWriteJson, deckPath, display, } from '@crouton-kit/humanloop';
10
- import { DECK_SCHEMA_HINT, followUpReview, spawnHumanJob, pickPlacement, runCmd, resolveMaxPanes, } from './shared.js';
10
+ import { DECK_SCHEMA_HINT, followUpReview, spawnHumanJob, detachHumanTui, resolveMaxPanes, } from './shared.js';
11
11
  /** The asking node's id, or null when run from a bare shell (no parent to route to). */
12
12
  function askingNode() {
13
13
  return process.env['CRTR_NODE_ID'] ?? null;
@@ -212,13 +212,7 @@ export const humanNotify = defineLeaf({
212
212
  atomicWriteJson(join(idir, 'run.json'), rc);
213
213
  let shown = false;
214
214
  if (isInTmux()) {
215
- const spawn = spawnAndDetach({
216
- command: runCmd(idir),
217
- cwd,
218
- placement: pickPlacement(),
219
- killAfterSeconds: 0,
220
- });
221
- shown = spawn.status === 'spawned';
215
+ shown = detachHumanTui(idir, cwd).status === 'spawned';
222
216
  }
223
217
  return { shown, dir: idir };
224
218
  },
@@ -1,3 +1,4 @@
1
+ import { type DetachResult } from '../../core/spawn.js';
1
2
  export declare const DECK_SCHEMA_HINT: string;
2
3
  export interface RunRecord {
3
4
  mode: 'ask' | 'approve' | 'notify' | 'review';
@@ -9,7 +10,31 @@ export interface RunRecord {
9
10
  pane_id?: string;
10
11
  }
11
12
  export declare function resolveMaxPanes(): number;
12
- export declare function pickPlacement(): 'split-h' | 'new-window';
13
+ export declare function pickPlacement(targetPane?: string): 'split-h' | 'new-window';
14
+ /**
15
+ * The tmux pane the humanloop TUI should open BESIDE so a PERSON actually sees
16
+ * it. A prompt raised by a canvas node must NOT land in the backstage `crtr`
17
+ * session (the holding ground for un-watched node panes) — it lands in the
18
+ * session the user is watching that node's graph in:
19
+ * 1. node prompt (CRTR_NODE_ID set) → the HIGHEST FOCUSED node of its graph
20
+ * (`graphSurfaceTarget`, the focused node closest to the graph root): split
21
+ * beside it, in the session/window the user already has it open in.
22
+ * 2. nothing in the graph on screen (or a dead focus pane), or a bare-shell
23
+ * prompt → the user's currently-attached pane (`attachedClientPane`).
24
+ * 3. neither resolvable → undefined (tmux falls back to the caller pane).
25
+ * The TUI lands in the right place but NEVER switches the user's session/window
26
+ * (no switch-client / select-window; new-window opens `-d`). They see it when
27
+ * they look at the node they are already watching.
28
+ */
29
+ export declare function resolveHumanTarget(): string | undefined;
30
+ /**
31
+ * Open the detached humanloop `_run` pane for an interaction dir, ROUTED to the
32
+ * session the user is watching (`resolveHumanTarget`). The single spawn path
33
+ * behind ask/approve/review (`spawnHumanJob`) and notify. `detached: true` keeps
34
+ * the user's view put — the prompt lands beside the watched node without
35
+ * jumping their session/window. `jobId`, when given, is injected as CRTR_JOB_ID.
36
+ */
37
+ export declare function detachHumanTui(idir: string, cwd: string, jobId?: string): DetachResult;
13
38
  export declare function runCmd(dir: string): string;
14
39
  export declare function followUpResult(_jobId: string): string;
15
40
  export declare function followUpDrain(_jobId: string): string;
@@ -2,7 +2,8 @@ import { readConfig } from '../../core/config.js';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { join } from 'node:path';
4
4
  import { atomicWriteJson, readJson } from '@crouton-kit/humanloop';
5
- import { countPanesInCurrentWindow, spawnAndDetach, shellQuote } from '../../core/spawn.js';
5
+ import { countPanesInWindow, spawnAndDetach, shellQuote, attachedClientPane, paneAlive, } from '../../core/spawn.js';
6
+ import { graphSurfaceTarget } from '../../core/runtime/placement.js';
6
7
  export const DECK_SCHEMA_HINT = 'Deck must match the humanloop deck schema: {title?, ' +
7
8
  'source?:{sessionName?,askedBy?,blockedSince?}, ' +
8
9
  'interactions:[{id,title,subtitle?,(body?|bodyPath?),options:[{id,label,' +
@@ -11,8 +12,51 @@ export const DECK_SCHEMA_HINT = 'Deck must match the humanloop deck schema: {tit
11
12
  export function resolveMaxPanes() {
12
13
  return readConfig('user').max_panes_per_window;
13
14
  }
14
- export function pickPlacement() {
15
- return countPanesInCurrentWindow() >= resolveMaxPanes() ? 'new-window' : 'split-h';
15
+ export function pickPlacement(targetPane) {
16
+ return countPanesInWindow(targetPane) >= resolveMaxPanes() ? 'new-window' : 'split-h';
17
+ }
18
+ /**
19
+ * The tmux pane the humanloop TUI should open BESIDE so a PERSON actually sees
20
+ * it. A prompt raised by a canvas node must NOT land in the backstage `crtr`
21
+ * session (the holding ground for un-watched node panes) — it lands in the
22
+ * session the user is watching that node's graph in:
23
+ * 1. node prompt (CRTR_NODE_ID set) → the HIGHEST FOCUSED node of its graph
24
+ * (`graphSurfaceTarget`, the focused node closest to the graph root): split
25
+ * beside it, in the session/window the user already has it open in.
26
+ * 2. nothing in the graph on screen (or a dead focus pane), or a bare-shell
27
+ * prompt → the user's currently-attached pane (`attachedClientPane`).
28
+ * 3. neither resolvable → undefined (tmux falls back to the caller pane).
29
+ * The TUI lands in the right place but NEVER switches the user's session/window
30
+ * (no switch-client / select-window; new-window opens `-d`). They see it when
31
+ * they look at the node they are already watching.
32
+ */
33
+ export function resolveHumanTarget() {
34
+ const nodeId = process.env['CRTR_NODE_ID'];
35
+ if (nodeId !== undefined && nodeId !== '') {
36
+ const f = graphSurfaceTarget(nodeId);
37
+ if (f !== null && f.pane !== null && paneAlive(f.pane))
38
+ return f.pane;
39
+ }
40
+ return attachedClientPane() ?? undefined;
41
+ }
42
+ /**
43
+ * Open the detached humanloop `_run` pane for an interaction dir, ROUTED to the
44
+ * session the user is watching (`resolveHumanTarget`). The single spawn path
45
+ * behind ask/approve/review (`spawnHumanJob`) and notify. `detached: true` keeps
46
+ * the user's view put — the prompt lands beside the watched node without
47
+ * jumping their session/window. `jobId`, when given, is injected as CRTR_JOB_ID.
48
+ */
49
+ export function detachHumanTui(idir, cwd, jobId) {
50
+ const targetPane = resolveHumanTarget();
51
+ return spawnAndDetach({
52
+ command: runCmd(idir),
53
+ cwd,
54
+ ...(jobId !== undefined ? { jobId } : {}),
55
+ placement: pickPlacement(targetPane),
56
+ killAfterSeconds: 0,
57
+ detached: true,
58
+ ...(targetPane !== undefined ? { targetPane } : {}),
59
+ });
16
60
  }
17
61
  export function runCmd(dir) {
18
62
  return `CRTR_HUMAN_DIR=${shellQuote(dir)} crtr human _run`;
@@ -51,13 +95,7 @@ export function followUpReview(_jobId) {
51
95
  * on run.json (not returned) so `human cancel` can later kill the TUI.
52
96
  */
53
97
  export function spawnHumanJob(jobId, idir, cwd) {
54
- const spawn = spawnAndDetach({
55
- command: runCmd(idir),
56
- cwd,
57
- jobId,
58
- placement: pickPlacement(),
59
- killAfterSeconds: 0,
60
- });
98
+ const spawn = detachHumanTui(idir, cwd, jobId);
61
99
  if (spawn.status !== 'spawned') {
62
100
  return { spawned: false, follow_up: followUpDrain(jobId) };
63
101
  }