guardian-framework 0.1.0

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 (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +463 -0
  3. package/dist/cli.js +12958 -0
  4. package/dist/exports.js +12765 -0
  5. package/package.json +61 -0
  6. package/templates/languages/go-patterns.md +411 -0
  7. package/templates/languages/java-patterns.md +545 -0
  8. package/templates/languages/python-patterns.md +312 -0
  9. package/templates/languages/rust-patterns.md +232 -0
  10. package/templates/languages/typescript-patterns.md +313 -0
  11. package/templates/pi/INDEX.md +437 -0
  12. package/templates/pi/agent/AGENTS.md +166 -0
  13. package/templates/pi/agents/architecture-coordinator.md +40 -0
  14. package/templates/pi/agents/architecture-validator.md +36 -0
  15. package/templates/pi/agents/bootstrap-implementer.md +37 -0
  16. package/templates/pi/agents/issue-factory.md +31 -0
  17. package/templates/pi/agents/operations-validator.md +32 -0
  18. package/templates/pi/agents/security-validator.md +33 -0
  19. package/templates/pi/architecture/CHANGELOG.md +78 -0
  20. package/templates/pi/architecture/decisions/ADR-template.md +76 -0
  21. package/templates/pi/architecture/diagrams/system-overview.md +159 -0
  22. package/templates/pi/architecture/modules/module-template.md +152 -0
  23. package/templates/pi/context/checklists.md +91 -0
  24. package/templates/pi/context/domain-workflow.md +180 -0
  25. package/templates/pi/context/output-formats.md +135 -0
  26. package/templates/pi/context/patterns-base.md +47 -0
  27. package/templates/pi/context/patterns.md +38 -0
  28. package/templates/pi/context/project.md +88 -0
  29. package/templates/pi/domain/exploration.md +93 -0
  30. package/templates/pi/domain/ubiquitous-language.md +24 -0
  31. package/templates/pi/extensions/architect-lib/generators.ts +528 -0
  32. package/templates/pi/extensions/architect-lib/helpers.ts +344 -0
  33. package/templates/pi/extensions/architect-lib/types.ts +117 -0
  34. package/templates/pi/extensions/architect.ts +1425 -0
  35. package/templates/pi/extensions/ask-user-question.ts +579 -0
  36. package/templates/pi/extensions/bash-guard.ts +556 -0
  37. package/templates/pi/extensions/config-reload.ts +115 -0
  38. package/templates/pi/extensions/coordinator.ts +304 -0
  39. package/templates/pi/extensions/curator.ts +543 -0
  40. package/templates/pi/extensions/domain-explorer.ts +990 -0
  41. package/templates/pi/extensions/filechanges.ts +656 -0
  42. package/templates/pi/extensions/goal-loop.ts +717 -0
  43. package/templates/pi/extensions/hooks.ts +452 -0
  44. package/templates/pi/extensions/kanban.ts +547 -0
  45. package/templates/pi/extensions/pipeline.ts +1246 -0
  46. package/templates/pi/extensions/plan-mode.ts +209 -0
  47. package/templates/pi/extensions/project-scaffolder.ts +284 -0
  48. package/templates/pi/extensions/read-only-mode.ts +192 -0
  49. package/templates/pi/extensions/redaction.ts +128 -0
  50. package/templates/pi/extensions/session-persistence.ts +174 -0
  51. package/templates/pi/extensions/slash-commands.ts +229 -0
  52. package/templates/pi/extensions/snippets.ts +201 -0
  53. package/templates/pi/extensions/validation-runner.ts +100 -0
  54. package/templates/pi/github/agents/architecture-coordinator.agent.md +27 -0
  55. package/templates/pi/github/agents/architecture-validator.agent.md +20 -0
  56. package/templates/pi/github/agents/bootstrap-implementer.agent.md +20 -0
  57. package/templates/pi/github/agents/epic-planner.agent.md +70 -0
  58. package/templates/pi/github/agents/issue-factory.agent.md +20 -0
  59. package/templates/pi/github/agents/operations-validator.agent.md +20 -0
  60. package/templates/pi/github/agents/security-validator.agent.md +20 -0
  61. package/templates/pi/github/copilot/settings.json +16 -0
  62. package/templates/pi/github/copilot-instructions.md +155 -0
  63. package/templates/pi/github/instructions/architecture.instructions.md +71 -0
  64. package/templates/pi/github/instructions/validation.instructions.md +69 -0
  65. package/templates/pi/github/workflows/01-planning-workflow.md +75 -0
  66. package/templates/pi/github/workflows/02-issue-generation-workflow.md +69 -0
  67. package/templates/pi/github/workflows/03-implementation-workflow.md +80 -0
  68. package/templates/pi/github/workflows/04-validation-workflow.md +68 -0
  69. package/templates/pi/preflight_report.json +99 -0
  70. package/templates/pi/prompts/blueprint-update.md +262 -0
  71. package/templates/pi/prompts/blueprint-validate.md +218 -0
  72. package/templates/pi/prompts/bug-fix.md +50 -0
  73. package/templates/pi/prompts/ci-blueprint.md +131 -0
  74. package/templates/pi/prompts/context-refresh.md +258 -0
  75. package/templates/pi/prompts/epic-plan.md +356 -0
  76. package/templates/pi/prompts/epic-template.md +186 -0
  77. package/templates/pi/prompts/feature-development.md +131 -0
  78. package/templates/pi/prompts/git-issues.md +243 -0
  79. package/templates/pi/prompts/hotfix.md +71 -0
  80. package/templates/pi/prompts/issue-closeout.md +342 -0
  81. package/templates/pi/prompts/issue-draft.md +218 -0
  82. package/templates/pi/prompts/issue-implementation-series.md +173 -0
  83. package/templates/pi/prompts/issue-merge.md +372 -0
  84. package/templates/pi/prompts/issue-template-set.md +392 -0
  85. package/templates/pi/prompts/issue-template.md +207 -0
  86. package/templates/pi/prompts/pattern-extract.md +269 -0
  87. package/templates/pi/prompts/plan-to-issues.md +430 -0
  88. package/templates/pi/prompts/refactoring.md +82 -0
  89. package/templates/pi/prompts/scope-analyzer.md +254 -0
  90. package/templates/pi/prompts/sync-check.md +230 -0
  91. package/templates/pi/scripts/categorize-issues.sh +171 -0
  92. package/templates/pi/scripts/ci/check_architecture_conformance.sh +748 -0
  93. package/templates/pi/scripts/ci/check_planning_packet.py +228 -0
  94. package/templates/pi/scripts/ci/run_hardening_stages.sh +264 -0
  95. package/templates/pi/scripts/ci/run_preflight.sh +649 -0
  96. package/templates/pi/scripts/ci/run_stage.sh +193 -0
  97. package/templates/pi/scripts/ci/stage_docs_policy.sh +82 -0
  98. package/templates/pi/scripts/ci/stage_integration.sh +3 -0
  99. package/templates/pi/scripts/ci/stage_lint.sh +130 -0
  100. package/templates/pi/scripts/ci/stage_migration_verify.sh +3 -0
  101. package/templates/pi/scripts/ci/stage_package_build.sh +3 -0
  102. package/templates/pi/scripts/ci/stage_release_readiness.sh +3 -0
  103. package/templates/pi/scripts/ci/stage_remaining.sh +242 -0
  104. package/templates/pi/scripts/ci/stage_security.sh +3 -0
  105. package/templates/pi/scripts/ci/stage_static_analysis.sh +114 -0
  106. package/templates/pi/scripts/ci/stage_unit.sh +119 -0
  107. package/templates/pi/scripts/ci/validate_agent_output.py +181 -0
  108. package/templates/pi/scripts/ci/validate_agent_output.sh +243 -0
  109. package/templates/pi/scripts/create-feature-branch.sh +136 -0
  110. package/templates/pi/scripts/create-mr.sh +117 -0
  111. package/templates/pi/scripts/fetch-issues.sh +131 -0
  112. package/templates/pi/scripts/generate-architecture.sh +161 -0
  113. package/templates/pi/scripts/git/close-epic.sh +69 -0
  114. package/templates/pi/scripts/git/close-issue.sh +35 -0
  115. package/templates/pi/scripts/git/create-tracking-issue.sh +174 -0
  116. package/templates/pi/scripts/git/link-issue-to-epic.sh +51 -0
  117. package/templates/pi/scripts/git/update-tracking-issue.sh +66 -0
  118. package/templates/pi/scripts/languages/go/validate-architecture.sh +139 -0
  119. package/templates/pi/scripts/languages/go/validate-canonical.sh +129 -0
  120. package/templates/pi/scripts/languages/go/validate-ci.sh +132 -0
  121. package/templates/pi/scripts/languages/go/validate-integration.sh +156 -0
  122. package/templates/pi/scripts/languages/go/validate-operations.sh +146 -0
  123. package/templates/pi/scripts/languages/go/validate-security.sh +135 -0
  124. package/templates/pi/scripts/languages/go/validate-tests.sh +125 -0
  125. package/templates/pi/scripts/languages/java/validate-annotations.sh +185 -0
  126. package/templates/pi/scripts/languages/java/validate-architecture.sh +159 -0
  127. package/templates/pi/scripts/languages/java/validate-canonical.sh +114 -0
  128. package/templates/pi/scripts/languages/java/validate-ci.sh +269 -0
  129. package/templates/pi/scripts/languages/java/validate-integration.sh +149 -0
  130. package/templates/pi/scripts/languages/java/validate-operations.sh +142 -0
  131. package/templates/pi/scripts/languages/java/validate-security.sh +174 -0
  132. package/templates/pi/scripts/languages/java/validate-spring-architecture.sh +227 -0
  133. package/templates/pi/scripts/languages/java/validate-tests.sh +159 -0
  134. package/templates/pi/scripts/languages/python/validate-architecture.sh +99 -0
  135. package/templates/pi/scripts/languages/python/validate-canonical.sh +124 -0
  136. package/templates/pi/scripts/languages/python/validate-ci.sh +130 -0
  137. package/templates/pi/scripts/languages/python/validate-integration.sh +119 -0
  138. package/templates/pi/scripts/languages/python/validate-operations.sh +107 -0
  139. package/templates/pi/scripts/languages/python/validate-security.sh +68 -0
  140. package/templates/pi/scripts/languages/python/validate-tests.sh +102 -0
  141. package/templates/pi/scripts/languages/rust/validate-architecture.sh +188 -0
  142. package/templates/pi/scripts/languages/rust/validate-canonical.sh +146 -0
  143. package/templates/pi/scripts/languages/rust/validate-ci.sh +147 -0
  144. package/templates/pi/scripts/languages/rust/validate-integration.sh +149 -0
  145. package/templates/pi/scripts/languages/rust/validate-operations.sh +154 -0
  146. package/templates/pi/scripts/languages/rust/validate-security.sh +153 -0
  147. package/templates/pi/scripts/languages/rust/validate-tests.sh +150 -0
  148. package/templates/pi/scripts/languages/typescript/validate-architecture.sh +157 -0
  149. package/templates/pi/scripts/languages/typescript/validate-canonical.sh +135 -0
  150. package/templates/pi/scripts/languages/typescript/validate-ci.sh +244 -0
  151. package/templates/pi/scripts/languages/typescript/validate-integration.sh +166 -0
  152. package/templates/pi/scripts/languages/typescript/validate-operations.sh +162 -0
  153. package/templates/pi/scripts/languages/typescript/validate-security.sh +169 -0
  154. package/templates/pi/scripts/languages/typescript/validate-tests.sh +158 -0
  155. package/templates/pi/scripts/merge-mr.sh +95 -0
  156. package/templates/pi/scripts/mr-validation.sh +228 -0
  157. package/templates/pi/scripts/validate-architecture-readiness.sh +153 -0
  158. package/templates/pi/scripts/validate-architecture.sh +22 -0
  159. package/templates/pi/scripts/validate-canonical.sh +22 -0
  160. package/templates/pi/scripts/validate-ci.sh +37 -0
  161. package/templates/pi/scripts/validate-integration.sh +22 -0
  162. package/templates/pi/scripts/validate-operations.sh +22 -0
  163. package/templates/pi/scripts/validate-security.sh +22 -0
  164. package/templates/pi/scripts/validate-tests.sh +22 -0
  165. package/templates/pi/scripts/validate-ubiquitous-language.sh +291 -0
  166. package/templates/pi/scripts/validation-cache.sh +139 -0
  167. package/templates/pi/skills/agents/architecture-coordinator.md +178 -0
  168. package/templates/pi/skills/agents/architecture-generator.md +79 -0
  169. package/templates/pi/skills/agents/architecture-validator.md +74 -0
  170. package/templates/pi/skills/agents/ci-mr-validator.md +46 -0
  171. package/templates/pi/skills/agents/code-developer.md +76 -0
  172. package/templates/pi/skills/agents/commit.md +46 -0
  173. package/templates/pi/skills/agents/curator.md +71 -0
  174. package/templates/pi/skills/agents/debug.md +59 -0
  175. package/templates/pi/skills/agents/documentation-maintainer.md +36 -0
  176. package/templates/pi/skills/agents/goal-loop.md +125 -0
  177. package/templates/pi/skills/agents/hooks.md +122 -0
  178. package/templates/pi/skills/agents/integration-validator.md +35 -0
  179. package/templates/pi/skills/agents/issue-creator.md +75 -0
  180. package/templates/pi/skills/agents/issue-factory.md +143 -0
  181. package/templates/pi/skills/agents/kanban.md +85 -0
  182. package/templates/pi/skills/agents/land.md +59 -0
  183. package/templates/pi/skills/agents/operations-validator.md +43 -0
  184. package/templates/pi/skills/agents/pipeline.md +113 -0
  185. package/templates/pi/skills/agents/plan-mode.md +77 -0
  186. package/templates/pi/skills/agents/pull.md +40 -0
  187. package/templates/pi/skills/agents/push.md +41 -0
  188. package/templates/pi/skills/agents/security-validator.md +85 -0
  189. package/templates/pi/skills/agents/session-persistence.md +119 -0
  190. package/templates/pi/skills/agents/slash-commands.md +117 -0
  191. package/templates/pi/skills/agents/snippets.md +99 -0
  192. package/templates/pi/skills/agents/subagent-registry.md +78 -0
  193. package/templates/pi/skills/agents/test-validator.md +42 -0
  194. package/templates/pi/skills/validators/architecture-validator.md +43 -0
  195. package/templates/pi/skills/validators/ci-validator.md +27 -0
  196. package/templates/pi/skills/validators/context-compaction.md +73 -0
  197. package/templates/pi/skills/validators/integration-validator.md +38 -0
  198. package/templates/pi/skills/validators/model-registry.md +96 -0
  199. package/templates/pi/skills/validators/operations-validator.md +34 -0
  200. package/templates/pi/skills/validators/security-guards.md +72 -0
  201. package/templates/pi/skills/validators/security-validator.md +41 -0
  202. package/templates/pi/skills/validators/system-prompt-tiers.md +69 -0
  203. package/templates/pi/skills/validators/test-validator.md +33 -0
  204. package/templates/pi/types.ts +62 -0
  205. package/templates/pi/validators/README.md +93 -0
  206. package/templates/pi/validators/default.toml +209 -0
  207. package/templates/pi/validators/spring.toml +142 -0
  208. package/templates/pi/workpad.md +53 -0
  209. package/templates/project/java/Dockerfile +14 -0
  210. package/templates/project/java/README.md +40 -0
  211. package/templates/project/java/pom.xml +132 -0
  212. package/templates/project/typescript/Dockerfile +15 -0
  213. package/templates/project/typescript/README.md +29 -0
  214. package/templates/project/typescript/package.json +23 -0
  215. package/templates/project/typescript/tsconfig.json +21 -0
  216. package/templates/workflow.md +123 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Redaction Extension for pi
3
+ *
4
+ * Automatically redacts sensitive data from tool results, terminal output,
5
+ * and agent responses. Prevents accidental leakage of API keys, tokens,
6
+ * and credentials in conversation history and exported files.
7
+ *
8
+ * Patterns detected:
9
+ * - OpenAI keys (sk-proj-...)
10
+ * - Anthropic keys (sk-ant-...)
11
+ * - AWS access keys (AKIA...)
12
+ * - GitHub tokens (ghp_, gho_, ghs_, gh_r_, github_pat_)
13
+ * - Google API keys (AIza...)
14
+ * - Slack tokens (xoxb-...)
15
+ * - Stripe keys (sk_live_, pk_live_, sk_test_)
16
+ * - JWTs (eyJ...)
17
+ * - Bearer tokens
18
+ * - Environment variable assignments (API_KEY=..., SECRET=...)
19
+ */
20
+
21
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
22
+
23
+ // ── Redaction patterns ──
24
+
25
+ const PATTERNS: Array<{ kind: string; re: RegExp }> = [
26
+ { kind: "openai-key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
27
+ { kind: "anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
28
+ { kind: "aws-access-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
29
+ { kind: "github-token", re: /\bgh[opsur]_[A-Za-z0-9]{36,}\b/g },
30
+ { kind: "github-pat", re: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g },
31
+ { kind: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
32
+ { kind: "slack-token", re: /\bxox[bpsare]-[A-Za-z0-9-]{10,}\b/g },
33
+ { kind: "stripe-key", re: /\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g },
34
+ { kind: "jwt", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
35
+ { kind: "bearer", re: /\bBearer\s+[A-Za-z0-9._-]{20,}/g },
36
+ {
37
+ kind: "env-assign",
38
+ re: /\b((?:[A-Z][A-Z0-9_]*)?(?:API[_-]?KEY|SECRET(?:[_-]?KEY)?|ACCESS[_-]?TOKEN|AUTH[_-]?TOKEN|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET)[A-Z0-9_]*)\s*[:=]\s*(["']?)([^\s"';|&]+)\2/gi,
39
+ },
40
+ ];
41
+
42
+ /**
43
+ * Redact sensitive data from text. Returns the redacted string.
44
+ */
45
+ export function redactSensitive(text: string): string {
46
+ let out = text;
47
+ for (const { kind, re } of PATTERNS) {
48
+ if (kind === "env-assign") {
49
+ out = out.replace(
50
+ re,
51
+ (_m: string, name: string, q: string, _val: string) => `${name}=${q}<REDACTED>${q}`,
52
+ );
53
+ } else {
54
+ out = out.replace(re, `<REDACTED:${kind}>`);
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /**
61
+ * Count redactions made (for logging/telemetry-free reporting).
62
+ */
63
+ export function countRedactions(text: string): number {
64
+ let count = 0;
65
+ for (const { re } of PATTERNS) {
66
+ const matches = text.match(re);
67
+ if (matches) count += matches.length;
68
+ }
69
+ return count;
70
+ }
71
+
72
+ // ── Main extension ──
73
+
74
+ export default function (pi: ExtensionAPI) {
75
+ // Redact sensitive data from tool results before showing to user
76
+ pi.on("tool_result", async (event) => {
77
+ if (!event.result) return;
78
+
79
+ // Redact from text results
80
+ if (typeof event.result === "string") {
81
+ event.result = redactSensitive(event.result);
82
+ }
83
+
84
+ // Redact from structured results
85
+ if (typeof event.result === "object" && event.result !== null) {
86
+ const result = event.result as Record<string, unknown>;
87
+ for (const [key, value] of Object.entries(result)) {
88
+ if (typeof value === "string") {
89
+ result[key] = redactSensitive(value);
90
+ }
91
+ }
92
+ }
93
+ });
94
+
95
+ // Redact from user input (in case they paste a key)
96
+ pi.on("input", async (event) => {
97
+ const input = (event as { input?: string }).input;
98
+ if (!input) return;
99
+ const redacted = redactSensitive(input);
100
+ if (redacted !== input) {
101
+ const count = countRedactions(event.input) - countRedactions(redacted);
102
+ if (count > 0) {
103
+ event.input = redacted;
104
+ // Note: pi extensions can't easily notify without ctx.ui,
105
+ // but the redaction is applied silently for safety
106
+ }
107
+ }
108
+ });
109
+
110
+ // Register /redact command for testing
111
+ pi.registerCommand("redact", {
112
+ description: "Test redaction on provided text (for debugging)",
113
+ handler: async (args, ctx) => {
114
+ const text = args.join(" ");
115
+ if (!text) {
116
+ ctx.ui.notify("Usage: /redact <text to test>", "info");
117
+ return;
118
+ }
119
+ const redacted = redactSensitive(text);
120
+ const count = countRedactions(text);
121
+ if (count === 0) {
122
+ ctx.ui.notify("No sensitive patterns detected.", "success");
123
+ } else {
124
+ ctx.ui.notify(`Redacted ${count} sensitive pattern(s):\n\n${redacted}`, "warn");
125
+ }
126
+ },
127
+ });
128
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Session Persistence Extension for pi
3
+ *
4
+ * Provides structured session lifecycle with:
5
+ * - Lazy-loaded message history
6
+ * - Auto-derived session titles
7
+ * - Per-session state isolation (todos, read cache)
8
+ * - Atomic JSON storage with temp-file writes
9
+ *
10
+ * Storage: guardian-sessions.json in workspace root
11
+ */
12
+
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
+
17
+ // ── Types ──
18
+
19
+ interface SessionMeta {
20
+ id: string;
21
+ title: string;
22
+ createdAt: number;
23
+ updatedAt: number;
24
+ }
25
+
26
+ interface StoredSession {
27
+ sessions: SessionMeta[];
28
+ activeId: string | null;
29
+ }
30
+
31
+ const STORE_FILE = "guardian-sessions.json";
32
+ const MAX_SESSIONS = 50;
33
+
34
+ // ── Storage helpers ──
35
+
36
+ function storePath(cwd: string): string {
37
+ return path.join(cwd, STORE_FILE);
38
+ }
39
+
40
+ function loadStore(cwd: string): StoredSession {
41
+ const p = storePath(cwd);
42
+ if (!fs.existsSync(p)) {
43
+ return { sessions: [], activeId: null };
44
+ }
45
+ try {
46
+ return JSON.parse(fs.readFileSync(p, "utf-8")) as StoredSession;
47
+ } catch {
48
+ return { sessions: [], activeId: null };
49
+ }
50
+ }
51
+
52
+ function saveStore(cwd: string, store: StoredSession): void {
53
+ const p = storePath(cwd);
54
+ const tmp = `${p}.tmp`;
55
+ fs.writeFileSync(tmp, JSON.stringify(store, null, 2), "utf-8");
56
+ fs.renameSync(tmp, p);
57
+ }
58
+
59
+ function newSessionId(): string {
60
+ const ts = Date.now().toString(36);
61
+ const rand = Math.random().toString(36).slice(2, 8);
62
+ return `s-${ts}-${rand}`;
63
+ }
64
+
65
+ // Derive title from first user message, stripping context blocks
66
+ function deriveTitle(firstUserMessage: string): string {
67
+ const text = firstUserMessage
68
+ .replace(/<terminal-context[\s\S]*?<\/terminal-context>/g, "")
69
+ .replace(/<selection[\s\S]*?<\/selection>/g, "")
70
+ .replace(/<file[\s\S]*?<\/file>/g, "")
71
+ .replace(/<snippet[\s\S]*?<\/snippet>/g, "")
72
+ .trim();
73
+ if (!text) return "New session";
74
+ const first = text.split("\n")[0].trim();
75
+ return first.length > 40 ? `${first.slice(0, 40)}…` : first;
76
+ }
77
+
78
+ // ── Main extension ──
79
+
80
+ export default function (pi: ExtensionAPI) {
81
+ let currentSession: SessionMeta | null = null;
82
+
83
+ pi.on("session_start", async (_event, ctx) => {
84
+ const store = loadStore(ctx.cwd);
85
+
86
+ // Restore active session or create new one
87
+ if (store.activeId) {
88
+ const existing = store.sessions.find((s) => s.id === store.activeId);
89
+ if (existing) {
90
+ currentSession = existing;
91
+ ctx.ui.setStatus("session", `📂 ${existing.title}`);
92
+ return;
93
+ }
94
+ }
95
+
96
+ // Create new session
97
+ currentSession = {
98
+ id: newSessionId(),
99
+ title: "New session",
100
+ createdAt: Date.now(),
101
+ updatedAt: Date.now(),
102
+ };
103
+
104
+ store.sessions.unshift(currentSession);
105
+
106
+ // Trim old sessions
107
+ if (store.sessions.length > MAX_SESSIONS) {
108
+ store.sessions = store.sessions.slice(0, MAX_SESSIONS);
109
+ }
110
+
111
+ store.activeId = currentSession.id;
112
+ saveStore(ctx.cwd, store);
113
+
114
+ ctx.ui.setStatus("session", `📂 ${currentSession.title}`);
115
+ });
116
+
117
+ // Update session title after first user message
118
+ pi.on("input", async (event, ctx) => {
119
+ if (!currentSession) return;
120
+ if (currentSession.title !== "New session") return;
121
+
122
+ const input = (event as { input?: string }).input;
123
+ if (!input) return;
124
+ const title = deriveTitle(input);
125
+ currentSession.title = title;
126
+ currentSession.updatedAt = Date.now();
127
+
128
+ const store = loadStore(ctx.cwd);
129
+ const idx = store.sessions.findIndex((s) => s.id === currentSession?.id);
130
+ if (idx >= 0) {
131
+ store.sessions[idx] = currentSession;
132
+ saveStore(ctx.cwd, store);
133
+ }
134
+
135
+ ctx.ui.setStatus("session", `📂 ${title}`);
136
+ });
137
+
138
+ // Track session activity
139
+ pi.on("tool_result", async (_event, ctx) => {
140
+ if (!currentSession) return;
141
+ currentSession.updatedAt = Date.now();
142
+
143
+ const store = loadStore(ctx.cwd);
144
+ const idx = store.sessions.findIndex((s) => s.id === currentSession?.id);
145
+ if (idx >= 0) {
146
+ store.sessions[idx] = currentSession;
147
+ saveStore(ctx.cwd, store);
148
+ }
149
+ });
150
+
151
+ // /sessions command — list sessions
152
+ pi.registerCommand("sessions", {
153
+ description: "List all sessions",
154
+ handler: async (_args, ctx) => {
155
+ const store = loadStore(ctx.cwd);
156
+ if (store.sessions.length === 0) {
157
+ ctx.ui.notify("No sessions.", "info");
158
+ return;
159
+ }
160
+ const lines = store.sessions.map((s) => {
161
+ const marker = s.id === currentSession?.id ? "▶ " : " ";
162
+ const age = Date.now() - s.updatedAt;
163
+ const ageStr =
164
+ age < 60_000
165
+ ? "just now"
166
+ : age < 3_600_000
167
+ ? `${Math.floor(age / 60_000)}m ago`
168
+ : `${Math.floor(age / 3_600_000)}h ago`;
169
+ return `${marker}${s.title} (${ageStr})`;
170
+ });
171
+ ctx.ui.notify(`Sessions:\n\n${lines.join("\n")}`, "info");
172
+ },
173
+ });
174
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Slash Commands Extension for pi
3
+ *
4
+ * Intercepts user input starting with `/` and transforms slash commands into
5
+ * structured actions. Supports:
6
+ * /init — Workspace initialization prompt
7
+ * /plan — Plan mode toggle
8
+ * /validate — Run all validators
9
+ * /scope — Classify task scope
10
+ * /snippet — Snippet management (#handle expansion)
11
+ *
12
+ * Also handles `#handle` token expansion for snippets.
13
+ */
14
+
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
18
+
19
+ // ── Snippet types ──
20
+
21
+ interface Snippet {
22
+ id: string;
23
+ handle: string;
24
+ name: string;
25
+ description: string;
26
+ content: string;
27
+ }
28
+
29
+ const SNIPPETS_FILE = "guardian-snippets.json";
30
+ const HANDLE_RE = /^[a-z0-9][a-z0-9-]*$/;
31
+
32
+ function loadSnippets(cwd: string): Snippet[] {
33
+ const snippetsPath = path.join(cwd, SNIPPETS_FILE);
34
+ if (!fs.existsSync(snippetsPath)) return [];
35
+ try {
36
+ return JSON.parse(fs.readFileSync(snippetsPath, "utf-8")) as Snippet[];
37
+ } catch {
38
+ return [];
39
+ }
40
+ }
41
+
42
+ function saveSnippets(cwd: string, snippets: Snippet[]): void {
43
+ const tmpPath = path.join(cwd, `${SNIPPETS_FILE}.tmp`);
44
+ const finalPath = path.join(cwd, SNIPPETS_FILE);
45
+ fs.writeFileSync(tmpPath, JSON.stringify(snippets, null, 2), "utf-8");
46
+ fs.renameSync(tmpPath, finalPath);
47
+ }
48
+
49
+ function newSnippetId(): string {
50
+ return `sn-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
51
+ }
52
+
53
+ function normalizeHandle(raw: string): string {
54
+ return raw
55
+ .trim()
56
+ .toLowerCase()
57
+ .replace(/\s+/g, "-")
58
+ .replace(/[^a-z0-9-]/g, "")
59
+ .replace(/-+/g, "-")
60
+ .replace(/^-+|-+$/g, "");
61
+ }
62
+
63
+ // Expand #handle tokens in text
64
+ function expandSnippetTokens(
65
+ text: string,
66
+ snippets: Snippet[],
67
+ ): { body: string; blocks: string[] } {
68
+ const byHandle = new Map(snippets.map((s) => [s.handle, s]));
69
+ const matched = new Map<string, Snippet>();
70
+
71
+ const re = /(^|\s)#([a-z0-9][a-z0-9-]*)\b/gi;
72
+ const body = text.replace(re, (full, lead: string, raw: string) => {
73
+ const h = raw.toLowerCase();
74
+ const snip = byHandle.get(h);
75
+ if (!snip) return full;
76
+ matched.set(snip.id, snip);
77
+ return lead;
78
+ });
79
+
80
+ const blocks = Array.from(matched.values()).map(
81
+ (s) => `<snippet name="${s.handle}">\n${s.content}\n</snippet>`,
82
+ );
83
+ return {
84
+ body: body.replace(/[ \t]+\n/g, "\n").trim(),
85
+ blocks,
86
+ };
87
+ }
88
+
89
+ // ── Command definitions ──
90
+
91
+ const INIT_PROMPT = `Scan this workspace and produce a project memory file (.pi/PROJECT.md) with:
92
+ - One-paragraph project description
93
+ - Build / test / dev commands
94
+ - Architecture overview (subsystems, data flow, key dirs)
95
+ - Conventions worth knowing (naming, patterns, gotchas)
96
+ - Paths to entry points
97
+ Cap under 200 lines.`;
98
+
99
+ const VALIDATE_PROMPT = `Run all Guardian validators. For each validator:
100
+ 1. Run the script: .pi/scripts/validate-<name>.sh
101
+ 2. Report pass/fail with output
102
+ 3. If any fail, suggest fixes
103
+ Validators to run: [check manifest for configured validators]`;
104
+
105
+ const SCOPE_PROMPT = `Classify the following task scope:
106
+ - Estimate files affected and lines changed
107
+ - Determine required validators (ci, architecture, security, etc.)
108
+ - Assess risk level (simple, moderate, complex, critical)
109
+
110
+ Task: {task_description}`;
111
+
112
+ // ── Main extension ──
113
+
114
+ export default function (pi: ExtensionAPI) {
115
+ pi.on("session_start", async () => {
116
+ // Load snippets at session start
117
+ const snippets = loadSnippets(process.cwd());
118
+ if (snippets.length > 0) {
119
+ pi.appendEntry({
120
+ key: "guardian:snippets",
121
+ value: snippets.map((s) => `#${s.handle} — ${s.description}`),
122
+ });
123
+ }
124
+ });
125
+
126
+ // Intercept user input for slash commands and snippet expansion
127
+ pi.on("input", async (event, ctx) => {
128
+ const input = (event as { input?: string }).input;
129
+ if (!input) return;
130
+ const trimmed = input.trim();
131
+
132
+ // Handle #handle snippet expansion
133
+ if (trimmed.startsWith("#")) {
134
+ const snippets = loadSnippets(ctx.cwd);
135
+ const { body, blocks } = expandSnippetTokens(input, snippets);
136
+ if (blocks.length > 0) {
137
+ event.input = [...blocks, body].join("\n\n");
138
+ return;
139
+ }
140
+ }
141
+
142
+ // Handle /command
143
+ if (!trimmed.startsWith("/")) return;
144
+ const [cmd, ...args] = trimmed.slice(1).split(/\s+/);
145
+ const argStr = args.join(" ");
146
+
147
+ switch (cmd) {
148
+ case "init":
149
+ event.input = INIT_PROMPT;
150
+ ctx.ui.setStatus("command", "/init — workspace scan");
151
+ break;
152
+
153
+ case "validate":
154
+ event.input = VALIDATE_PROMPT;
155
+ ctx.ui.setStatus("command", "/validate — running validators");
156
+ break;
157
+
158
+ case "scope":
159
+ event.input = SCOPE_PROMPT.replace("{task_description}", argStr || "current task");
160
+ ctx.ui.setStatus("command", "/scope — classifying task");
161
+ break;
162
+
163
+ case "plan":
164
+ // Plan mode is handled by plan-mode.ts extension
165
+ ctx.ui.notify("Plan mode: use the plan-mode extension for /plan commands", "info");
166
+ break;
167
+
168
+ case "snippet": {
169
+ const snippets = loadSnippets(ctx.cwd);
170
+ if (args[0] === "list") {
171
+ if (snippets.length === 0) {
172
+ ctx.ui.notify(
173
+ "No snippets configured. Use /snippet add <handle> to create one.",
174
+ "info",
175
+ );
176
+ } else {
177
+ const list = snippets
178
+ .map((s) => `#${s.handle} — ${s.name}: ${s.description}`)
179
+ .join("\n");
180
+ ctx.ui.notify(`Snippets:\n\n${list}`, "info");
181
+ }
182
+ } else if (args[0] === "add" && args.length >= 2) {
183
+ const handle = normalizeHandle(args[1]);
184
+ if (!HANDLE_RE.test(handle)) {
185
+ ctx.ui.notify(
186
+ `Invalid handle: "${args[1]}". Use lowercase letters, numbers, and hyphens.`,
187
+ "error",
188
+ );
189
+ break;
190
+ }
191
+ const name = args.slice(2).join(" ") || handle;
192
+ const content = await ctx.ui.input("Snippet content", "Enter the snippet body...");
193
+ if (!content) break;
194
+ snippets.push({
195
+ id: newSnippetId(),
196
+ handle,
197
+ name,
198
+ description: name,
199
+ content,
200
+ });
201
+ saveSnippets(ctx.cwd, snippets);
202
+ ctx.ui.notify(`Snippet #${handle} created.`, "success");
203
+ } else if (args[0] === "remove" && args.length >= 2) {
204
+ const handle = normalizeHandle(args[1]);
205
+ const before = snippets.length;
206
+ const filtered = snippets.filter((s) => s.handle !== handle);
207
+ if (filtered.length === before) {
208
+ ctx.ui.notify(`Snippet #${handle} not found.`, "error");
209
+ } else {
210
+ saveSnippets(ctx.cwd, filtered);
211
+ ctx.ui.notify(`Snippet #${handle} removed.`, "success");
212
+ }
213
+ } else {
214
+ ctx.ui.notify(
215
+ "Usage: /snippet list | /snippet add <handle> [name] | /snippet remove <handle>",
216
+ "info",
217
+ );
218
+ }
219
+ // Don't send to agent — command handled
220
+ event.input = "(snippet command handled — no agent response needed)";
221
+ break;
222
+ }
223
+
224
+ default:
225
+ // Unknown command — let it through as normal text
226
+ break;
227
+ }
228
+ });
229
+ }