@crouton-kit/crouter 0.3.21 → 0.3.23

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 (111) hide show
  1. package/dist/commands/canvas-history/read.d.ts +1 -0
  2. package/dist/commands/canvas-history/read.js +61 -0
  3. package/dist/commands/canvas-history/search.d.ts +1 -0
  4. package/dist/commands/canvas-history/search.js +270 -0
  5. package/dist/commands/canvas-history/show.d.ts +1 -0
  6. package/dist/commands/canvas-history/show.js +79 -0
  7. package/dist/commands/canvas-history.d.ts +2 -0
  8. package/dist/commands/canvas-history.js +33 -0
  9. package/dist/commands/canvas.js +3 -2
  10. package/dist/commands/memory/find.js +7 -40
  11. package/dist/commands/memory/lint.js +19 -34
  12. package/dist/commands/memory/list.js +8 -18
  13. package/dist/commands/memory/read.js +4 -37
  14. package/dist/commands/pkg/bridge.d.ts +1 -0
  15. package/dist/commands/pkg/bridge.js +137 -0
  16. package/dist/commands/pkg/plugin-inspect.js +7 -6
  17. package/dist/commands/pkg.js +2 -1
  18. package/dist/commands/skill/author.js +35 -44
  19. package/dist/commands/skill/shared.d.ts +1 -5
  20. package/dist/commands/skill/shared.js +9 -63
  21. package/dist/commands/skill.js +5 -6
  22. package/dist/commands/sys/doctor.js +6 -144
  23. package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
  24. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
  25. package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
  26. package/dist/core/__tests__/child-death-wake.test.js +133 -185
  27. package/dist/core/__tests__/daemon-liveness.test.js +28 -258
  28. package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
  29. package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
  30. package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
  31. package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
  32. package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
  33. package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
  34. package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
  35. package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +1 -1
  36. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
  37. package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
  38. package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +5 -5
  39. package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
  40. package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
  41. package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
  42. package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
  43. package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
  44. package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
  45. package/dist/core/__tests__/full/spike-harness.test.js +117 -0
  46. package/dist/core/__tests__/grace-clock.test.js +72 -75
  47. package/dist/core/__tests__/helpers/harness.d.ts +35 -1
  48. package/dist/core/__tests__/helpers/harness.js +70 -12
  49. package/dist/core/__tests__/live-mutation-verbs.test.js +99 -104
  50. package/dist/core/__tests__/live-mutation.test.js +111 -134
  51. package/dist/core/__tests__/revive.test.js +96 -86
  52. package/dist/core/__tests__/subscription-delivery.test.js +116 -138
  53. package/dist/core/__tests__/wake-origin.test.js +54 -50
  54. package/dist/core/bridge-map.d.ts +19 -0
  55. package/dist/core/bridge-map.js +73 -0
  56. package/dist/core/canvas/history.d.ts +91 -0
  57. package/dist/core/canvas/history.js +389 -0
  58. package/dist/core/canvas/index.d.ts +1 -0
  59. package/dist/core/canvas/index.js +1 -0
  60. package/dist/core/config.js +3 -22
  61. package/dist/core/memory-resolver.d.ts +9 -2
  62. package/dist/core/memory-resolver.js +47 -7
  63. package/dist/core/resolver.d.ts +1 -30
  64. package/dist/core/resolver.js +6 -546
  65. package/dist/core/runtime/persona.js +5 -7
  66. package/dist/core/scope.d.ts +7 -5
  67. package/dist/core/scope.js +16 -17
  68. package/dist/core/substrate/ceiling.d.ts +28 -0
  69. package/dist/core/substrate/ceiling.js +87 -0
  70. package/dist/core/substrate/index.d.ts +2 -0
  71. package/dist/core/substrate/index.js +1 -0
  72. package/dist/core/substrate/on-read.js +18 -2
  73. package/dist/core/substrate/render.d.ts +2 -3
  74. package/dist/core/substrate/render.js +63 -51
  75. package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
  76. package/dist/types.d.ts +3 -18
  77. package/dist/types.js +0 -8
  78. package/package.json +4 -3
  79. package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
  80. package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
  81. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
  82. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
  83. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
  84. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
  85. package/dist/builtin-skills/skills/design/SKILL.md +0 -51
  86. package/dist/builtin-skills/skills/development/SKILL.md +0 -109
  87. package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
  88. package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
  89. package/dist/commands/skill/state.d.ts +0 -3
  90. package/dist/commands/skill/state.js +0 -71
  91. package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
  92. package/dist/core/__tests__/broker-dialogs.test.js +0 -126
  93. package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
  94. package/dist/core/__tests__/resolver.test.js +0 -181
  95. package/dist/core/__tests__/spike-harness.test.js +0 -242
  96. /package/dist/core/__tests__/{broker-attach-limits.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
  97. /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
  98. /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
  99. /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
  100. /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
  101. /package/dist/core/__tests__/{cascade-close.test.d.ts → full/cascade-close.test.d.ts} +0 -0
  102. /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
  103. /package/dist/core/__tests__/{detach-focus.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
  104. /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/detach-focus.test.d.ts} +0 -0
  105. /package/dist/core/__tests__/{placement-focus.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
  106. /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/placement-focus.test.d.ts} +0 -0
  107. /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
  108. /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-revive.test.d.ts} +0 -0
  109. /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
  110. /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
  111. /package/dist/core/__tests__/{spike-harness.test.d.ts → full/spike-harness.test.d.ts} +0 -0
