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.
- package/LICENSE +21 -0
- package/README.md +463 -0
- package/dist/cli.js +12958 -0
- package/dist/exports.js +12765 -0
- package/package.json +61 -0
- package/templates/languages/go-patterns.md +411 -0
- package/templates/languages/java-patterns.md +545 -0
- package/templates/languages/python-patterns.md +312 -0
- package/templates/languages/rust-patterns.md +232 -0
- package/templates/languages/typescript-patterns.md +313 -0
- package/templates/pi/INDEX.md +437 -0
- package/templates/pi/agent/AGENTS.md +166 -0
- package/templates/pi/agents/architecture-coordinator.md +40 -0
- package/templates/pi/agents/architecture-validator.md +36 -0
- package/templates/pi/agents/bootstrap-implementer.md +37 -0
- package/templates/pi/agents/issue-factory.md +31 -0
- package/templates/pi/agents/operations-validator.md +32 -0
- package/templates/pi/agents/security-validator.md +33 -0
- package/templates/pi/architecture/CHANGELOG.md +78 -0
- package/templates/pi/architecture/decisions/ADR-template.md +76 -0
- package/templates/pi/architecture/diagrams/system-overview.md +159 -0
- package/templates/pi/architecture/modules/module-template.md +152 -0
- package/templates/pi/context/checklists.md +91 -0
- package/templates/pi/context/domain-workflow.md +180 -0
- package/templates/pi/context/output-formats.md +135 -0
- package/templates/pi/context/patterns-base.md +47 -0
- package/templates/pi/context/patterns.md +38 -0
- package/templates/pi/context/project.md +88 -0
- package/templates/pi/domain/exploration.md +93 -0
- package/templates/pi/domain/ubiquitous-language.md +24 -0
- package/templates/pi/extensions/architect-lib/generators.ts +528 -0
- package/templates/pi/extensions/architect-lib/helpers.ts +344 -0
- package/templates/pi/extensions/architect-lib/types.ts +117 -0
- package/templates/pi/extensions/architect.ts +1425 -0
- package/templates/pi/extensions/ask-user-question.ts +579 -0
- package/templates/pi/extensions/bash-guard.ts +556 -0
- package/templates/pi/extensions/config-reload.ts +115 -0
- package/templates/pi/extensions/coordinator.ts +304 -0
- package/templates/pi/extensions/curator.ts +543 -0
- package/templates/pi/extensions/domain-explorer.ts +990 -0
- package/templates/pi/extensions/filechanges.ts +656 -0
- package/templates/pi/extensions/goal-loop.ts +717 -0
- package/templates/pi/extensions/hooks.ts +452 -0
- package/templates/pi/extensions/kanban.ts +547 -0
- package/templates/pi/extensions/pipeline.ts +1246 -0
- package/templates/pi/extensions/plan-mode.ts +209 -0
- package/templates/pi/extensions/project-scaffolder.ts +284 -0
- package/templates/pi/extensions/read-only-mode.ts +192 -0
- package/templates/pi/extensions/redaction.ts +128 -0
- package/templates/pi/extensions/session-persistence.ts +174 -0
- package/templates/pi/extensions/slash-commands.ts +229 -0
- package/templates/pi/extensions/snippets.ts +201 -0
- package/templates/pi/extensions/validation-runner.ts +100 -0
- package/templates/pi/github/agents/architecture-coordinator.agent.md +27 -0
- package/templates/pi/github/agents/architecture-validator.agent.md +20 -0
- package/templates/pi/github/agents/bootstrap-implementer.agent.md +20 -0
- package/templates/pi/github/agents/epic-planner.agent.md +70 -0
- package/templates/pi/github/agents/issue-factory.agent.md +20 -0
- package/templates/pi/github/agents/operations-validator.agent.md +20 -0
- package/templates/pi/github/agents/security-validator.agent.md +20 -0
- package/templates/pi/github/copilot/settings.json +16 -0
- package/templates/pi/github/copilot-instructions.md +155 -0
- package/templates/pi/github/instructions/architecture.instructions.md +71 -0
- package/templates/pi/github/instructions/validation.instructions.md +69 -0
- package/templates/pi/github/workflows/01-planning-workflow.md +75 -0
- package/templates/pi/github/workflows/02-issue-generation-workflow.md +69 -0
- package/templates/pi/github/workflows/03-implementation-workflow.md +80 -0
- package/templates/pi/github/workflows/04-validation-workflow.md +68 -0
- package/templates/pi/preflight_report.json +99 -0
- package/templates/pi/prompts/blueprint-update.md +262 -0
- package/templates/pi/prompts/blueprint-validate.md +218 -0
- package/templates/pi/prompts/bug-fix.md +50 -0
- package/templates/pi/prompts/ci-blueprint.md +131 -0
- package/templates/pi/prompts/context-refresh.md +258 -0
- package/templates/pi/prompts/epic-plan.md +356 -0
- package/templates/pi/prompts/epic-template.md +186 -0
- package/templates/pi/prompts/feature-development.md +131 -0
- package/templates/pi/prompts/git-issues.md +243 -0
- package/templates/pi/prompts/hotfix.md +71 -0
- package/templates/pi/prompts/issue-closeout.md +342 -0
- package/templates/pi/prompts/issue-draft.md +218 -0
- package/templates/pi/prompts/issue-implementation-series.md +173 -0
- package/templates/pi/prompts/issue-merge.md +372 -0
- package/templates/pi/prompts/issue-template-set.md +392 -0
- package/templates/pi/prompts/issue-template.md +207 -0
- package/templates/pi/prompts/pattern-extract.md +269 -0
- package/templates/pi/prompts/plan-to-issues.md +430 -0
- package/templates/pi/prompts/refactoring.md +82 -0
- package/templates/pi/prompts/scope-analyzer.md +254 -0
- package/templates/pi/prompts/sync-check.md +230 -0
- package/templates/pi/scripts/categorize-issues.sh +171 -0
- package/templates/pi/scripts/ci/check_architecture_conformance.sh +748 -0
- package/templates/pi/scripts/ci/check_planning_packet.py +228 -0
- package/templates/pi/scripts/ci/run_hardening_stages.sh +264 -0
- package/templates/pi/scripts/ci/run_preflight.sh +649 -0
- package/templates/pi/scripts/ci/run_stage.sh +193 -0
- package/templates/pi/scripts/ci/stage_docs_policy.sh +82 -0
- package/templates/pi/scripts/ci/stage_integration.sh +3 -0
- package/templates/pi/scripts/ci/stage_lint.sh +130 -0
- package/templates/pi/scripts/ci/stage_migration_verify.sh +3 -0
- package/templates/pi/scripts/ci/stage_package_build.sh +3 -0
- package/templates/pi/scripts/ci/stage_release_readiness.sh +3 -0
- package/templates/pi/scripts/ci/stage_remaining.sh +242 -0
- package/templates/pi/scripts/ci/stage_security.sh +3 -0
- package/templates/pi/scripts/ci/stage_static_analysis.sh +114 -0
- package/templates/pi/scripts/ci/stage_unit.sh +119 -0
- package/templates/pi/scripts/ci/validate_agent_output.py +181 -0
- package/templates/pi/scripts/ci/validate_agent_output.sh +243 -0
- package/templates/pi/scripts/create-feature-branch.sh +136 -0
- package/templates/pi/scripts/create-mr.sh +117 -0
- package/templates/pi/scripts/fetch-issues.sh +131 -0
- package/templates/pi/scripts/generate-architecture.sh +161 -0
- package/templates/pi/scripts/git/close-epic.sh +69 -0
- package/templates/pi/scripts/git/close-issue.sh +35 -0
- package/templates/pi/scripts/git/create-tracking-issue.sh +174 -0
- package/templates/pi/scripts/git/link-issue-to-epic.sh +51 -0
- package/templates/pi/scripts/git/update-tracking-issue.sh +66 -0
- package/templates/pi/scripts/languages/go/validate-architecture.sh +139 -0
- package/templates/pi/scripts/languages/go/validate-canonical.sh +129 -0
- package/templates/pi/scripts/languages/go/validate-ci.sh +132 -0
- package/templates/pi/scripts/languages/go/validate-integration.sh +156 -0
- package/templates/pi/scripts/languages/go/validate-operations.sh +146 -0
- package/templates/pi/scripts/languages/go/validate-security.sh +135 -0
- package/templates/pi/scripts/languages/go/validate-tests.sh +125 -0
- package/templates/pi/scripts/languages/java/validate-annotations.sh +185 -0
- package/templates/pi/scripts/languages/java/validate-architecture.sh +159 -0
- package/templates/pi/scripts/languages/java/validate-canonical.sh +114 -0
- package/templates/pi/scripts/languages/java/validate-ci.sh +269 -0
- package/templates/pi/scripts/languages/java/validate-integration.sh +149 -0
- package/templates/pi/scripts/languages/java/validate-operations.sh +142 -0
- package/templates/pi/scripts/languages/java/validate-security.sh +174 -0
- package/templates/pi/scripts/languages/java/validate-spring-architecture.sh +227 -0
- package/templates/pi/scripts/languages/java/validate-tests.sh +159 -0
- package/templates/pi/scripts/languages/python/validate-architecture.sh +99 -0
- package/templates/pi/scripts/languages/python/validate-canonical.sh +124 -0
- package/templates/pi/scripts/languages/python/validate-ci.sh +130 -0
- package/templates/pi/scripts/languages/python/validate-integration.sh +119 -0
- package/templates/pi/scripts/languages/python/validate-operations.sh +107 -0
- package/templates/pi/scripts/languages/python/validate-security.sh +68 -0
- package/templates/pi/scripts/languages/python/validate-tests.sh +102 -0
- package/templates/pi/scripts/languages/rust/validate-architecture.sh +188 -0
- package/templates/pi/scripts/languages/rust/validate-canonical.sh +146 -0
- package/templates/pi/scripts/languages/rust/validate-ci.sh +147 -0
- package/templates/pi/scripts/languages/rust/validate-integration.sh +149 -0
- package/templates/pi/scripts/languages/rust/validate-operations.sh +154 -0
- package/templates/pi/scripts/languages/rust/validate-security.sh +153 -0
- package/templates/pi/scripts/languages/rust/validate-tests.sh +150 -0
- package/templates/pi/scripts/languages/typescript/validate-architecture.sh +157 -0
- package/templates/pi/scripts/languages/typescript/validate-canonical.sh +135 -0
- package/templates/pi/scripts/languages/typescript/validate-ci.sh +244 -0
- package/templates/pi/scripts/languages/typescript/validate-integration.sh +166 -0
- package/templates/pi/scripts/languages/typescript/validate-operations.sh +162 -0
- package/templates/pi/scripts/languages/typescript/validate-security.sh +169 -0
- package/templates/pi/scripts/languages/typescript/validate-tests.sh +158 -0
- package/templates/pi/scripts/merge-mr.sh +95 -0
- package/templates/pi/scripts/mr-validation.sh +228 -0
- package/templates/pi/scripts/validate-architecture-readiness.sh +153 -0
- package/templates/pi/scripts/validate-architecture.sh +22 -0
- package/templates/pi/scripts/validate-canonical.sh +22 -0
- package/templates/pi/scripts/validate-ci.sh +37 -0
- package/templates/pi/scripts/validate-integration.sh +22 -0
- package/templates/pi/scripts/validate-operations.sh +22 -0
- package/templates/pi/scripts/validate-security.sh +22 -0
- package/templates/pi/scripts/validate-tests.sh +22 -0
- package/templates/pi/scripts/validate-ubiquitous-language.sh +291 -0
- package/templates/pi/scripts/validation-cache.sh +139 -0
- package/templates/pi/skills/agents/architecture-coordinator.md +178 -0
- package/templates/pi/skills/agents/architecture-generator.md +79 -0
- package/templates/pi/skills/agents/architecture-validator.md +74 -0
- package/templates/pi/skills/agents/ci-mr-validator.md +46 -0
- package/templates/pi/skills/agents/code-developer.md +76 -0
- package/templates/pi/skills/agents/commit.md +46 -0
- package/templates/pi/skills/agents/curator.md +71 -0
- package/templates/pi/skills/agents/debug.md +59 -0
- package/templates/pi/skills/agents/documentation-maintainer.md +36 -0
- package/templates/pi/skills/agents/goal-loop.md +125 -0
- package/templates/pi/skills/agents/hooks.md +122 -0
- package/templates/pi/skills/agents/integration-validator.md +35 -0
- package/templates/pi/skills/agents/issue-creator.md +75 -0
- package/templates/pi/skills/agents/issue-factory.md +143 -0
- package/templates/pi/skills/agents/kanban.md +85 -0
- package/templates/pi/skills/agents/land.md +59 -0
- package/templates/pi/skills/agents/operations-validator.md +43 -0
- package/templates/pi/skills/agents/pipeline.md +113 -0
- package/templates/pi/skills/agents/plan-mode.md +77 -0
- package/templates/pi/skills/agents/pull.md +40 -0
- package/templates/pi/skills/agents/push.md +41 -0
- package/templates/pi/skills/agents/security-validator.md +85 -0
- package/templates/pi/skills/agents/session-persistence.md +119 -0
- package/templates/pi/skills/agents/slash-commands.md +117 -0
- package/templates/pi/skills/agents/snippets.md +99 -0
- package/templates/pi/skills/agents/subagent-registry.md +78 -0
- package/templates/pi/skills/agents/test-validator.md +42 -0
- package/templates/pi/skills/validators/architecture-validator.md +43 -0
- package/templates/pi/skills/validators/ci-validator.md +27 -0
- package/templates/pi/skills/validators/context-compaction.md +73 -0
- package/templates/pi/skills/validators/integration-validator.md +38 -0
- package/templates/pi/skills/validators/model-registry.md +96 -0
- package/templates/pi/skills/validators/operations-validator.md +34 -0
- package/templates/pi/skills/validators/security-guards.md +72 -0
- package/templates/pi/skills/validators/security-validator.md +41 -0
- package/templates/pi/skills/validators/system-prompt-tiers.md +69 -0
- package/templates/pi/skills/validators/test-validator.md +33 -0
- package/templates/pi/types.ts +62 -0
- package/templates/pi/validators/README.md +93 -0
- package/templates/pi/validators/default.toml +209 -0
- package/templates/pi/validators/spring.toml +142 -0
- package/templates/pi/workpad.md +53 -0
- package/templates/project/java/Dockerfile +14 -0
- package/templates/project/java/README.md +40 -0
- package/templates/project/java/pom.xml +132 -0
- package/templates/project/typescript/Dockerfile +15 -0
- package/templates/project/typescript/README.md +29 -0
- package/templates/project/typescript/package.json +23 -0
- package/templates/project/typescript/tsconfig.json +21 -0
- package/templates/workflow.md +123 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Mode Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Intercepts mutating tool calls (edit, write_file, create_directory) and queues
|
|
5
|
+
* them for batch review instead of executing immediately. Shell commands are
|
|
6
|
+
* refused in plan mode. The user reviews all queued changes as a single diff,
|
|
7
|
+
* then accepts/rejects in batch.
|
|
8
|
+
*
|
|
9
|
+
* Usage: /plan (toggles on/off), /plan on, /plan off
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as crypto from "node:crypto";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
16
|
+
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
// ── State ──
|
|
19
|
+
|
|
20
|
+
interface QueuedEdit {
|
|
21
|
+
id: string;
|
|
22
|
+
kind: "edit" | "multi_edit" | "write_file" | "create_directory";
|
|
23
|
+
filePath: string;
|
|
24
|
+
originalContent: string;
|
|
25
|
+
proposedContent: string;
|
|
26
|
+
isNewFile: boolean;
|
|
27
|
+
description?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let planModeActive = false;
|
|
31
|
+
const editQueue: QueuedEdit[] = [];
|
|
32
|
+
|
|
33
|
+
function newEditId(): string {
|
|
34
|
+
const ts = Date.now().toString(36);
|
|
35
|
+
const rand = Math.random().toString(36).slice(2, 7);
|
|
36
|
+
return `qe-${ts}-${rand}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Plan mode toggle via slash command interception ──
|
|
40
|
+
|
|
41
|
+
function handleSlashCommand(input: string): string | null {
|
|
42
|
+
const trimmed = input.trim();
|
|
43
|
+
if (!trimmed.startsWith("/plan") && trimmed !== "/plan") return null;
|
|
44
|
+
|
|
45
|
+
const args = trimmed.slice(5).trim().toLowerCase();
|
|
46
|
+
if (args === "off" || args === "exit") {
|
|
47
|
+
planModeActive = false;
|
|
48
|
+
editQueue.length = 0;
|
|
49
|
+
return "✅ Plan mode off. Queue cleared.";
|
|
50
|
+
}
|
|
51
|
+
planModeActive = true;
|
|
52
|
+
return "✅ Plan mode on. Mutations will be queued for review. Use `/plan off` to exit.";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Helper: read file content safely ──
|
|
56
|
+
|
|
57
|
+
function readFileContent(filePath: string): string {
|
|
58
|
+
if (!fs.existsSync(filePath)) return "";
|
|
59
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Helper: hash for tracking ──
|
|
63
|
+
|
|
64
|
+
function hashContent(content: string): string {
|
|
65
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Main extension ──
|
|
69
|
+
|
|
70
|
+
export default function (pi: ExtensionAPI) {
|
|
71
|
+
pi.on("session_start", async () => {
|
|
72
|
+
planModeActive = false;
|
|
73
|
+
editQueue.length = 0;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Intercept user input for slash commands
|
|
77
|
+
pi.on("input", async (event) => {
|
|
78
|
+
const input = (event as { input?: string }).input;
|
|
79
|
+
if (!input) return;
|
|
80
|
+
const result = handleSlashCommand(input);
|
|
81
|
+
if (result) {
|
|
82
|
+
// Inject the status message as a system note
|
|
83
|
+
event.input = result;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Intercept tool calls when plan mode is active
|
|
88
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
89
|
+
if (!planModeActive) return;
|
|
90
|
+
if (
|
|
91
|
+
!isToolCallEventType("bash", event) &&
|
|
92
|
+
!isToolCallEventType("write", event) &&
|
|
93
|
+
!isToolCallEventType("edit", event)
|
|
94
|
+
)
|
|
95
|
+
return;
|
|
96
|
+
|
|
97
|
+
const toolName = event.toolName;
|
|
98
|
+
const blockedTools = ["bash", "bash_background", "run_command"];
|
|
99
|
+
if (blockedTools.includes(toolName)) {
|
|
100
|
+
return {
|
|
101
|
+
block: true,
|
|
102
|
+
reason:
|
|
103
|
+
"Shell commands are not permitted in plan mode. Use /plan off to exit plan mode, or restrict yourself to read-only tools (read_file, grep, list_directory).",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Intercept write and edit tools
|
|
108
|
+
if (toolName === "write" || toolName === "edit") {
|
|
109
|
+
const inputPath = event.input.file_path ?? event.input.path;
|
|
110
|
+
if (!inputPath) return;
|
|
111
|
+
|
|
112
|
+
const absPath = path.isAbsolute(inputPath) ? inputPath : path.join(ctx.cwd, inputPath);
|
|
113
|
+
const originalContent = readFileContent(absPath);
|
|
114
|
+
const isNew = !fs.existsSync(absPath);
|
|
115
|
+
|
|
116
|
+
// For edit tool, capture proposed content
|
|
117
|
+
let proposedContent = "";
|
|
118
|
+
if (toolName === "write") {
|
|
119
|
+
proposedContent = event.input.content ?? "";
|
|
120
|
+
} else if (toolName === "edit") {
|
|
121
|
+
// For edit, we'd need to apply the edit to get proposed content
|
|
122
|
+
// Store the edit params; actual content computed at review time
|
|
123
|
+
proposedContent = `[edit queued: ${event.input.old_string?.length ?? 0} chars → ${event.input.new_string?.length ?? 0} chars]`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const queuedEdit: QueuedEdit = {
|
|
127
|
+
id: newEditId(),
|
|
128
|
+
kind: toolName as QueuedEdit["kind"],
|
|
129
|
+
filePath: absPath,
|
|
130
|
+
originalContent,
|
|
131
|
+
proposedContent,
|
|
132
|
+
isNewFile: isNew,
|
|
133
|
+
description: `${toolName} → ${path.basename(absPath)}`,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
editQueue.push(queuedEdit);
|
|
137
|
+
|
|
138
|
+
// Notify the user via UI status
|
|
139
|
+
ctx.ui.setStatus("plan-mode", `📋 Plan: ${editQueue.length} edit(s) queued`);
|
|
140
|
+
|
|
141
|
+
// Block the tool from executing — it's queued instead
|
|
142
|
+
return {
|
|
143
|
+
block: true,
|
|
144
|
+
reason: `Queued for plan review: ${queuedEdit.description}. ${editQueue.length} edit(s) in queue. Use /plan off to review and apply.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Register /plan command
|
|
150
|
+
pi.registerCommand("plan", {
|
|
151
|
+
description: "Toggle plan mode. Queues file mutations for batch review.",
|
|
152
|
+
handler: async (_args, ctx) => {
|
|
153
|
+
if (planModeActive) {
|
|
154
|
+
planModeActive = false;
|
|
155
|
+
const count = editQueue.length;
|
|
156
|
+
editQueue.length = 0;
|
|
157
|
+
ctx.ui.setStatus("plan-mode", null);
|
|
158
|
+
ctx.ui.notify(`Plan mode off. ${count} queued edit(s) discarded.`, "warn");
|
|
159
|
+
} else {
|
|
160
|
+
planModeActive = true;
|
|
161
|
+
ctx.ui.setStatus("plan-mode", "📋 Plan mode active");
|
|
162
|
+
ctx.ui.notify("Plan mode on. All file mutations will be queued for review.", "info");
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Register /plan-apply command (review and apply queued edits)
|
|
168
|
+
pi.registerCommand("plan-apply", {
|
|
169
|
+
description: "Review and apply queued plan mode edits.",
|
|
170
|
+
handler: async (_args, ctx) => {
|
|
171
|
+
if (!planModeActive || editQueue.length === 0) {
|
|
172
|
+
ctx.ui.notify("No queued edits to apply.", "info");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const summary = editQueue
|
|
177
|
+
.map(
|
|
178
|
+
(e, i) =>
|
|
179
|
+
`${i + 1}. ${e.kind}: ${e.filePath} (${e.isNewFile ? "new" : hashContent(e.originalContent).slice(0, 8)} → ${hashContent(e.proposedContent).slice(0, 8)})`,
|
|
180
|
+
)
|
|
181
|
+
.join("\n");
|
|
182
|
+
|
|
183
|
+
const confirmed = await ctx.ui.confirm(`Apply ${editQueue.length} queued edit(s)?`, summary);
|
|
184
|
+
|
|
185
|
+
if (confirmed) {
|
|
186
|
+
let applied = 0;
|
|
187
|
+
for (const edit of editQueue) {
|
|
188
|
+
try {
|
|
189
|
+
const dir = path.dirname(edit.filePath);
|
|
190
|
+
if (!fs.existsSync(dir)) {
|
|
191
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
fs.writeFileSync(edit.filePath, edit.proposedContent, "utf-8");
|
|
194
|
+
applied++;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
ctx.ui.notify(`Failed to apply ${edit.filePath}: ${err}`, "error");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
editQueue.length = 0;
|
|
200
|
+
ctx.ui.setStatus("plan-mode", null);
|
|
201
|
+
ctx.ui.notify(`Applied ${applied}/${editQueue.length} edits.`, "success");
|
|
202
|
+
} else {
|
|
203
|
+
editQueue.length = 0;
|
|
204
|
+
ctx.ui.setStatus("plan-mode", null);
|
|
205
|
+
ctx.ui.notify("All queued edits discarded.", "warn");
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Scaffolder Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Scaffolds greenfield project source directories, build configuration,
|
|
5
|
+
* and CI pipeline FROM architecture decisions.
|
|
6
|
+
*
|
|
7
|
+
* This is Epic 0: the first thing run after domain exploration and architecture
|
|
8
|
+
* decisions are made, before any implementation begins.
|
|
9
|
+
*
|
|
10
|
+
* Consistency:
|
|
11
|
+
* /domain → discovers bounded contexts (what to build)
|
|
12
|
+
* /project → scaffolds project from architecture (how to structure it)
|
|
13
|
+
* /architect → plans and orchestrates epics (how to build it)
|
|
14
|
+
*
|
|
15
|
+
* Commands:
|
|
16
|
+
* /project create --lang java --buildTool maven --groupId com.mycompany
|
|
17
|
+
* /project create --lang typescript --validators ci,tests,security --dryRun
|
|
18
|
+
* /project create --lang java --buildTool gradle --force
|
|
19
|
+
* /project status
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { type ExecSyncOptions, execSync } from "node:child_process";
|
|
23
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
|
|
27
|
+
// ── Types ──
|
|
28
|
+
|
|
29
|
+
type ExtensionContext = {
|
|
30
|
+
cwd: string;
|
|
31
|
+
ui: {
|
|
32
|
+
notify(message: string, level?: string): void;
|
|
33
|
+
setStatus(key: string, message: string | null): void;
|
|
34
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type ExtensionAPI = {
|
|
39
|
+
registerCommand(
|
|
40
|
+
name: string,
|
|
41
|
+
options: {
|
|
42
|
+
description: string;
|
|
43
|
+
handler(args: string, ctx: ExtensionContext): unknown | Promise<unknown>;
|
|
44
|
+
},
|
|
45
|
+
): void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ── Helpers ──
|
|
49
|
+
|
|
50
|
+
const SUPPORTED_LANGUAGES = ["typescript", "rust", "python", "go", "java"] as const;
|
|
51
|
+
const BUILD_TOOLS: Record<string, string[]> = {
|
|
52
|
+
java: ["maven", "gradle"],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function detectGuardianCommand(ctx: ExtensionContext): string {
|
|
56
|
+
// Try npx first, then check if local bun project
|
|
57
|
+
if (existsSync(join(ctx.cwd, "node_modules", ".bin", "guardian-framework"))) {
|
|
58
|
+
return "npx guardian-framework";
|
|
59
|
+
}
|
|
60
|
+
if (existsSync(join(ctx.cwd, "src", "index.ts"))) {
|
|
61
|
+
return "bun run src/index.ts";
|
|
62
|
+
}
|
|
63
|
+
return "npx guardian-framework";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cmd(
|
|
67
|
+
base: string,
|
|
68
|
+
args: Record<string, string | boolean | undefined>,
|
|
69
|
+
): string {
|
|
70
|
+
const parts = [base];
|
|
71
|
+
for (const [key, value] of Object.entries(args)) {
|
|
72
|
+
if (value === undefined || value === false) continue;
|
|
73
|
+
const flag = key.length === 1 ? `-${key}` : `--${key}`;
|
|
74
|
+
if (value === true) {
|
|
75
|
+
parts.push(flag);
|
|
76
|
+
} else {
|
|
77
|
+
parts.push(`${flag} "${String(value)}"`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return parts.join(" ");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Extension ──
|
|
84
|
+
|
|
85
|
+
export default function (pi: ExtensionAPI) {
|
|
86
|
+
pi.registerCommand("project", {
|
|
87
|
+
description:
|
|
88
|
+
"Scaffold a greenfield project from architecture decisions. " +
|
|
89
|
+
"Subcommands: create, status",
|
|
90
|
+
handler(args: string, ctx: ExtensionContext) {
|
|
91
|
+
const trimmed = args.trim();
|
|
92
|
+
const segments = trimmed.split(/\s+/);
|
|
93
|
+
const subcommand = segments[0];
|
|
94
|
+
|
|
95
|
+
// ── /project status ──
|
|
96
|
+
if (subcommand === "status") {
|
|
97
|
+
const archDir = join(ctx.cwd, ".pi", "architecture");
|
|
98
|
+
const modulesDir = join(archDir, "modules");
|
|
99
|
+
const decisionsDir = join(archDir, "decisions");
|
|
100
|
+
const srcDir = join(ctx.cwd, "src");
|
|
101
|
+
|
|
102
|
+
const lines: string[] = ["## Project Scaffolding Status", ""];
|
|
103
|
+
|
|
104
|
+
lines.push(`Architecture directory: ${existsSync(archDir) ? "✅" : "❌"} ${archDir}`);
|
|
105
|
+
const moduleCount = existsSync(modulesDir) ? readdirSync(modulesDir).filter((f: string) => f.endsWith(".md")).length : 0;
|
|
106
|
+
const decisionCount = existsSync(decisionsDir) ? readdirSync(decisionsDir).filter((f: string) => f.endsWith(".md")).length : 0;
|
|
107
|
+
lines.push(` Modules: ${existsSync(modulesDir) ? `✅ ${moduleCount} module files` : "❌ not found"}`);
|
|
108
|
+
lines.push(` Decisions: ${existsSync(decisionsDir) ? `✅ ${decisionCount} ADR files` : "❌ not found"}`);
|
|
109
|
+
lines.push(`Source directory: ${existsSync(srcDir) ? "✅ exists" : "⚠️ not yet scaffolded"}`);
|
|
110
|
+
|
|
111
|
+
lines.push("", "### Available Subcommands", "");
|
|
112
|
+
lines.push(" /project create --lang <language> [options]");
|
|
113
|
+
lines.push(" Scaffold project from architecture decisions");
|
|
114
|
+
lines.push("", " /project status");
|
|
115
|
+
lines.push(" Show current scaffolding status");
|
|
116
|
+
|
|
117
|
+
ctx.ui.notify("Project scaffolding status", "info");
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── /project create ──
|
|
122
|
+
if (subcommand === "create") {
|
|
123
|
+
// Parse flags from remaining args
|
|
124
|
+
const flagArgs = segments.slice(1);
|
|
125
|
+
const parsed: Record<string, string | boolean> = {};
|
|
126
|
+
let i = 0;
|
|
127
|
+
while (i < flagArgs.length) {
|
|
128
|
+
const arg = flagArgs[i];
|
|
129
|
+
if (arg.startsWith("--")) {
|
|
130
|
+
const key = arg.slice(2);
|
|
131
|
+
if (flagArgs[i + 1] && !flagArgs[i + 1].startsWith("--")) {
|
|
132
|
+
parsed[key] = flagArgs[i + 1];
|
|
133
|
+
i += 2;
|
|
134
|
+
} else {
|
|
135
|
+
parsed[key] = true;
|
|
136
|
+
i += 1;
|
|
137
|
+
}
|
|
138
|
+
} else if (arg.startsWith("-") && arg.length === 2) {
|
|
139
|
+
const key = arg.slice(1);
|
|
140
|
+
if (flagArgs[i + 1] && !flagArgs[i + 1].startsWith("-")) {
|
|
141
|
+
parsed[key] = flagArgs[i + 1];
|
|
142
|
+
i += 2;
|
|
143
|
+
} else {
|
|
144
|
+
parsed[key] = true;
|
|
145
|
+
i += 1;
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
i += 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const lang = String(parsed.lang || parsed.language || "").toLowerCase();
|
|
153
|
+
const buildTool = String(parsed.buildTool || "").toLowerCase() || undefined;
|
|
154
|
+
|
|
155
|
+
// Read groupId from manifest if not passed via CLI (matches guardian CLI behavior)
|
|
156
|
+
let groupId = String(parsed.groupId || parsed.group || "");
|
|
157
|
+
if (!groupId) {
|
|
158
|
+
try {
|
|
159
|
+
const manifestPath = path.join(ctx.cwd, "guardian-manifest.json");
|
|
160
|
+
if (fs.existsSync(manifestPath)) {
|
|
161
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
162
|
+
groupId = manifest.groupId || "";
|
|
163
|
+
}
|
|
164
|
+
} catch { /* ignore malformed manifest */ }
|
|
165
|
+
}
|
|
166
|
+
if (!groupId) groupId = "com.example";
|
|
167
|
+
const validators = String(parsed.validators || "ci,tests");
|
|
168
|
+
const dryRun = parsed.dryRun === true || parsed["dry-run"] === true || parsed.d === true;
|
|
169
|
+
const force = parsed.force === true || parsed.f === true;
|
|
170
|
+
|
|
171
|
+
// Validate language
|
|
172
|
+
if (!lang || !SUPPORTED_LANGUAGES.includes(lang as typeof SUPPORTED_LANGUAGES[number])) {
|
|
173
|
+
ctx.ui.notify(
|
|
174
|
+
`Unsupported language: "${lang || ""}". Supported: ${SUPPORTED_LANGUAGES.join(", ")}`,
|
|
175
|
+
"error",
|
|
176
|
+
);
|
|
177
|
+
return [
|
|
178
|
+
"Usage: /project create --lang <language> [options]",
|
|
179
|
+
"",
|
|
180
|
+
"Required:",
|
|
181
|
+
` --lang <name> Language: ${SUPPORTED_LANGUAGES.join(", ")}`,
|
|
182
|
+
"",
|
|
183
|
+
"Options:",
|
|
184
|
+
" --buildTool <name> Build tool (maven|gradle for Java)",
|
|
185
|
+
" --groupId <name> Package prefix (default: com.example)",
|
|
186
|
+
" --validators <list> Comma-separated (default: ci,tests)",
|
|
187
|
+
" --dryRun Preview without writing files",
|
|
188
|
+
" --force Override existing project guard",
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Validate build tool for language
|
|
193
|
+
if (lang === "java" && buildTool && !BUILD_TOOLS.java.includes(buildTool)) {
|
|
194
|
+
ctx.ui.notify(
|
|
195
|
+
`Unsupported build tool: "${buildTool}". Java supports: ${BUILD_TOOLS.java.join(", ")}`,
|
|
196
|
+
"error",
|
|
197
|
+
);
|
|
198
|
+
return "(project command handled)";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const guardianCmd = detectGuardianCommand(ctx);
|
|
202
|
+
const shellArgs: Record<string, string | boolean | undefined> = {
|
|
203
|
+
lang,
|
|
204
|
+
buildTool,
|
|
205
|
+
groupId,
|
|
206
|
+
validators,
|
|
207
|
+
};
|
|
208
|
+
if (dryRun) shellArgs.dryRun = true;
|
|
209
|
+
if (force) shellArgs.force = true;
|
|
210
|
+
|
|
211
|
+
const fullCmd = cmd(`${guardianCmd} project create`, shellArgs);
|
|
212
|
+
|
|
213
|
+
ctx.ui.setStatus("command", `/project create — scaffolding ${lang} project`);
|
|
214
|
+
ctx.ui.notify(
|
|
215
|
+
`Scaffolding ${lang} project${buildTool ? ` (${buildTool})` : ""}...`,
|
|
216
|
+
"info",
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const result = execSync(fullCmd, {
|
|
221
|
+
cwd: ctx.cwd,
|
|
222
|
+
encoding: "utf-8",
|
|
223
|
+
timeout: 60000,
|
|
224
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
225
|
+
} as ExecSyncOptions);
|
|
226
|
+
|
|
227
|
+
ctx.ui.setStatus("command", null);
|
|
228
|
+
ctx.ui.notify(
|
|
229
|
+
dryRun ? "Dry run complete" : "Project scaffolding complete",
|
|
230
|
+
"success",
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return [
|
|
234
|
+
"## Project Scaffolding Result",
|
|
235
|
+
"",
|
|
236
|
+
`Language: ${lang}`,
|
|
237
|
+
buildTool ? `Build Tool: ${buildTool}` : null,
|
|
238
|
+
`Group ID: ${groupId}`,
|
|
239
|
+
`Validators: ${validators}`,
|
|
240
|
+
dryRun ? "Mode: **Dry Run** (no files written)" : "Mode: **Live**",
|
|
241
|
+
"",
|
|
242
|
+
"```",
|
|
243
|
+
result.trim(),
|
|
244
|
+
"```",
|
|
245
|
+
"",
|
|
246
|
+
dryRun
|
|
247
|
+
? "Run without --dryRun to create the project."
|
|
248
|
+
: "Project scaffolded. Next: create architecture module docs and use /architect to plan epics.",
|
|
249
|
+
]
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.join("\n");
|
|
252
|
+
} catch (err) {
|
|
253
|
+
ctx.ui.setStatus("command", null);
|
|
254
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
255
|
+
ctx.ui.notify(`Project scaffolding failed: ${errorMsg}`, "error");
|
|
256
|
+
return `Project scaffolding failed:\n\`\`\`\n${errorMsg}\n\`\`\``;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Unknown subcommand ──
|
|
261
|
+
ctx.ui.notify(
|
|
262
|
+
[
|
|
263
|
+
"Usage:",
|
|
264
|
+
" /project create --lang java --buildTool maven --groupId com.mycompany",
|
|
265
|
+
" /project create --lang typescript --validators ci,tests,security --dryRun",
|
|
266
|
+
" /project status",
|
|
267
|
+
].join("\n"),
|
|
268
|
+
"info",
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return [
|
|
272
|
+
"Available /project subcommands:",
|
|
273
|
+
"",
|
|
274
|
+
" /project create --lang <language> [options]",
|
|
275
|
+
" Scaffold project from architecture decisions",
|
|
276
|
+
" Required: --lang (typescript, rust, python, go, java)",
|
|
277
|
+
" Options: --buildTool, --groupId, --validators, --dryRun, --force",
|
|
278
|
+
"",
|
|
279
|
+
" /project status",
|
|
280
|
+
" Show current scaffolding status",
|
|
281
|
+
].join("\n");
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-Only Mode Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Hard-enforces a tiny tool allowlist: read, grep, find, ls.
|
|
5
|
+
* Blocks every other tool call while enabled.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* /read-only → toggle
|
|
9
|
+
* /read-only on → enable
|
|
10
|
+
* /read-only off → disable
|
|
11
|
+
* /read-only status → show current state
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
15
|
+
import {
|
|
16
|
+
createFindTool,
|
|
17
|
+
createGrepTool,
|
|
18
|
+
createLsTool,
|
|
19
|
+
createReadTool,
|
|
20
|
+
} from "@mariozechner/pi-coding-agent";
|
|
21
|
+
|
|
22
|
+
const COMMAND_NAME = "read-only";
|
|
23
|
+
const STATUS_KEY = "read-only-mode";
|
|
24
|
+
const WIDGET_KEY = "read-only-mode";
|
|
25
|
+
const READ_ONLY_TOOL_NAMES = ["read", "grep", "find", "ls"] as const;
|
|
26
|
+
|
|
27
|
+
function getReadOnlyToolNames(pi: ExtensionAPI): string[] {
|
|
28
|
+
const allToolNames = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
29
|
+
return READ_ONLY_TOOL_NAMES.filter((name) => allToolNames.has(name));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function updateUi(pi: ExtensionAPI, ctx: ExtensionContext, enabled: boolean): void {
|
|
33
|
+
if (!enabled) {
|
|
34
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
35
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tools = getReadOnlyToolNames(pi).join(", ");
|
|
40
|
+
const theme = ctx.ui.theme;
|
|
41
|
+
ctx.ui.setStatus(STATUS_KEY, theme.fg("accent", "🔒") + theme.fg("muted", " read-only"));
|
|
42
|
+
ctx.ui.setWidget(WIDGET_KEY, [theme.fg("muted", `🔒 ${tools || "(none)"}`)]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function applyReadOnlyTools(pi: ExtensionAPI): void {
|
|
46
|
+
pi.setActiveTools(getReadOnlyToolNames(pi));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function restoreTools(pi: ExtensionAPI, toolsBefore?: string[]): void {
|
|
50
|
+
const allToolNames = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
51
|
+
const toolNames = (toolsBefore ?? pi.getAllTools().map((tool) => tool.name)).filter((name) =>
|
|
52
|
+
allToolNames.has(name),
|
|
53
|
+
);
|
|
54
|
+
pi.setActiveTools(toolNames);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default function (pi: ExtensionAPI) {
|
|
58
|
+
let enabled = false;
|
|
59
|
+
let toolsBeforeReadOnly: string[] | undefined;
|
|
60
|
+
|
|
61
|
+
// Register read-only variants of allowed tools (use session cwd)
|
|
62
|
+
const readTool = createReadTool(process.cwd());
|
|
63
|
+
pi.registerTool({
|
|
64
|
+
...readTool,
|
|
65
|
+
async execute(tc, params, signal, _up, ctx) {
|
|
66
|
+
return createReadTool(ctx.cwd).execute(tc, params, signal, _up);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const grepTool = createGrepTool(process.cwd());
|
|
71
|
+
pi.registerTool({
|
|
72
|
+
...grepTool,
|
|
73
|
+
async execute(tc, params, signal, _up, ctx) {
|
|
74
|
+
return createGrepTool(ctx.cwd).execute(tc, params, signal, _up);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const findTool = createFindTool(process.cwd());
|
|
79
|
+
pi.registerTool({
|
|
80
|
+
...findTool,
|
|
81
|
+
async execute(tc, params, signal, _up, ctx) {
|
|
82
|
+
return createFindTool(ctx.cwd).execute(tc, params, signal, _up);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const lsTool = createLsTool(process.cwd());
|
|
87
|
+
pi.registerTool({
|
|
88
|
+
...lsTool,
|
|
89
|
+
async execute(tc, params, signal, _up, ctx) {
|
|
90
|
+
return createLsTool(ctx.cwd).execute(tc, params, signal, _up);
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function enable(ctx: ExtensionContext): void {
|
|
95
|
+
if (enabled) {
|
|
96
|
+
updateUi(pi, ctx, enabled);
|
|
97
|
+
ctx.ui.notify("Read-only mode is already enabled.", "info");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
enabled = true;
|
|
101
|
+
toolsBeforeReadOnly = pi.getActiveTools();
|
|
102
|
+
applyReadOnlyTools(pi);
|
|
103
|
+
updateUi(pi, ctx, enabled);
|
|
104
|
+
ctx.ui.notify(
|
|
105
|
+
`Read-only mode enabled. Tools: ${getReadOnlyToolNames(pi).join(", ") || "(none)"}.`,
|
|
106
|
+
"info",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function disable(ctx: ExtensionContext): void {
|
|
111
|
+
if (!enabled) {
|
|
112
|
+
updateUi(pi, ctx, enabled);
|
|
113
|
+
ctx.ui.notify("Read-only mode is already disabled.", "info");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
enabled = false;
|
|
117
|
+
restoreTools(pi, toolsBeforeReadOnly);
|
|
118
|
+
toolsBeforeReadOnly = undefined;
|
|
119
|
+
updateUi(pi, ctx, enabled);
|
|
120
|
+
ctx.ui.notify("Read-only mode disabled. Previous tool access restored.", "info");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
pi.registerCommand(COMMAND_NAME, {
|
|
124
|
+
description: "Toggle hard-enforced read-only mode",
|
|
125
|
+
handler: async (args, ctx) => {
|
|
126
|
+
const action = args.trim().toLowerCase();
|
|
127
|
+
switch (action) {
|
|
128
|
+
case "":
|
|
129
|
+
case "toggle":
|
|
130
|
+
toggle(ctx);
|
|
131
|
+
return;
|
|
132
|
+
case "on":
|
|
133
|
+
case "enable":
|
|
134
|
+
enable(ctx);
|
|
135
|
+
return;
|
|
136
|
+
case "off":
|
|
137
|
+
case "disable":
|
|
138
|
+
disable(ctx);
|
|
139
|
+
return;
|
|
140
|
+
case "status":
|
|
141
|
+
updateUi(pi, ctx, enabled);
|
|
142
|
+
ctx.ui.notify(
|
|
143
|
+
enabled
|
|
144
|
+
? `Read-only mode is ON. Allowed: ${getReadOnlyToolNames(pi).join(", ") || "(none)"}.`
|
|
145
|
+
: "Read-only mode is OFF.",
|
|
146
|
+
"info",
|
|
147
|
+
);
|
|
148
|
+
return;
|
|
149
|
+
default:
|
|
150
|
+
ctx.ui.notify(`Usage: /${COMMAND_NAME} [on|off|toggle|status]`, "warning");
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
156
|
+
if (!enabled) {
|
|
157
|
+
updateUi(pi, ctx, enabled);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
applyReadOnlyTools(pi);
|
|
161
|
+
updateUi(pi, ctx, enabled);
|
|
162
|
+
const tools = getReadOnlyToolNames(pi).join(", ") || "(none)";
|
|
163
|
+
return {
|
|
164
|
+
systemPrompt: `${event.systemPrompt}\n\n[Read-only mode is active]\n- You may only use these tools: ${tools}.\n- You must not attempt any action that changes local files, processes, git state, dependencies, databases, remote systems, or any other external state.\n- If the user asks for any write or side-effecting action, explain that read-only mode is enabled and tell them to run /${COMMAND_NAME} off first.`,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
pi.on("tool_call", async (event) => {
|
|
169
|
+
if (!enabled) return;
|
|
170
|
+
const allowed = new Set(getReadOnlyToolNames(pi));
|
|
171
|
+
if (allowed.has(event.toolName)) return;
|
|
172
|
+
return {
|
|
173
|
+
block: true,
|
|
174
|
+
reason: `Read-only mode is active. Tool "${event.toolName}" is blocked. Allowed: ${Array.from(allowed).join(", ") || "(none)"}. Use /${COMMAND_NAME} off to restore full tool access.`,
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
179
|
+
if (enabled) applyReadOnlyTools(pi);
|
|
180
|
+
updateUi(pi, ctx, enabled);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
184
|
+
if (enabled) applyReadOnlyTools(pi);
|
|
185
|
+
updateUi(pi, ctx, enabled);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
pi.on("session_fork", async (_event, ctx) => {
|
|
189
|
+
if (enabled) applyReadOnlyTools(pi);
|
|
190
|
+
updateUi(pi, ctx, enabled);
|
|
191
|
+
});
|
|
192
|
+
}
|