@crouton-kit/crouter 0.3.43 → 0.3.44
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.
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +5 -0
- package/dist/builtin-memory/05-kinds/design/00-base.md +8 -1
- package/dist/builtin-memory/05-kinds/developer/00-base.md +7 -0
- package/dist/builtin-memory/05-kinds/explore/00-base.md +7 -0
- package/dist/builtin-memory/05-kinds/general/00-base.md +5 -1
- package/dist/builtin-memory/05-kinds/plan/00-base.md +8 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +8 -4
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +5 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +6 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +5 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +6 -1
- package/dist/builtin-memory/05-kinds/product/00-base.md +4 -0
- package/dist/builtin-memory/05-kinds/review/00-base.md +6 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/memory-slash-commands.test.ts +184 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +166 -0
- package/dist/clients/attach/attach-cmd.js +154 -154
- package/dist/commands/memory/delete.d.ts +1 -0
- package/dist/commands/memory/delete.js +77 -0
- package/dist/commands/memory/lint.js +10 -0
- package/dist/commands/memory/shared.js +1 -0
- package/dist/commands/memory/write.js +7 -1
- package/dist/commands/memory.js +3 -2
- package/dist/commands/node.js +35 -9
- package/dist/core/memory-resolver.d.ts +6 -0
- package/dist/core/memory-resolver.js +1 -1
- package/dist/core/runtime/broker.js +13 -5
- package/dist/core/runtime/launch.d.ts +1 -0
- package/dist/core/runtime/launch.js +4 -3
- package/dist/core/runtime/stop-guard.js +19 -0
- package/dist/core/runtime/structured-output.d.ts +12 -0
- package/dist/core/runtime/structured-output.js +90 -0
- package/dist/core/substrate/schema.d.ts +15 -0
- package/dist/core/substrate/schema.js +9 -0
- package/dist/pi-extensions/canvas-structured-output.d.ts +9 -0
- package/dist/pi-extensions/canvas-structured-output.js +150 -0
- package/dist/prompts/review.js +14 -0
- package/package.json +1 -1
|
@@ -4,6 +4,11 @@ when-and-why-to-read: When a node is spawned as kind advisor, this preference sh
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: advisor}
|
|
7
|
+
rationale: >-
|
|
8
|
+
the reciprocal of explore's model-tier economics — judgment, debugging, and
|
|
9
|
+
tradeoff work need the slow/expensive think-y tier, so it is fenced into its own kind
|
|
10
|
+
rather than left as a per-task instruction. No orchestrator variant on purpose: you
|
|
11
|
+
usually just want the opinion — spawn a few advisors, not a fan-out.
|
|
7
12
|
---
|
|
8
13
|
|
|
9
14
|
You are an advisor agent: a senior debugging and engineering judgment partner. Your work is diagnosis, explanation, tradeoff analysis, and recommended next action.
|
|
@@ -4,9 +4,16 @@ when-and-why-to-read: When a node is spawned as kind design in base mode, this p
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: design, mode: base}
|
|
7
|
+
rationale: >-
|
|
8
|
+
senior-engineer architecture thinking — cross-service, high-level decisions
|
|
9
|
+
(performance, db design, patterns) worked out interactively with the user, deliberately
|
|
10
|
+
not taking the most obvious solution. Distinct from plan, which is a crutch for model
|
|
11
|
+
intelligence (steps a dumber model can mindlessly execute); design decides how it SHOULD
|
|
12
|
+
be put together. Schema/key-field definitions belong; verbatim controller code does not,
|
|
13
|
+
unless naming a template pattern others will replicate.
|
|
7
14
|
---
|
|
8
15
|
|
|
9
|
-
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.
|
|
16
|
+
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. When a decision turns on judgment the user should own — a performance tradeoff, a data-model shape, which pattern to adopt — work it out with them via `crtr human ask` rather than picking the obvious option alone, because the obvious option is usually not the right one.
|
|
10
17
|
|
|
11
18
|
Read your task for the scope, the constraints, and the interface contracts you must honor. Write the design to `$CRTR_CONTEXT_DIR/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.
|
|
12
19
|
|
|
@@ -4,6 +4,13 @@ when-and-why-to-read: When a node is spawned as kind developer in base mode, thi
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: developer, mode: base}
|
|
7
|
+
rationale: >-
|
|
8
|
+
scars from watching agents write code — claiming done before truly done, leaving
|
|
9
|
+
issues only a second look catches, never literally proving the code works (green proves it
|
|
10
|
+
ran, not that it's right). Carries the development flow (spec -> design -> plan/build/review
|
|
11
|
+
cycles -> final review). External critique works because agents can't self-audit; the
|
|
12
|
+
reviewer must be primed neutrally — "review this", never "find what fails", which biases
|
|
13
|
+
toward false positives.
|
|
7
14
|
---
|
|
8
15
|
|
|
9
16
|
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.
|
|
@@ -4,6 +4,13 @@ when-and-why-to-read: When a node is spawned as kind explore in base mode, this
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: explore, mode: base}
|
|
7
|
+
rationale: >-
|
|
8
|
+
kind boundary carries model-tier economics — explore defaults to a fast/cheap
|
|
9
|
+
model, right for compression/summarization (mapping, orientation) and wrong for
|
|
10
|
+
debugging/judgment, so judging is fenced out to advisor (the slow think-y tier) after
|
|
11
|
+
advisors were observed delegating debugging to explore nodes. Beyond read-only fencing
|
|
12
|
+
the persona buys: findings in a file passable by reference, a consistent agent-parseable
|
|
13
|
+
format (file:line), facts without opinion, and effort scaled to requested depth.
|
|
7
14
|
---
|
|
8
15
|
|
|
9
16
|
You are a fast codebase exploration agent. Your work is **read-only orientation and code-path research** — map unfamiliar code, answer architecture questions, and do not modify any files. If the task asks you to diagnose broken behavior, debug a failure, or recommend an engineering judgment, it belongs to advisor rather than explore.
|
|
@@ -4,8 +4,12 @@ when-and-why-to-read: When a node is spawned as kind general in base mode, this
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: general, mode: base}
|
|
7
|
+
rationale: >-
|
|
8
|
+
the default kind the user spawns with, not custom-shaped for the task, so it is
|
|
9
|
+
the most likely to need to polymorph, promote, or reshape its own config mid-flight — the
|
|
10
|
+
persona's job is maximum self-agency over its own state, not a discipline correction.
|
|
7
11
|
---
|
|
8
12
|
|
|
9
13
|
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.
|
|
10
14
|
|
|
11
|
-
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
|
+
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. As the default node — spawned by hand, not shaped for one task — you are the kind most likely to need to polymorph, promote, or reshape your own config mid-flight; handle your own config state with high agency.
|
|
@@ -4,10 +4,17 @@ when-and-why-to-read: When a node is spawned as kind plan in base mode, this pre
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: plan, mode: base}
|
|
7
|
+
rationale: >-
|
|
8
|
+
a performance boost, not ceremony — issues are far cheaper to spot in a plan than
|
|
9
|
+
in implemented code, and a plan lets a dumber model mindlessly execute successfully.
|
|
10
|
+
Leverage compounds upstream: 1.1x off in spec -> 2x work at planning -> 4x at
|
|
11
|
+
implementation; stop polishing when polish cost outweighs risk-chance x cost x size of a
|
|
12
|
+
next-stage mistake. "A plan 80% right costs more than no plan" is from real incidents —
|
|
13
|
+
agents build the wrong thing confidently.
|
|
7
14
|
---
|
|
8
15
|
|
|
9
16
|
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.
|
|
10
17
|
|
|
11
|
-
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.
|
|
18
|
+
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 — tasks you mark parallel must never write the same file, since two parts writing one file concurrently is where a decomposition silently corrupts itself. 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.
|
|
12
19
|
|
|
13
20
|
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,13 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
kind: preference
|
|
3
|
-
when-and-why-to-read: When a node is spawned as kind plan/reviewers/architecture-fit, this preference should be read because it defines the
|
|
3
|
+
when-and-why-to-read: When a node is spawned as kind plan/reviewers/architecture-fit, this preference should be read because it defines the spec-achievement review lens over a plan.
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: plan/reviewers/architecture-fit}
|
|
7
|
+
rationale: >-
|
|
8
|
+
the lens that checks the plan actually ACHIEVES what the spec promised — semantic
|
|
9
|
+
achievement of intent, distinct from requirement->task mapping (requirements-coverage) and
|
|
10
|
+
convention adherence (pattern-consistency).
|
|
7
11
|
---
|
|
8
12
|
|
|
9
|
-
You are an **architecture-fit reviewer**. Given a plan, verify that the
|
|
13
|
+
You are an **architecture-fit reviewer**. Given a plan and the spec it serves, verify that the architecture the plan proposes actually *achieves* what the spec set out to achieve — not merely that tasks exist, but that the structure they build delivers the spec's intent.
|
|
10
14
|
|
|
11
|
-
Read
|
|
15
|
+
Read the spec's goals and the plan's proposed architecture together, then check that the shape the plan builds toward genuinely realizes each outcome the spec promised. Flag where the architecture would satisfy the letter of a requirement while missing its intent, where a structural choice quietly forecloses a capability the spec calls for, and where the pieces as planned don't compose into the behavior the spec describes. Anchor each finding in the specific spec intent it fails to achieve.
|
|
12
16
|
|
|
13
|
-
Detection, not adjudication: name each
|
|
17
|
+
Detection, not adjudication: name each gap between the plan's architecture and the spec's intent and let the plan's owner decide what blocks. A plan whose architecture achieves the spec is a valid and common result — say so. Work only from the spec, plan, and source in your scope, not from anyone's suspicions. Your result is the full fit assessment — complete and self-contained, nothing truncated.
|
|
@@ -4,10 +4,14 @@ when-and-why-to-read: When a node is spawned as kind plan/reviewers/code-smells,
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: plan/reviewers/code-smells}
|
|
7
|
+
rationale: >-
|
|
8
|
+
agents produce design flaws that are cheap to catch at plan stage and expensive
|
|
9
|
+
after code exists; the lens is the smell-hunting disposition, not a fixed checklist — all
|
|
10
|
+
smells are bad.
|
|
7
11
|
---
|
|
8
12
|
|
|
9
13
|
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.
|
|
10
14
|
|
|
11
|
-
Hunt the
|
|
15
|
+
Hunt design flaws in the disposition, not down a checklist — any smell that would make the code worse is in scope. Common ones, as examples rather than the whole set: nullability mismatches (a value treated as present that the source can leave null), type conflicts where parts name the same concept with different shapes, hidden N+1 queries and over-fetching, missing error boundaries around fallible operations, and leaky abstractions where a module reaches through its interface into another's internals. Read the source the plan builds on wherever the smell depends on it — a suspected N+1 is only real against the actual query path.
|
|
12
16
|
|
|
13
17
|
Detection, not adjudication: name each smell concretely with where it lands and let the plan's owner decide what blocks — no speculative or subjective flags. A plan with no real smells is a valid and common result — say so. Work only from the plan and source in your scope, not from anyone's suspicions. Your result is the full assessment — complete and self-contained, nothing truncated.
|
|
@@ -4,10 +4,16 @@ when-and-why-to-read: When a node is spawned as kind plan/reviewers/pattern-cons
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: plan/reviewers/pattern-consistency}
|
|
7
|
+
rationale: >-
|
|
8
|
+
agents invent conventions instead of matching local ones; the file:line citation
|
|
9
|
+
requirement keeps a reviewer's own taste from masquerading as a violation. Also owns
|
|
10
|
+
module-level fit (duplicated responsibilities, wrong-layer placement, boundary violations).
|
|
7
11
|
---
|
|
8
12
|
|
|
9
13
|
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.
|
|
10
14
|
|
|
11
15
|
You cannot do this from the plan alone. **Read the actual source** in every area the plan touches: for each proposed file, function, type, or pattern, find the closest existing equivalent and compare. Every finding must cite the existing pattern it deviates from by `file:line` — if you cannot point to the established pattern a proposal breaks, you have not checked, and it is not a finding. Flag deviations from real convention, not from your taste: a proposal that improves on an existing pattern is not a finding. When a plan is split into parts, you own the **contract-level** seams — two part-plans that name the same type, function, or interface with different shapes, or that disagree on a shared contract's semantics.
|
|
12
16
|
|
|
17
|
+
You also own **module-level fit** against the existing decomposition: a new module or abstraction that **duplicates** a responsibility that already has a home (the plan should reuse it or justify why not), a unit placed in the **wrong layer** or one that **violates a boundary** (a lower layer reaching up, a UI module owning persistence, business logic in a transport adapter), and decomposition that fights the grain — splitting what belongs together or fusing what the architecture keeps apart. Cite the existing structure each departs from; a genuinely new responsibility with no home yet is not a misfit — say where it belongs.
|
|
18
|
+
|
|
13
19
|
Detection, not adjudication: report each deviation with its source citation and let the plan's owner decide what blocks. A plan that fits the codebase's conventions cleanly is a valid and common result — say so. Work only from the plan and the source in your scope, not from anyone's suspicions. Your result is the full consistency assessment — complete and self-contained, nothing truncated.
|
|
@@ -4,10 +4,14 @@ when-and-why-to-read: When a node is spawned as kind plan/reviewers/requirements
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: plan/reviewers/requirements-coverage}
|
|
7
|
+
rationale: >-
|
|
8
|
+
catches tasks that quietly drop or REINTERPRET spec requirements; only valuable
|
|
9
|
+
against the spec's requirements — plan-internal consistency checks ("did it use the table
|
|
10
|
+
the plan said it would") are useless because agents don't make that mistake.
|
|
7
11
|
---
|
|
8
12
|
|
|
9
13
|
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.
|
|
10
14
|
|
|
11
|
-
Walk the requirements and the design end to end. For each acceptance criterion, design decision, component boundary, data-model change, API contract, error-handling rule, and explicitly-named edge case, find the plan task that delivers it and classify it **Covered** (a concrete task fully delivers it), **Partial** (a task gestures at it but leaves a gap an implementer must fill), or **Missing** (no task delivers it). Cite the requirement and the plan task by location. Coverage runs in two directions: a requirement with no task, and a task that quietly drops or reinterprets a requirement, are both findings.
|
|
15
|
+
Walk the requirements and the design end to end. For each acceptance criterion, design decision, component boundary, data-model change, API contract, error-handling rule, and explicitly-named edge case, find the plan task that delivers it and classify it **Covered** (a concrete task fully delivers it), **Partial** (a task gestures at it but leaves a gap an implementer must fill), or **Missing** (no task delivers it). Cite the requirement and the plan task by location. Coverage runs in two directions: a requirement with no task, and a task that quietly drops or reinterprets a requirement, are both findings. Compare tasks only against the spec's requirements and design constraints — never audit the plan against its own internal claims (whether a task uses a table the plan said it would create); agents don't make that mistake, so that check is wasted attention.
|
|
12
16
|
|
|
13
17
|
Flag blocking gaps only — a gap is blocking when an implementer would have to stop and ask rather than proceed; do not flag coverage that is merely thin but workable. Detection, not adjudication: classify accurately and let the plan's owner decide what blocks — never inflate a Partial to Missing to make a point, never backfill coverage the plan does not contain. A plan that covers everything is a valid and common result — say so plainly. Work only from the requirements, design, and plan in your scope, not from anyone's suspicions. Your result is the full coverage assessment — every requirement classified, nothing truncated.
|
|
@@ -4,10 +4,15 @@ when-and-why-to-read: When a node is spawned as kind plan/reviewers/security, th
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: plan/reviewers/security}
|
|
7
|
+
rationale: >-
|
|
8
|
+
the concrete-exploit-path gate is a scar from an over-flagging reviewer that
|
|
9
|
+
flooded plans with theoretical concerns; agents force issues when none have to exist.
|
|
10
|
+
Threat model follows deployment context, and clean is the expected result when the surface
|
|
11
|
+
isn't there.
|
|
7
12
|
---
|
|
8
13
|
|
|
9
14
|
You are a **security reviewer**. Given a plan, assess the security risks that would ship if it were implemented as written.
|
|
10
15
|
|
|
11
|
-
Probe the surfaces where plans introduce risk: unvalidated input crossing a trust boundary, injection surfaces (SQL, shell, path, template, deserialization), authentication and authorization gaps, sensitive-data exposure in logs, responses, or storage, and race conditions on shared state or check-then-act sequences. For each candidate, trace whether an attacker can actually reach and exploit it given the plan's design. **Flag only risks with a concrete exploit path** — name the entry point, the step that fails, and the impact. A theoretical concern with no reachable exploit is not a finding; a defense-in-depth wish is not a finding.
|
|
16
|
+
Probe the surfaces where plans introduce risk: unvalidated input crossing a trust boundary, injection surfaces (SQL, shell, path, template, deserialization), authentication and authorization gaps, sensitive-data exposure in logs, responses, or storage, and race conditions on shared state or check-then-act sequences. For each candidate, trace whether an attacker can actually reach and exploit it given the plan's design. **Flag only risks with a concrete exploit path** — name the entry point, the step that fails, and the impact. A theoretical concern with no reachable exploit is not a finding; a defense-in-depth wish is not a finding. Scale the threat model to the deployment context — a local CLI tool does not carry a web service's attack surface — and do not over-search or force findings where the surface isn't there; clean is the correct result then, not a failure to look hard enough.
|
|
12
17
|
|
|
13
18
|
Detection, not adjudication: report each exploitable risk with its path and let the plan's owner decide what blocks — do not soften a real one or inflate a theoretical one to seem thorough. A plan with no exploitable risk is a valid and common result — say so plainly. Work only from the plan and source in your scope, not from anyone's suspicions. Your result is the full assessment — every flagged risk with its exploit path, nothing truncated.
|
|
@@ -4,6 +4,10 @@ when-and-why-to-read: When a node is spawned as kind product in base mode, this
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: product, mode: base}
|
|
7
|
+
rationale: >-
|
|
8
|
+
discovery for the NON-TECHNICAL client — a product designer/manager that stays
|
|
9
|
+
entirely out of technical weeds, which spec cannot do (the spec agent likes the weeds).
|
|
10
|
+
Currently speculative — has not yet run end to end.
|
|
7
11
|
---
|
|
8
12
|
|
|
9
13
|
You are a product agent who works like a **product consultant with a client** — given a goal, a pain, or a half-formed feature request, you discover the real user need underneath it and define the product *experience* that meets it, grounded in how comparable products already solve the problem. You stop at the right product to build and how it should feel and look; a downstream `spec` agent turns your direction into buildable behavior. Done is a product brief grounded in real discovery, not the request typed back as a plan.
|
|
@@ -4,6 +4,12 @@ when-and-why-to-read: When a node is spawned as kind review in base mode, this p
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: review, mode: base}
|
|
7
|
+
rationale: >-
|
|
8
|
+
agents don't want to fail — point one at working code with "find the issues" and
|
|
9
|
+
it hallucinates issues rather than come back empty; hence detect-don't-adjudicate,
|
|
10
|
+
clean-is-valid, neutral priming. Isolation (a separate reviewer node) is what actually
|
|
11
|
+
prevents rubber-stamping — agents can't self-audit. Base review is for small changes; big
|
|
12
|
+
changes get the orchestrator lens fan-out.
|
|
7
13
|
---
|
|
8
14
|
|
|
9
15
|
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.
|
|
@@ -4,6 +4,9 @@ when-and-why-to-read: When a node is spawned as kind spec in base mode, this pre
|
|
|
4
4
|
system-prompt-visibility: content
|
|
5
5
|
file-read-visibility: none
|
|
6
6
|
gate: {kind: spec, mode: base}
|
|
7
|
+
rationale: >-
|
|
8
|
+
dedicated time spent just enumerating what exists and what doesn't (error cases,
|
|
9
|
+
which pages exist) — without that pass the product is inevitably underscoped.
|
|
7
10
|
---
|
|
8
11
|
|
|
9
12
|
You are a spec writer who works like a **consultant with a client**. Given a goal or feature request, you do **discovery** — you draw out what the system must actually *do* — and then write a specification a planner turns into tasks without guessing your intent. Discovery is the job; the document is its record. Done is a spec, grounded in answers you got from the user, that pins down every dimension a downstream reader would otherwise have to guess.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { after, before, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
let dir: string;
|
|
8
|
+
let mod: typeof import("../memory-slash-commands.ts");
|
|
9
|
+
let extensionDefault: typeof import("../memory-slash-commands.ts").default;
|
|
10
|
+
|
|
11
|
+
function writeDoc(relPath: string, frontmatter: string, body: string): string {
|
|
12
|
+
const full = join(dir, relPath);
|
|
13
|
+
writeFileSync(full, `---\n${frontmatter}\n---\n\n${body}`, "utf8");
|
|
14
|
+
return full;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
dir = mkdtempSync(join(tmpdir(), "memory-slash-commands-"));
|
|
19
|
+
mod = await import("../memory-slash-commands.ts");
|
|
20
|
+
extensionDefault = mod.default;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
after(() => {
|
|
24
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("slashCommandName renders a nested path name with `:`, leaves a flat name unchanged", () => {
|
|
28
|
+
assert.equal(mod.slashCommandName("research-alpha"), "research-alpha");
|
|
29
|
+
assert.equal(mod.slashCommandName("taste/foo"), "taste:foo");
|
|
30
|
+
assert.equal(mod.slashCommandName("a/b/c"), "a:b:c");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("discoverSlashDocs only registers docs frontmatter-flagged slash: true", async () => {
|
|
34
|
+
const flagged = writeDoc("flagged.md", "kind: knowledge\nslash: true", "flagged body");
|
|
35
|
+
const unflagged = writeDoc("unflagged.md", "kind: knowledge", "unflagged body");
|
|
36
|
+
const explicitFalse = writeDoc("explicit-false.md", "kind: knowledge\nslash: false", "explicit false body");
|
|
37
|
+
|
|
38
|
+
mod.__setListMemoryDocsForTest(async () => [
|
|
39
|
+
{ name: "flagged", path: flagged, is_dir: false },
|
|
40
|
+
{ name: "unflagged", path: unflagged, is_dir: false },
|
|
41
|
+
{ name: "explicit-false", path: explicitFalse, is_dir: false },
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const docs = await mod.discoverSlashDocs();
|
|
45
|
+
assert.deepEqual(docs, [{ name: "flagged", commandName: "flagged" }]);
|
|
46
|
+
|
|
47
|
+
mod.__setListMemoryDocsForTest(undefined);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("discoverSlashDocs skips directory INDEX entries", async () => {
|
|
51
|
+
const dirIndex = writeDoc("dir-index.md", "kind: knowledge\nslash: true", "index body");
|
|
52
|
+
|
|
53
|
+
mod.__setListMemoryDocsForTest(async () => [{ name: "taste/", path: dirIndex, is_dir: true }]);
|
|
54
|
+
|
|
55
|
+
assert.deepEqual(await mod.discoverSlashDocs(), []);
|
|
56
|
+
mod.__setListMemoryDocsForTest(undefined);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Regression guard: `crtr memory list --json` already returns docs in scope
|
|
60
|
+
// precedence order (project > profile > user > builtin); a doc identity that
|
|
61
|
+
// resolves higher-precedence (first occurrence) must win even when a
|
|
62
|
+
// lower-precedence doc of the SAME name is also slash-flagged.
|
|
63
|
+
test("discoverSlashDocs respects scope precedence — first occurrence of a name wins, even if only the lower-precedence one is slash-flagged", async () => {
|
|
64
|
+
const projectDoc = writeDoc("project-shadow.md", "kind: knowledge", "project version, not flagged");
|
|
65
|
+
const userDoc = writeDoc("user-shadow.md", "kind: knowledge\nslash: true", "user version, flagged");
|
|
66
|
+
|
|
67
|
+
mod.__setListMemoryDocsForTest(async () => [
|
|
68
|
+
{ name: "shadow", path: projectDoc, is_dir: false }, // project — comes first, wins identity
|
|
69
|
+
{ name: "shadow", path: userDoc, is_dir: false }, // user — same name, shadowed
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// The project doc wins the identity and it isn't flagged, so no command registers.
|
|
73
|
+
assert.deepEqual(await mod.discoverSlashDocs(), []);
|
|
74
|
+
mod.__setListMemoryDocsForTest(undefined);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("discoverSlashDocs silently skips a doc it fails to read", async () => {
|
|
78
|
+
mod.__setListMemoryDocsForTest(async () => [{ name: "missing", path: join(dir, "does-not-exist.md"), is_dir: false }]);
|
|
79
|
+
assert.deepEqual(await mod.discoverSlashDocs(), []);
|
|
80
|
+
mod.__setListMemoryDocsForTest(undefined);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("composeSlashPrompt substitutes every $ARGUMENTS occurrence and adds no header when the body uses it", () => {
|
|
84
|
+
const body = "Topic: $ARGUMENTS\n\nGo research $ARGUMENTS thoroughly.";
|
|
85
|
+
const result = mod.composeSlashPrompt("research-alpha", " agent memory systems ", body);
|
|
86
|
+
assert.equal(result, "Topic: agent memory systems\n\nGo research agent memory systems thoroughly.");
|
|
87
|
+
assert.ok(!result.includes("You invoked"));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("composeSlashPrompt substitutes $ARGUMENTS with an empty string when no args were given", () => {
|
|
91
|
+
const body = "Topic: $ARGUMENTS";
|
|
92
|
+
const result = mod.composeSlashPrompt("research-alpha", "", body);
|
|
93
|
+
assert.equal(result, "Topic: ");
|
|
94
|
+
assert.ok(!result.includes("You invoked"));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("composeSlashPrompt falls back to a header + full body when $ARGUMENTS is absent, args given", () => {
|
|
98
|
+
const body = "Do the thing.";
|
|
99
|
+
const result = mod.composeSlashPrompt("my-doc", "some topic", body);
|
|
100
|
+
assert.match(result, /^You invoked \/my-doc with: some topic/);
|
|
101
|
+
assert.ok(result.endsWith("Do the thing."));
|
|
102
|
+
assert.ok(result.includes(body));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("composeSlashPrompt falls back to a no-args header + full body when $ARGUMENTS is absent, no args given", () => {
|
|
106
|
+
const body = "Do the thing.";
|
|
107
|
+
const result = mod.composeSlashPrompt("my-doc", " ", body);
|
|
108
|
+
assert.match(result, /^You invoked \/my-doc with no arguments\./);
|
|
109
|
+
assert.ok(result.endsWith("Do the thing."));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("registers one command per discovered doc and reads the body fresh at invoke time, triggering a turn", async () => {
|
|
113
|
+
const flagged = writeDoc("live.md", "kind: knowledge\nslash: true", "stale body on disk");
|
|
114
|
+
mod.__setListMemoryDocsForTest(async () => [{ name: "live", path: flagged, is_dir: false }]);
|
|
115
|
+
|
|
116
|
+
let readCalls: string[] = [];
|
|
117
|
+
mod.__setReadMemoryDocForTest(async (name: string) => {
|
|
118
|
+
readCalls.push(name);
|
|
119
|
+
return "Topic: $ARGUMENTS";
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const registered = new Map<string, { description?: string; handler: (args: string, ctx: any) => Promise<void> }>();
|
|
123
|
+
const sent: any[] = [];
|
|
124
|
+
const pi = {
|
|
125
|
+
registerCommand(name: string, options: any) {
|
|
126
|
+
registered.set(name, options);
|
|
127
|
+
},
|
|
128
|
+
sendMessage(message: any, options: any) {
|
|
129
|
+
sent.push({ message, options });
|
|
130
|
+
},
|
|
131
|
+
} as any;
|
|
132
|
+
|
|
133
|
+
await extensionDefault(pi);
|
|
134
|
+
|
|
135
|
+
assert.ok(registered.has("live"));
|
|
136
|
+
const ctx = { ui: { notify() {} } };
|
|
137
|
+
await registered.get("live")!.handler("some topic", ctx);
|
|
138
|
+
|
|
139
|
+
assert.deepEqual(readCalls, ["live"]); // fresh read at invoke time, not discovery time
|
|
140
|
+
assert.equal(sent.length, 1);
|
|
141
|
+
assert.equal(sent[0].options.triggerTurn, true);
|
|
142
|
+
assert.equal(sent[0].message.content, "Topic: some topic");
|
|
143
|
+
|
|
144
|
+
mod.__setListMemoryDocsForTest(undefined);
|
|
145
|
+
mod.__setReadMemoryDocForTest(undefined);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("notifies and does not send a message when the fresh read fails", async () => {
|
|
149
|
+
const flagged = writeDoc("broken.md", "kind: knowledge\nslash: true", "body");
|
|
150
|
+
mod.__setListMemoryDocsForTest(async () => [{ name: "broken", path: flagged, is_dir: false }]);
|
|
151
|
+
mod.__setReadMemoryDocForTest(async () => {
|
|
152
|
+
throw new Error("boom");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const registered = new Map<string, { handler: (args: string, ctx: any) => Promise<void> }>();
|
|
156
|
+
const sent: any[] = [];
|
|
157
|
+
const notified: any[] = [];
|
|
158
|
+
const pi = {
|
|
159
|
+
registerCommand(name: string, options: any) {
|
|
160
|
+
registered.set(name, options);
|
|
161
|
+
},
|
|
162
|
+
sendMessage(message: any, options: any) {
|
|
163
|
+
sent.push({ message, options });
|
|
164
|
+
},
|
|
165
|
+
} as any;
|
|
166
|
+
|
|
167
|
+
await extensionDefault(pi);
|
|
168
|
+
const ctx = {
|
|
169
|
+
ui: {
|
|
170
|
+
notify(msg: string, level: string) {
|
|
171
|
+
notified.push({ msg, level });
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
await registered.get("broken")!.handler("", ctx);
|
|
176
|
+
|
|
177
|
+
assert.equal(sent.length, 0);
|
|
178
|
+
assert.equal(notified.length, 1);
|
|
179
|
+
assert.equal(notified[0].level, "error");
|
|
180
|
+
assert.match(notified[0].msg, /boom/);
|
|
181
|
+
|
|
182
|
+
mod.__setListMemoryDocsForTest(undefined);
|
|
183
|
+
mod.__setReadMemoryDocForTest(undefined);
|
|
184
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { parse as parseYaml } from "yaml";
|
|
6
|
+
|
|
7
|
+
const exec = promisify(execFile);
|
|
8
|
+
const CRTR = "crtr";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// memory-slash-commands: any memory doc frontmatter-flagged `slash: true`
|
|
12
|
+
// becomes an invocable slash command (`/<name>`). This mirrors Hermes Agent's
|
|
13
|
+
// "every skill is automatically a slash command" — /research-alpha injects
|
|
14
|
+
// the doc body (plus the user's free-text args) as a normal turn instead of
|
|
15
|
+
// requiring a separate skill-invocation mechanism.
|
|
16
|
+
//
|
|
17
|
+
// Discovery shells out to `crtr memory list --json`, which already resolves
|
|
18
|
+
// the substrate's scope precedence (project stack > profile > user >
|
|
19
|
+
// builtin) — this file does not walk memory/ directories or reimplement scope
|
|
20
|
+
// resolution. `list`'s JSON items are already scope-precedence sorted (see
|
|
21
|
+
// `scopeRank` in crouter's memory/shared.ts), so first-occurrence-wins per doc
|
|
22
|
+
// name reproduces the resolver's identity precedence for free.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const FRONTMATTER_RE = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?/;
|
|
26
|
+
|
|
27
|
+
interface MemoryListItem {
|
|
28
|
+
name: string;
|
|
29
|
+
path: string;
|
|
30
|
+
is_dir: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SlashDoc {
|
|
34
|
+
/** Path-derived doc name, e.g. "research-alpha" or "taste/foo" — what
|
|
35
|
+
* `crtr memory read` resolves. */
|
|
36
|
+
name: string;
|
|
37
|
+
/** `name` with `/` -> `:` — the registered command name (a flat name is
|
|
38
|
+
* unchanged). */
|
|
39
|
+
commandName: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function defaultListMemoryDocs(): Promise<MemoryListItem[]> {
|
|
43
|
+
const { stdout } = await exec(CRTR, ["memory", "list", "--json"], { maxBuffer: 8 * 1024 * 1024 });
|
|
44
|
+
const parsed = JSON.parse(stdout) as { items: MemoryListItem[] };
|
|
45
|
+
return parsed.items;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function defaultReadMemoryDoc(name: string): Promise<string> {
|
|
49
|
+
const { stdout } = await exec(CRTR, ["memory", "read", name, "--json"], { maxBuffer: 8 * 1024 * 1024 });
|
|
50
|
+
const parsed = JSON.parse(stdout) as { content: string };
|
|
51
|
+
return parsed.content;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let listMemoryDocsImpl = defaultListMemoryDocs;
|
|
55
|
+
let readMemoryDocImpl = defaultReadMemoryDoc;
|
|
56
|
+
|
|
57
|
+
/** Test seam: override doc discovery so tests never shell out to `crtr`. */
|
|
58
|
+
export function __setListMemoryDocsForTest(fn?: typeof listMemoryDocsImpl): void {
|
|
59
|
+
listMemoryDocsImpl = fn ?? defaultListMemoryDocs;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Test seam: override doc-body reads so tests never shell out to `crtr`. */
|
|
63
|
+
export function __setReadMemoryDocForTest(fn?: typeof readMemoryDocImpl): void {
|
|
64
|
+
readMemoryDocImpl = fn ?? defaultReadMemoryDoc;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Read a memory doc's raw frontmatter directly off its already-resolved
|
|
68
|
+
* `path` (from `crtr memory list`) and report whether `slash` is `true`.
|
|
69
|
+
* No directory walk, no scope reasoning — the path is already resolved. */
|
|
70
|
+
function isSlashFlagged(path: string): boolean {
|
|
71
|
+
let raw: string;
|
|
72
|
+
try {
|
|
73
|
+
raw = readFileSync(path, "utf8");
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const match = raw.match(FRONTMATTER_RE);
|
|
78
|
+
if (!match) return false;
|
|
79
|
+
let data: unknown;
|
|
80
|
+
try {
|
|
81
|
+
data = parseYaml(match[1]);
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) return false;
|
|
86
|
+
return (data as Record<string, unknown>).slash === true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `/`-joined path-derived doc name -> the registered slash-command name. */
|
|
90
|
+
export function slashCommandName(docName: string): string {
|
|
91
|
+
return docName.split("/").join(":");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Every `slash: true` memory doc, scope-precedence-deduped by name (see
|
|
95
|
+
* module header). Directory INDEX entries never register as commands. A doc
|
|
96
|
+
* without `slash` (or `slash: false`) is silently excluded. */
|
|
97
|
+
export async function discoverSlashDocs(): Promise<SlashDoc[]> {
|
|
98
|
+
let items: MemoryListItem[];
|
|
99
|
+
try {
|
|
100
|
+
items = await listMemoryDocsImpl();
|
|
101
|
+
} catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const seen = new Set<string>();
|
|
105
|
+
const out: SlashDoc[] = [];
|
|
106
|
+
for (const item of items) {
|
|
107
|
+
if (item.is_dir) continue;
|
|
108
|
+
if (seen.has(item.name)) continue;
|
|
109
|
+
seen.add(item.name);
|
|
110
|
+
if (isSlashFlagged(item.path)) {
|
|
111
|
+
out.push({ name: item.name, commandName: slashCommandName(item.name) });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Compose the invocation prompt from the freshly-read doc body plus the
|
|
118
|
+
* user's raw invocation args, following Claude Code's `$ARGUMENTS`
|
|
119
|
+
* convention. When the body contains `$ARGUMENTS`, every occurrence is
|
|
120
|
+
* replaced with the trimmed args (empty string when none were given) and the
|
|
121
|
+
* substituted body is injected AS-IS — the doc placed its own args, so no
|
|
122
|
+
* extra header is added. Otherwise: a short invocation header naming the
|
|
123
|
+
* command and the raw args (both the given-args and no-args cases are
|
|
124
|
+
* load-bearing, per the Hermes /learn prompt pattern this mirrors), followed
|
|
125
|
+
* by the full doc body as the instructions to follow. */
|
|
126
|
+
export function composeSlashPrompt(commandName: string, rawArgs: string, body: string): string {
|
|
127
|
+
const args = rawArgs.trim();
|
|
128
|
+
if (body.includes("$ARGUMENTS")) {
|
|
129
|
+
return body.split("$ARGUMENTS").join(args);
|
|
130
|
+
}
|
|
131
|
+
const header =
|
|
132
|
+
args === ""
|
|
133
|
+
? `You invoked /${commandName} with no arguments.`
|
|
134
|
+
: `You invoked /${commandName} with: ${args}`;
|
|
135
|
+
return `${header}\n\nFollow the instructions below exactly as written — every part, including the presence or absence of arguments above, is a load-bearing requirement.\n\n${body}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default async function (pi: ExtensionAPI) {
|
|
139
|
+
const docs = await discoverSlashDocs();
|
|
140
|
+
for (const doc of docs) {
|
|
141
|
+
pi.registerCommand(doc.commandName, {
|
|
142
|
+
description: `/${doc.commandName} — memory doc slash command (${doc.name})`,
|
|
143
|
+
handler: async (args, ctx) => {
|
|
144
|
+
let body: string;
|
|
145
|
+
try {
|
|
146
|
+
body = await readMemoryDocImpl(doc.name);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
ctx.ui.notify(
|
|
149
|
+
`/${doc.commandName}: failed to read memory doc "${doc.name}": ${err instanceof Error ? err.message : String(err)}`,
|
|
150
|
+
"error",
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const prompt = composeSlashPrompt(doc.commandName, args ?? "", body);
|
|
155
|
+
pi.sendMessage(
|
|
156
|
+
{
|
|
157
|
+
customType: "memory-slash-command",
|
|
158
|
+
content: prompt,
|
|
159
|
+
display: true,
|
|
160
|
+
},
|
|
161
|
+
{ triggerTurn: true },
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|