@@ -1,109 +0,0 @@
1
- ---
2
- name: development
3
- type: playbook
4
- description: Use when shaping or reshaping a build roadmap — choosing a development style, selecting a phase skeleton, or setting exit criteria for a software goal.
5
- ---
6
-
7
- # Development Playbook
8
-
9
- ## Development Styles
10
-
11
- Pick one style as your primary frame before you write phases. Each fits a different risk/knowledge profile.
12
-
13
- **Vertical slice.** Start with the thinnest path end-to-end — one real request touching every layer — before thickening any of them. Use when the integration seams are the riskiest unknowns and a working skeleton keeps the team aligned on "done". Fits new features where you know what to build but not how the layers will talk.
14
-
15
- **Spike-then-harden.** Build a throwaway prototype of the one thing you don't understand, validate the approach, then discard it and build it properly. Use when there is a genuine technical unknown (unfamiliar API, unclear performance profile, novel algorithm) that blocks everything else. The spike is not the deliverable — the hardened version is.
16
-
17
- **Test-first.** Write the failing test before the implementation for every unit of logic. Use when the requirements are precise and stable (a parser, a data transform, a well-specified algorithm). Do not apply to exploratory or UI-heavy work where the spec is discovered by building.
18
-
19
- **Strangler-fig.** Introduce a new implementation path alongside the old one, route traffic to it incrementally, and delete the old path when migration is complete. Use for migrations and rewrites where you cannot replace atomically and must maintain a working system throughout.
20
-
21
- **Bottom-up.** Build foundational primitives first; compose them into higher-order behaviour last. Use when building a library or shared infrastructure where the interface must be right before consumers are written. Risky if the top-level requirements aren't settled — you may build the wrong primitives.
22
-
23
- **Decision rule:** if the riskiest unknown is technical feasibility, spike first. If it is integration correctness, vertical slice. If requirements are precise and logic-heavy, test-first. If it is a live-system migration, strangler-fig. If it is a foundational library with settled requirements, bottom-up. Default to vertical slice for ambiguous new feature work.
24
-
25
- ---
26
-
27
- ## Roadmap Shapes by Scenario
28
-
29
- These are concrete phase skeletons. Adapt names and granularity; don't add phases that serve no exit criterion.
30
-
31
- ### New feature
32
- 1. **Explore** — map the affected subsystems, identify entry points and constraints, produce `context/explore.md`.
33
- 2. **Spec** — define the interface, behaviour, and acceptance criteria; output `context/spec.md`.
34
- 3. **Plan** — decompose spec into file-level tasks with dependency order; output `context/plan.md`.
35
- 4. **Vertical slice** — implement the thinnest end-to-end path; validate it works before widening.
36
- 5. **Harden** — fill out the remaining logic, edge cases, error paths.
37
- 6. **Review** — non-implementer critique pass on the whole surface.
38
- 7. **Fix** — action review findings.
39
- 8. **Validate** — end-to-end confirmation against spec's acceptance criteria.
40
-
41
- ### Refactor
42
- 1. **Characterise** — write or identify tests that describe current behaviour; they must pass before and after.
43
- 2. **Plan safe steps** — decompose into the smallest semantics-preserving transformations; each step independently reviewable.
44
- 3. **Transform** — apply each step, running the characterisation suite after each one.
45
- 4. **Verify equivalence** — confirm no observable behaviour changed; review for unintended scope drift.
46
-
47
- ### Bug-fix campaign
48
- 1. **Reproduce** — produce a reliable reproduction case for each bug; nothing proceeds without one.
49
- 2. **Root cause** — trace the defect to its source; group bugs sharing a root cause.
50
- 3. **Fix** — implement the minimal correct change; no opportunistic cleanups in the same commit.
51
- 4. **Regression test** — add a test that would have caught this.
52
- 5. **Validate** — confirm the reproduction case no longer triggers.
53
-
54
- ### Greenfield
55
- 1. **Explore/research** — understand the problem domain, constraints, and comparable systems.
56
- 2. **Spec** — define the interface and top-level behaviour in enough detail to plan.
57
- 3. **Architecture decision** — commit to the structural shape; record in `context/architecture.md`.
58
- 4. **Spike** (if technical unknowns exist) — validate the risky piece before building around it.
59
- 5. **Bottom-up build** — primitives first, then composition; validate each layer before building on it.
60
- 6. **Integration** — assemble layers; validate end-to-end.
61
- 7. **Review + fix** — critique full surface; action findings.
62
-
63
- ### Migration / upgrade
64
- 1. **Inventory** — enumerate every call site, every affected API, every integration point.
65
- 2. **Compatibility plan** — decide the strangler-fig boundary; define the coexistence period.
66
- 3. **New path** — implement the replacement without removing the old.
67
- 4. **Route incrementally** — shift traffic or call sites in small batches; validate after each batch.
68
- 5. **Delete old path** — only after full migration is confirmed.
69
- 6. **Validate** — confirm nothing regressed; run the full integration surface.
70
-
71
- ### Performance work
72
- 1. **Baseline** — measure and record current performance numbers; define the target.
73
- 2. **Profile** — identify the actual bottleneck; do not optimise before you know where the heat is.
74
- 3. **Fix the bottleneck** — targeted change only; no speculative optimisation.
75
- 4. **Measure again** — confirm the target is met against the same baseline method.
76
- 5. **Review** — check that the fix doesn't introduce correctness or maintainability regressions.
77
-
78
- ---
79
-
80
- ## Setting Exit Criteria per Phase
81
-
82
- Every phase needs a concrete, evaluable condition that tells you it is genuinely done — not "looks good" or "mostly working". Write exit criteria when you write the phase, not after.
83
-
84
- - **Explore:** a context doc exists that accurately describes the relevant subsystem; a reviewer or subsequent spec agent should not need to re-explore to write the spec.
85
- - **Spec:** acceptance criteria are concrete enough that an implementer can derive test cases from them without ambiguity.
86
- - **Plan:** every task maps to identified files; no task says "figure out how"; dependencies are explicit.
87
- - **Implementation:** the code compiles, all existing tests pass, and the acceptance criteria from the spec are provably met (by tests or by a validation agent's manual check).
88
- - **Review:** a non-implementer has read the diff and produced a report; all Major and Critical findings are addressed.
89
- - **Validation:** end-to-end confirmation against the spec's acceptance criteria passes in the real runtime, not just in isolation.
90
-
91
- If you cannot write a concrete exit criterion for a phase, the phase is underspecified — split it or spec it further before adding it to the roadmap.
92
-
93
- ---
94
-
95
- ## The Build-Cycle Discipline
96
-
97
- This is the delegation pipeline from spec to shipped, with the coupling that makes it rigorous.
98
-
99
- **Spec → Plan.** The plan agent receives the spec as input; it does not re-derive requirements. If the spec is ambiguous, the plan agent reports the ambiguity — the orchestrator resolves it and re-delegates, not the plan agent by guessing.
100
-
101
- **Plan → Implement (parallel where safe).** Tasks with disjoint file sets run concurrently. Before spawning parallel implementers, verify file-level independence; if two tasks touch the same file, serialize them. Every implementation agent receives: the goal in one sentence, its specific task and done condition, the relevant context files by path, and the e2e validation recipe.
102
-
103
- **Implement → Review (non-implementer).** The reviewer receives the full diff and the relevant context docs. It produces a report sorted by severity — Critical, Major, Minor — and does not propose fixes inline. One review pass per implementation batch; do not re-review after fixes, validate instead.
104
-
105
- **Review → Fix.** The orchestrator triages the report, skips false positives, and delegates fix agents pointing at the report path. Fix agents read the findings, understand the code, and implement the correct fix — they are not given line-by-line instructions. Do not spawn a second reviewer after fixes land.
106
-
107
- **Fix → Validate.** Validation confirms the thing works end-to-end in the real runtime. It is distinct from tests passing — it exercises the integrated system. If validation fails, spawn fix agents against the failure, re-validate. Do not advance to the next phase until validation passes.
108
-
109
- **When review or validation exposes a phase gap** — a wrong assumption in the spec, a plan that missed a dependency, an implementation that reveals the design is wrong — re-delegate the affected phase rather than patching forward. A corrected spec or plan paid for in one extra wake costs less than an implementation built on a bad foundation.
@@ -1,59 +0,0 @@
1
- ---
2
- name: planning
3
- type: playbook
4
- description: Use when shaping a planning roadmap, deciding plan structure, or fanning out plan-review specialists before declaring a plan ready.
5
- ---
6
-
7
- # Planning Playbook
8
-
9
- ## Plan Shapes and the Decomposition Decision
10
-
11
- Every planning effort produces either a flat plan or a decomposed plan (index + part-plans). Choosing the wrong shape wastes a cycle — a flat plan that is too large forces an implementer to hold too much at once; a decomposed plan for something small adds overhead for no gain.
12
-
13
- **Use a flat plan** when the work is a single coherent domain, involves fewer than ~6 files, and can be written at consistent task granularity without exceeding roughly 150–200 lines. A flat plan has an overview, ordered phases, and a verification section. No sub-plans. One file.
14
-
15
- **Use a decomposed plan** when the change spans multiple domains (e.g., data layer, API surface, UI), involves 6+ files, or would require a master plan that cannot be written at consistent granularity without ballooning. In this case: produce an index plan (the navigable master) and delegate each domain slice to a `plan`-kind child node, giving each child its slice scope, the relevant portion of the spec, and its place in the dependency graph. A slice that itself decomposes further — multiple sub-domains, more than one window's worth of planning — goes to a `plan` sub-orchestrator created directly (`crtr node new --kind plan --mode orchestrator`), not a base child relied on to promote itself. The index plan is the synthesis artifact — it lists all sub-plans by path, defines phases and their dependencies, and contains a task table the implementation orchestrator can execute directly. Detail lives in sub-plans; the master is not allowed to carry it.
16
-
17
- **The decomposition trigger is domain boundary, not size alone.** Three backend files and three frontend files are two domains even if the total count is modest — plan them separately and synthesize, because the integration seam is where bugs live and one agent reading both halves won't catch them as cleanly as two agents each going deep.
18
-
19
- After collecting part-plans from children, synthesize before declaring done: resolve file ownership conflicts (two sub-plans naming the same file means you decide the sequence), align naming across all parts, fill integration gaps at domain boundaries, and ensure the task table in the index accurately reflects dependencies exposed only by reading all sub-plans together.
20
-
21
- ## What a Good Task Looks Like
22
-
23
- A task is the atomic unit a single implementation node picks up and executes in one context window. Write tasks so that any implementation agent can pick one up cold and know exactly what to do.
24
-
25
- A good task has: a file path (or a small list of paths it exclusively owns), an explicit statement of what changes in that file, a list of its hard dependencies (which other tasks must land first), and a clear output — what type, what function signature, what export the next task can assume exists. If a task requires a type defined by a sibling task in the same phase, that dependency is explicit in the task row.
26
-
27
- A good task is **parallel-safe**: its files are not owned by another task in the same phase. If two tasks must touch the same file, serialize them across phases and say so. A task that shares files without serialization is a merge conflict waiting to happen.
28
-
29
- A good task is **bounded**: an implementation agent should be able to finish it in one context window without needing to re-read the entire plan. If a task description runs longer than a short paragraph, the task is too large — split it.
30
-
31
- ## Plan-Review Specialist Roster
32
-
33
- Before declaring any plan ready for implementation, fan out the following reviewers as `review`-kind child nodes in parallel. Each reviewer checks one lens; running them together catches what no single pass can. Do not proceed to the implementation phase until all reviewer findings are folded back in — deferred findings become implementation bugs.
34
-
35
- ### Requirements Coverage
36
- **What it checks:** Every requirement and design constraint maps to a concrete plan task; nothing is invented; nothing is missed. Specifically: API routes, data model fields, UI states (loading, empty, error), error handling, and edge cases called out in the spec all have explicit plan tasks.
37
- **Spawn task template:** "Review the plan at `<path>` against requirements `<req-path>` and design `<design-path>`. Check that every requirement and design constraint has a concrete, actionable plan section. Classify each as Covered / Partial / Missing. Flag blocking gaps only."
38
-
39
- ### Pattern Consistency
40
- **What it checks:** The plan honours the codebase's established architecture, module structure, naming conventions, error-handling utilities, API response shapes, and frontend patterns. Deviations that would confuse an implementer or create inconsistency are flagged.
41
- **Spawn task template:** "Review the plan at `<path>` for pattern consistency against the codebase. Check architecture conventions, naming, error handling, API shapes, and frontend patterns. Read source files in the areas the plan touches; do not review in isolation. Flag deviations that contradict established patterns."
42
-
43
- ### Code Smells / Design
44
- **What it checks:** Nullability mismatches between plan and data source, type conflicts across sub-plans, hidden N+1 queries, over-fetching, missing error boundaries in batch operations, leaky abstractions that couple unrelated concerns, file-ownership conflicts when multiple sub-plans name the same file.
45
- **Spawn task template:** "Review the plan at `<path>` for design problems: nullability mismatches, N+1 queries, type conflicts between parts, over-fetching, missing error boundaries, leaky abstractions. Read existing code in target areas. Report concrete issues only — no style or speculation."
46
-
47
- ### Security
48
- **What it checks:** Input validation gaps (missing length limits, type constraints, enum checks), injection surfaces (raw SQL, shell, path traversal), missing auth/authz guards, data exposure in planned responses, and race conditions or TOCTOU bugs in planned state mutations. Only flags risks with a concrete exploit path in the plan.
49
- **Spawn task template:** "Review the plan at `<path>` for security risks. Check input validation, injection surfaces, auth/authz coverage, data exposure, and race conditions. Only flag risks with a concrete exploit path — no theoretical concerns."
50
-
51
- ### Architecture Fit
52
- **What it checks:** The plan's proposed boundaries — new files, new modules, new abstractions — fit the system's existing decomposition. A new service that duplicates an existing one, a new abstraction layer that cuts across established boundaries, or a module proposed in the wrong layer are all findings.
53
- **Spawn task template:** "Review the plan at `<path>` for architecture fit against the existing system. Check whether proposed file locations, module boundaries, and abstractions align with how the codebase is currently decomposed. Flag new units that duplicate existing ones or violate established layer boundaries."
54
-
55
- ## Folding Findings Back In
56
-
57
- After all reviewers report, collect their findings, triage by severity, and revise the plan before advancing. Critical and High findings must be resolved — either fix the plan or document in the task that the implementer must handle a specific constraint. Medium findings are addressed where straightforward; explicitly carried as a note in the relevant task when they are implementation-time concerns rather than plan-shape concerns. Do not dismiss findings without a reason.
58
-
59
- A plan that passes all five lenses with no unresolved Critical or High findings is ready to hand to an implementation orchestrator.
@@ -1,83 +0,0 @@
1
- ---
2
- name: spec
3
- type: playbook
4
- description: Use when running a specification effort, shaping a spec roadmap, or deciding how to stage design and requirements work. Covers the three-stage shape→design→requirements methodology, when to delegate design to a child node, the isolation principle behind the design/requirements split, and what a finished spec contains.
5
- ---
6
-
7
- ## The Three Stages
8
-
9
- A specification effort runs in exactly this order: **SHAPE** → **DESIGN** → **REQUIREMENTS**. Do not collapse them, skip ahead, or run them in parallel. Each stage has a gate; the next stage starts only when that gate is met.
10
-
11
- ### Stage 1 — Shape
12
-
13
- Shape is the only stage that is genuinely interactive. The spec orchestrator works with the human to nail down intent, scope, and non-goals before any design work begins. The deliverable is not an artifact — it is a shared mental model sufficient to write a sharp design brief.
14
-
15
- Run an inquiry loop: name the most important ambiguity, form a provisional take, offer 2–4 concrete options, get a decision. Track these turns carefully. The shape stage is done when: (1) 3–7 named components or functional areas are identified, (2) the user's intent can be restated without correction, and (3) no unresolved contradictions remain between the user's goal and the existing codebase. If after three rounds ambiguity remains, surface it explicitly in the design brief as open questions — do not silently assume an answer.
16
-
17
- Gate: human confirms readiness to proceed to design.
18
-
19
- ### Stage 2 — Design
20
-
21
- Design produces the blueprint: components and their topology, end-to-end flows, files and directories affected, locked decisions, and open questions resolved. The altitude is infra/services — no function signatures, no algorithm descriptions, no implementation ordering. Design answers "what shape does this take?" — planning answers "how is it built?"
22
-
23
- Small or simple design work (one surface, clear scope, few components) can be done by a single `design`-kind child node. Large or complex design work — multi-surface features, multiple interacting subsystems, significant architectural choices — must be delegated to a **design orchestrator** (a `design`-kind node created directly with `--mode orchestrator`), which decomposes the design internally and returns a finished artifact. The trigger for spawning a design orchestrator rather than a base design node: if the design effort has more than one distinct phase or more than ~5 interacting components, use an orchestrator.
24
-
25
- Gate: human approves the rendered design artifact.
26
-
27
- ### Stage 3 — Requirements
28
-
29
- Requirements are derived from the finished, approved design. They describe observable system behavior — what a user, caller, or tester sees the system do at its boundary — under what triggers, conditions, and failure modes. Each requirement is written in EARS format (WHEN/WHILE/IF/WHERE + SHALL). Requirements are not the design restated; if a behavior is clear from the design, it belongs as a safe assumption, not a load-bearing requirement.
30
-
31
- Delegate requirements writing to a terminal `spec` agent (base lifecycle). Pass it the rendered design text only. Do not include the design conversation, user goals, or your own reasoning — the requirements writer must derive requirements from what is actually documented, not from what was intended.
32
-
33
- Gate: human reviews and approves all load-bearing requirements; no `rejected` or unresolved `draft` items remain.
34
-
35
- ---
36
-
37
- ## The Design/Requirements Split — Why Isolation Matters
38
-
39
- Requirements written by the same context that argued out the design carry that context's blind spots. If the design left a behavior ambiguous and the design author filled it in mentally, requirements derived from that same mental state will encode the assumption without surfacing it for review. Written by a fresh context against the rendered design document alone, ambiguous points surface as gaps in `agentNotes` rather than silently-inherited assumptions.
40
-
41
- The isolation is structural, not stylistic. The requirements writer receives: the rendered design text and an output path. Nothing else. No user goal, no exploration findings, no conversation history. If something the user "intended" is not written in the design, it does not appear in the requirements — and that absence becomes visible, which is the desired outcome.
42
-
43
- ---
44
-
45
- ## The Yield-Between-Runs Rule
46
-
47
- After the design is approved, the spec orchestrator runs `crtr node yield` before starting requirements work. This is mandatory, not optional.
48
-
49
- Why: the design conversation fills context with reasoning about tradeoffs, rejected alternatives, and design intent. That context biases delegation — it causes the orchestrator to frame the requirements task with assumptions from the design discussion. After yielding, the orchestrator revives fresh against `context/roadmap.md`, which records the finished design artifact path. It reads the design artifact cold and delegates the requirements work from that clean window, anchored on the rendered design rather than on the design conversation.
50
-
51
- The roadmap must record the design artifact path and the current stage before yielding. On revive, the first action is to read `context/roadmap.md`, confirm the design is landed, and delegate requirements work.
52
-
53
- ---
54
-
55
- ## Roadmap Shape for a Spec Effort
56
-
57
- When shaping the roadmap at the start, structure it as follows. The goal section states what is being specified and for whom. Scope assumptions record what is in scope and what is not — a non-goal stated here propagates to every child without restating it. `## Strategy / phases` holds exactly three phases: Shape (gate: human sign-off), Design (gate: design artifact approved), Requirements (gate: all requirements approved). The current phase carries a one-line status of where it stands; completed phases are deleted, not summarized.
58
-
59
- After yield-and-revive, `## Strategy / phases` plus `## Active context` must let the fresh orchestrator orient in one pass without reading any child reports: the current phase's status line names what's in flight and which gate it's waiting on, and `## Active context` lists the design artifact and any other live context-file paths. Human-confirmed decisions and design detail fold into those context files, not the roadmap.
60
-
61
- ---
62
-
63
- ## Delegating Design: Base Node vs. Orchestrator
64
-
65
- Spawn a base `design` node (terminal) when: the design surface is bounded, one component or subsystem, no multi-phase structure required. The node produces `context/design.md` and `context/design.json` and returns.
66
-
67
- Spawn a `design` orchestrator (resident) when: the feature spans multiple subsystems, has distinct implementation phases that need separate design treatment, or the design effort is itself likely to fill one context window before it's finished. Create it directly as an orchestrator — `crtr node new --kind design --mode orchestrator` — rather than spawning a base design node and counting on it to promote itself once it discovers the surface is too big; self-promotion is unreliable, and a node born an orchestrator is strictly more capable than one hoping to become one. Pass it the shape brief as its goal; it owns the decomposition and integration internally and reports a finished design artifact when done.
68
-
69
- In either case, the spec orchestrator waits for the design to land and the human to approve it before proceeding.
70
-
71
- ---
72
-
73
- ## What a Finished Spec Contains
74
-
75
- A finished spec is precise enough that a planner can produce an implementation task breakdown without guessing intent. It contains:
76
-
77
- - **Behavior** — what the system does at its external boundary, organized by functional area, written in EARS format.
78
- - **Non-goals** — what is explicitly out of scope, so planners and implementers don't expand into it.
79
- - **Interfaces / inputs / outputs** — the data shapes and interaction contracts (at semantic-type level, not TypeScript declarations).
80
- - **Edge cases** — the failure modes, boundary conditions, and unusual states that must be handled, surfaced explicitly rather than left to the implementer to discover.
81
- - **Acceptance criteria** — per-requirement, testable conditions: "given input X, observe output Y" or "given state X, observe behavior Y."
82
-
83
- A spec that requires the reader to infer intent, assume behavior, or resolve design questions is not finished. If those gaps remain at the end of Stage 3, surface them explicitly as open questions before pushing final.
@@ -1,3 +0,0 @@
1
- export declare const stateEnable: import("../../core/command.js").LeafDef;
2
- export declare const stateDisable: import("../../core/command.js").LeafDef;
3
- export declare const stateBranch: import("../../core/command.js").BranchDef;
@@ -1,71 +0,0 @@
1
- import { defineLeaf, defineBranch } from '../../core/command.js';
2
- import { skillConfigKey } from '../../types.js';
3
- import { resolveSkill } from '../../core/resolver.js';
4
- import { requireScopeRoot } from '../../core/scope.js';
5
- import { updateConfig, ensureScopeInitialized } from '../../core/config.js';
6
- import { resolveWriteScope } from './shared.js';
7
- async function toggleSkill(input, enabled) {
8
- const nameRaw = input['name'];
9
- const scopeStr = input['scope'];
10
- const scope = resolveWriteScope(scopeStr);
11
- const skillObj = resolveSkill(nameRaw);
12
- const key = skillConfigKey(skillObj.plugin, skillObj.name);
13
- const scopeRootPath = requireScopeRoot(scope);
14
- ensureScopeInitialized(scope, scopeRootPath);
15
- updateConfig(scope, (cfg) => {
16
- cfg.skills[key] = { enabled };
17
- });
18
- return { name: skillObj.name, scope, enabled };
19
- }
20
- export const stateEnable = defineLeaf({
21
- name: 'enable',
22
- description: 'enable a skill',
23
- whenToUse: 're-enable a skill that was previously disabled, making it visible to list and agent discovery again in the target scope.',
24
- help: {
25
- name: 'skill state enable',
26
- summary: 'enable a skill in the given scope',
27
- params: [
28
- { kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read.' },
29
- { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
30
- ],
31
- output: [
32
- { name: 'name', type: 'string', required: true, constraint: 'Resolved skill name.' },
33
- { name: 'scope', type: 'string', required: true, constraint: 'Scope where the enable was applied.' },
34
- { name: 'enabled', type: 'boolean', required: true, constraint: 'Always true.' },
35
- ],
36
- outputKind: 'object',
37
- effects: ['Writes the skill enable flag to config.json in the target scope.'],
38
- },
39
- run: async (input) => toggleSkill(input, true),
40
- });
41
- export const stateDisable = defineLeaf({
42
- name: 'disable',
43
- description: 'disable a skill',
44
- whenToUse: 'hide a skill from list and agent discovery without deleting it — writes a disable flag to config.json in the target scope; reverse it later with `crtr skill state enable`.',
45
- help: {
46
- name: 'skill state disable',
47
- summary: 'disable a skill in the given scope, hiding it from list and agent discovery',
48
- params: [
49
- { kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read.' },
50
- { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
51
- ],
52
- output: [
53
- { name: 'name', type: 'string', required: true, constraint: 'Resolved skill name.' },
54
- { name: 'scope', type: 'string', required: true, constraint: 'Scope where the disable was applied.' },
55
- { name: 'enabled', type: 'boolean', required: true, constraint: 'Always false.' },
56
- ],
57
- outputKind: 'object',
58
- effects: ['Writes the skill disable flag to config.json in the target scope.'],
59
- },
60
- run: async (input) => toggleSkill(input, false),
61
- });
62
- export const stateBranch = defineBranch({
63
- name: 'state',
64
- description: 'enable or disable skills',
65
- whenToUse: 'turn a skill on or off in a scope — disable to hide it from discovery without removing it, enable to bring it back.',
66
- help: {
67
- name: 'skill state',
68
- summary: 'enable or disable skills',
69
- },
70
- children: [stateEnable, stateDisable],
71
- });
@@ -1,116 +0,0 @@
1
- // Run with: node --import tsx/esm --test src/core/__tests__/broker-crash-teardown.test.ts
2
- //
3
- // Broker crash / teardown / boot-failure — acceptance items 4–6 of the headless-
4
- // broker migration plus the M-1/M-2 boot-failure regression. Split out of
5
- // broker-lifecycle.test.ts (see its header for the full file map); the tests are
6
- // the original acceptance gate, unchanged, on their own isolated harness:
7
- // 4 survive a broker crash via grace-revive RESUME on the saved .jsonl → "CRASH"
8
- // 5 tear down cleanly — close → shutdown frame → exit, socket unlinked → "clean teardown"
9
- // 6 ONE-WRITER (R2) — never two engine pids alive across crash→grace-revive → "CRASH"
10
- // M-1/M-2 a broker that dies before session_start is reaped, not stranded → "boot failure"
11
- import { test, before, after } from 'node:test';
12
- import assert from 'node:assert/strict';
13
- import { existsSync, readFileSync } from 'node:fs';
14
- import { join } from 'node:path';
15
- import { createHarness, hasTmux } from './helpers/harness.js';
16
- import { isPidAlive } from '../canvas/pid.js';
17
- import { FAIL_BEFORE_SESSION_START } from './fixtures/fake-engine.js';
18
- // crtrd.ts module const (not exported). The fresh-pi-boot grace window the daemon
19
- // waits before grace-reviving a pi observed dead. Reference: crtrd.ts
20
- // `REVIVE_GRACE_MS = 20_000`.
21
- const REVIVE_GRACE_MS = 20_000;
22
- let h;
23
- let root;
24
- before(async () => {
25
- if (!hasTmux())
26
- return;
27
- h = await createHarness({ sessionPrefix: 'crtr-brkcrash' });
28
- root = h.spawnRoot('broker-crash suite root');
29
- });
30
- after(async () => {
31
- if (h !== undefined)
32
- await h.dispose();
33
- });
34
- // ===========================================================================
35
- // Item 4 + 6 — broker CRASH → grace-revive RESUME on the saved .jsonl; ONE-
36
- // WRITER: the crashed pid is dead BEFORE the revive launches and never
37
- // resurrects, and the revived pid is distinct (never two engine pids at once).
38
- // ===========================================================================
39
- test('CRASH → grace-revive RESUME; one-writer (old pid dead before new, never resurrects)', { skip: !hasTmux() }, async () => {
40
- const id = await h.spawnHeadlessChild(root, 'headless worker — crash');
41
- const oldPid = h.node(id).pi_pid;
42
- assert.equal(isPidAlive(oldPid), true, 'broker alive before the crash');
43
- assert.equal(h.node(id).intent ?? null, null, 'fresh broker has a null intent (not refresh/idle-release)');
44
- const boots = h.bootCount(id);
45
- // Kill the broker out from under the daemon (a crash). Use a fixed clock so the
46
- // grace window is exercised deterministically.
47
- process.kill(oldPid, 'SIGKILL');
48
- await h.waitFor(() => !isPidAlive(oldPid), { label: 'crashed broker pid is dead' });
49
- // ONE-WRITER: the old engine pid is dead BEFORE any revive can launch.
50
- assert.equal(isPidAlive(oldPid), false, 'crashed pid dead before the daemon revives');
51
- const NOW = 5_000_000;
52
- await h.tick(NOW); // pid dead, intent null → handleBrokerLiveness → handleLiveWindow marks pending
53
- assert.equal(h.bootCount(id), boots, 'inside the grace window → NOT yet revived');
54
- await h.tick(NOW + REVIVE_GRACE_MS + 1); // grace elapsed → reviveNode(resume:true)
55
- const boot2 = await h.awaitBoot(id, { minCount: boots + 1 });
56
- assert.equal(boot2.resuming, true, 'grace-revive RESUMES the saved .jsonl');
57
- const newPid = h.node(id).pi_pid;
58
- assert.ok(newPid != null && isPidAlive(newPid), 'the revived broker pid is alive');
59
- assert.notEqual(newPid, oldPid, 'the revived pid is distinct from the crashed one');
60
- assert.equal(isPidAlive(oldPid), false, 'one-writer: the crashed pid never resurrected');
61
- });
62
- // ===========================================================================
63
- // Item 5 — clean teardown: close → hostFor(meta).teardown → `shutdown` frame →
64
- // broker disposes + exits, socket unlinked, status canceled, daemon leaves it.
65
- // ===========================================================================
66
- test('clean teardown — node close → broker shutdown frame → exit, socket unlinked, not revived', { skip: !hasTmux() }, async () => {
67
- const id = await h.spawnHeadlessChild(root, 'headless worker — teardown');
68
- const pid = h.node(id).pi_pid;
69
- const sock = h.brokerSock(id);
70
- await h.waitFor(() => existsSync(sock), { label: 'broker created its view.sock' });
71
- // Close via the REAL CLI (resolves the node from --node; the subprocess carries
72
- // no TMUX_PANE). closeNode marks canceled BEFORE teardown (crash-safe order),
73
- // then hostFor(broker).teardown sends the `shutdown` frame.
74
- const res = h.cli(root, ['node', 'close', '--node', id]);
75
- assert.equal(res.code, 0, `node close should exit 0\n--stderr--\n${res.stderr}`);
76
- await h.waitFor(() => !isPidAlive(pid), { label: 'broker process exited on shutdown frame' });
77
- await h.waitFor(() => !existsSync(sock), { label: 'broker unlinked its socket on exit' });
78
- assert.equal(h.status(id), 'canceled', 'the closed node is canceled');
79
- // listNodes only surfaces active|idle, so a canceled broker is never supervised.
80
- await h.tick();
81
- assert.equal(h.status(id), 'canceled', 'still canceled after a daemon tick');
82
- assert.equal(isPidAlive(pid), false, 'the broker stays dead — never revived');
83
- });
84
- // ===========================================================================
85
- // M-1 regression — a broker that DIES before session_start must surface a boot
86
- // failure, not strand the node forever. Before the fix handleBrokerLiveness read
87
- // pid==null UNCONDITIONALLY as "still booting", so a broker that threw before
88
- // recording a pid/session left the node 'active' with no engine FOREVER and the
89
- // parent waited on a dead child. The daemon must instead crash it past the boot
90
- // grace + push a boot-failure up the spine. (Also proves M-2: the broker's fatal
91
- // diagnostic reaches job/broker.log instead of /dev/null.)
92
- // ===========================================================================
93
- test('boot failure — a broker that dies before session_start is reaped, not stranded (review M-1/M-2)', { skip: !hasTmux() }, async () => {
94
- // A headless child whose broker throws before session_start (no pid, no session
95
- // EVER recorded). spawnHeadlessChildNoBoot does not awaitBoot (there is none).
96
- const id = await h.spawnHeadlessChildNoBoot(root, `headless worker — boot fail ${FAIL_BEFORE_SESSION_START}`);
97
- // M-2: the broker logs its fatal stack to job/broker.log and exits(1). Without
98
- // the stdio redirect this diagnostic would be lost to /dev/null.
99
- const brokerLog = join(h.home, 'nodes', id, 'job', 'broker.log');
100
- await h.waitFor(() => existsSync(brokerLog) && /simulated pre-session_start boot failure/.test(readFileSync(brokerLog, 'utf8')), { label: 'broker logged its pre-session_start failure to job/broker.log' });
101
- // The node is stuck 'active' with a null pid and a null session — the strand.
102
- const n0 = h.node(id);
103
- assert.equal(n0.pi_pid ?? null, null, 'no broker pid was ever recorded');
104
- assert.equal(n0.pi_session_id ?? null, null, 'no session was ever recorded');
105
- // Inside the boot grace the daemon LEAVES it (could be the sub-second boot gap).
106
- const NOW = 9_000_000;
107
- await h.tick(NOW);
108
- assert.equal(h.status(id), 'active', 'inside the boot grace → daemon leaves it (boot gap)');
109
- // Past the boot grace with STILL no pid and no session → crash + boot failure.
110
- await h.tick(NOW + REVIVE_GRACE_MS + 1);
111
- await h.waitForStatus(id, 'dead');
112
- assert.equal(h.status(id), 'dead', 'a never-booted broker is reaped, not stranded active forever');
113
- // …and the parent (an active subscriber of the child) was told up the spine.
114
- const note = h.inbox(root).find((e) => /never started/.test(e.label ?? ''));
115
- assert.ok(note !== undefined, 'the parent received a boot-failure notice up the spine');
116
- });
@@ -1,126 +0,0 @@
1
- // Run with: node --import tsx/esm --test src/core/__tests__/broker-dialogs.test.ts
2
- //
3
- // Broker dialog handling — the §5.4 C2 forward-progress proof plus the T8 attach
4
- // gates G5/G5b/G6. Split out of broker-lifecycle.test.ts (see its header for the
5
- // full file map); the tests are the original acceptance gate, unchanged, on their
6
- // own isolated harness. The attach-client helpers live in
7
- // helpers/broker-clients.ts (the PRODUCTION ViewSocketClient, §0 one-writer: a
8
- // viewer holds ONLY a socket).
9
- import { test, before, after, afterEach } from 'node:test';
10
- import assert from 'node:assert/strict';
11
- import { createHarness, hasTmux } from './helpers/harness.js';
12
- import { isPidAlive } from '../canvas/pid.js';
13
- import { createAttachKit } from './helpers/broker-clients.js';
14
- let h;
15
- let root;
16
- const kit = createAttachKit(() => h);
17
- const { attach, attachUntil } = kit;
18
- before(async () => {
19
- if (!hasTmux())
20
- return;
21
- h = await createHarness({ sessionPrefix: 'crtr-brkdlg' });
22
- root = h.spawnRoot('broker-dialogs suite root');
23
- });
24
- after(async () => {
25
- if (h !== undefined)
26
- await h.dispose();
27
- });
28
- afterEach(() => {
29
- kit.closeAll();
30
- });
31
- const brokerPid = (id) => h.node(id).pi_pid;
32
- // ===========================================================================
33
- // C2 forward-progress (zero-viewer path) — an unattended blocking dialog
34
- // resolves to its default (false) IMMEDIATELY: with no controller connected the
35
- // broker's REAL makeBrokerUiContext falls back to noOp resolution, so the engine
36
- // never deadlocks AND never waits on a per-dialog timeout (the design §5.4
37
- // timeout premise is false). Red/green: the OLD broker armed a setTimeout and
38
- // resolved only after `timeout` ms; the fixed broker resolves in ~0ms. (Supports
39
- // acceptance item 7: the existing suite stays green; this only ADDS coverage.)
40
- // ===========================================================================
41
- test('C2 — unattended dialog resolves to its default IMMEDIATELY (noOp), never waits on a timeout', { skip: !hasTmux() }, async () => {
42
- const id = await h.spawnHeadlessChild(root, 'headless worker — dialog');
43
- const pid = h.node(id).pi_pid;
44
- // Drive the fake engine to call uiContext.confirm(..., { timeout: 5000 }) with
45
- // NO controller. C2 fix: makeBrokerUiContext resolves the default (false) AT
46
- // ONCE — it does NOT arm/await the timeout. A generous < 2000ms bound is still a
47
- // hard fail against the old ~5000ms timeout-wait while staying robust on slow CI.
48
- h.fakeCmd(id, { cmd: 'dialog', timeout: 5000 });
49
- const results = await h.waitFor(() => {
50
- const r = h.dialogResults(id);
51
- return r.length > 0 ? r : null;
52
- }, { timeoutMs: 15_000, label: 'unattended dialog resolved' });
53
- assert.equal(results[0].resolved, false, 'unattended confirm resolves to its default (false / deny)');
54
- assert.ok(results[0].ms < 2000, `C2: resolved IMMEDIATELY (noOp), not after the 5000ms timeout — got ${results[0].ms}ms`);
55
- assert.equal(isPidAlive(pid), true, 'the broker made forward progress (still alive, did not hang or exit)');
56
- });
57
- // ---------------------------------------------------------------------------
58
- // G5 — dialog forward + answer. Guards: a blocking dialog reaches the controller
59
- // as extension_ui_request and the controller's extension_ui_response unblocks the
60
- // engine with ITS answer (not the default). Failure mode: a dialog the controller
61
- // can't see/answer (silent deadlock).
62
- // ---------------------------------------------------------------------------
63
- test('G5 — controller receives an extension_ui_request, answers it, and the engine proceeds with that answer', { skip: !hasTmux() }, async () => {
64
- const id = await h.spawnHeadlessChild(root, 'headless worker — G5a');
65
- const c = await attach(id, 'controller', 'g5-ctrl');
66
- h.fakeCmd(id, { cmd: 'dialog', timeout: 20_000 }); // generous: the controller answers first
67
- const req = await c.waitFrame((f) => f.type === 'extension_ui_request', 'G5 dialog forwarded to controller');
68
- assert.equal(req.method, 'confirm', 'G5: the forwarded dialog is the confirm() the engine raised');
69
- const reqId = req.id;
70
- c.send({ type: 'extension_ui_response', id: reqId, confirmed: true });
71
- const results = await h.waitFor(() => {
72
- const r = h.dialogResults(id);
73
- return r.length > 0 ? r : null;
74
- }, { label: 'G5 dialog resolved by the controller', timeoutMs: 15_000 });
75
- assert.equal(results[0].resolved, true, 'G5: the engine proceeded with the controller answer (true), not the default (false)');
76
- });
77
- // ---------------------------------------------------------------------------
78
- // G5 (mid-dialog attach) — Guards: a dialog raised under a prior controller stays
79
- // pending across that controller's detach (M2) and is delivered to whoever takes
80
- // control next via welcome.pending_dialog. Failure mode: a pending dialog lost on
81
- // controller handoff.
82
- // ---------------------------------------------------------------------------
83
- test('G5 — a controller attaching MID-dialog receives the pending dialog via welcome.pending_dialog', { skip: !hasTmux() }, async () => {
84
- const id = await h.spawnHeadlessChild(root, 'headless worker — G5b');
85
- const a = await attach(id, 'controller', 'g5b-A');
86
- h.fakeCmd(id, { cmd: 'dialog', timeout: 30_000 }); // stays pending long enough for the handoff
87
- const reqA = await a.waitFrame((f) => f.type === 'extension_ui_request', 'G5b dialog forwarded to controller A');
88
- const reqId = reqA.id;
89
- a.send({ type: 'bye' });
90
- a.close(); // M2: detach frees control but does NOT cancel the pending dialog
91
- // Controller B takes control (retry covers the close→controllerId=null beat) and
92
- // its welcome carries the still-pending dialog.
93
- const b = await attachUntil(id, 'controller', 'g5b-B', (x) => x.welcome.role === 'controller' && x.welcome.pending_dialog != null, 'G5b controller B takes control with the pending dialog');
94
- assert.equal(b.welcome.pending_dialog.id, reqId, 'G5b: welcome.pending_dialog is the same dialog raised under A');
95
- assert.equal(b.welcome.pending_dialog.method, 'confirm', 'G5b: the pending dialog is the confirm()');
96
- b.send({ type: 'extension_ui_response', id: reqId, confirmed: true });
97
- const results = await h.waitFor(() => {
98
- const r = h.dialogResults(id);
99
- return r.length > 0 ? r : null;
100
- }, { label: 'G5b dialog resolved by controller B', timeoutMs: 15_000 });
101
- assert.equal(results[0].resolved, true, 'G5b: controller B answered the handed-off dialog and the engine proceeded');
102
- });
103
- // ---------------------------------------------------------------------------
104
- // G6 — anti-deadlock. (a) a zero-viewer dialog resolves to its default AT ONCE
105
- // (noOp). (b) an ATTENDED dialog the controller never answers resolves on a SHORT
106
- // per-dialog broker timeout. Guards: the engine never hangs on a dialog with no
107
- // answerer. Failure mode: a forever-blocked turn.
108
- // ---------------------------------------------------------------------------
109
- test('G6 — zero-viewer dialog resolves immediately; an unanswered attended dialog resolves on the broker timeout', { skip: !hasTmux() }, async () => {
110
- const id = await h.spawnHeadlessChild(root, 'headless worker — G6');
111
- const pid = brokerPid(id);
112
- // (a) zero viewers → immediate noOp default (NOT the 5000ms timeout).
113
- h.fakeCmd(id, { cmd: 'dialog', timeout: 5000 });
114
- const r1 = await h.waitFor(() => (h.dialogResults(id).length >= 1 ? h.dialogResults(id) : null), { label: 'G6a zero-viewer dialog resolved', timeoutMs: 15_000 });
115
- assert.equal(r1[0].resolved, false, 'G6a: zero-viewer dialog resolves to the default (deny)');
116
- assert.ok(r1[0].ms < 2000, `G6a: resolved immediately (noOp), not after the 5000ms timeout — got ${r1[0].ms}ms`);
117
- // (b) controller attached but silent → the broker resolves on the SHORT explicit
118
- // per-dialog timeout (800ms), never the 120s default.
119
- const c = await attach(id, 'controller', 'g6-ctrl');
120
- h.fakeCmd(id, { cmd: 'dialog', timeout: 800 });
121
- await c.waitFrame((f) => f.type === 'extension_ui_request', 'G6b dialog forwarded to controller'); // received, deliberately NOT answered
122
- const r2 = await h.waitFor(() => (h.dialogResults(id).length >= 2 ? h.dialogResults(id) : null), { label: 'G6b attended dialog resolved on timeout', timeoutMs: 15_000 });
123
- assert.equal(r2[1].resolved, false, 'G6b: an unanswered attended dialog resolves to the default (deny)');
124
- assert.ok(r2[1].ms >= 600 && r2[1].ms < 5000, `G6b: resolved on the ~800ms per-dialog timeout, not instantly and not the 120s default — got ${r2[1].ms}ms`);
125
- assert.equal(isPidAlive(pid), true, 'G6: the engine made forward progress on both dialogs (still alive)');
126
- });