@crouton-kit/crouter 0.3.42 → 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.
Files changed (47) hide show
  1. package/bin/crouter +12 -1
  2. package/bin/crtr +12 -1
  3. package/bin/crtrd +12 -1
  4. package/dist/builtin-memory/05-kinds/advisor/00-base.md +5 -0
  5. package/dist/builtin-memory/05-kinds/design/00-base.md +8 -1
  6. package/dist/builtin-memory/05-kinds/developer/00-base.md +7 -0
  7. package/dist/builtin-memory/05-kinds/explore/00-base.md +7 -0
  8. package/dist/builtin-memory/05-kinds/general/00-base.md +5 -1
  9. package/dist/builtin-memory/05-kinds/plan/00-base.md +8 -1
  10. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +8 -4
  11. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +5 -1
  12. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +6 -0
  13. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +5 -1
  14. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +6 -1
  15. package/dist/builtin-memory/05-kinds/product/00-base.md +4 -0
  16. package/dist/builtin-memory/05-kinds/review/00-base.md +6 -0
  17. package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -0
  18. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +34 -0
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/memory-slash-commands.test.ts +184 -0
  20. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +166 -0
  21. package/dist/clients/attach/attach-cmd.js +306 -306
  22. package/dist/commands/memory/delete.d.ts +1 -0
  23. package/dist/commands/memory/delete.js +77 -0
  24. package/dist/commands/memory/lint.js +10 -0
  25. package/dist/commands/memory/shared.js +1 -0
  26. package/dist/commands/memory/write.js +7 -1
  27. package/dist/commands/memory.js +3 -2
  28. package/dist/commands/node.js +35 -9
  29. package/dist/core/__tests__/pid-zombie.test.d.ts +1 -0
  30. package/dist/core/__tests__/pid-zombie.test.js +42 -0
  31. package/dist/core/canvas/pid.d.ts +14 -3
  32. package/dist/core/canvas/pid.js +41 -4
  33. package/dist/core/memory-resolver.d.ts +6 -0
  34. package/dist/core/memory-resolver.js +1 -1
  35. package/dist/core/runtime/broker.js +13 -5
  36. package/dist/core/runtime/launch.d.ts +1 -0
  37. package/dist/core/runtime/launch.js +4 -3
  38. package/dist/core/runtime/spawn.js +18 -3
  39. package/dist/core/runtime/stop-guard.js +19 -0
  40. package/dist/core/runtime/structured-output.d.ts +12 -0
  41. package/dist/core/runtime/structured-output.js +90 -0
  42. package/dist/core/substrate/schema.d.ts +15 -0
  43. package/dist/core/substrate/schema.js +9 -0
  44. package/dist/pi-extensions/canvas-structured-output.d.ts +9 -0
  45. package/dist/pi-extensions/canvas-structured-output.js +150 -0
  46. package/dist/prompts/review.js +14 -0
  47. package/package.json +1 -1
@@ -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
+ }