@slowdini/slow-powers-opencode 0.4.1 → 0.4.3
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/README.md +2 -6
- package/opencode/plugins/slow-powers.js +30 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,12 +60,8 @@ You can also browse and install it interactively: run `codex`, open
|
|
|
60
60
|
|
|
61
61
|
### OpenCode
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
```json
|
|
66
|
-
{
|
|
67
|
-
"plugin": ["@slowdini/slow-powers-opencode"]
|
|
68
|
-
}
|
|
63
|
+
```bash
|
|
64
|
+
opencode plugin @slowdini/slow-powers-opencode -g
|
|
69
65
|
```
|
|
70
66
|
|
|
71
67
|
## The skills
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* Intercepts plan file writes in plan mode and triggers hardening-plans skill.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
10
9
|
import fs from "node:fs";
|
|
11
10
|
import path from "node:path";
|
|
12
11
|
import { fileURLToPath } from "node:url";
|
|
@@ -24,12 +23,18 @@ const bootstrapLeadingPhrase = "<EXTREMELY-IMPORTANT>";
|
|
|
24
23
|
// once eliminates redundant fs work on every agent step.
|
|
25
24
|
let _bootstrapCache; // undefined = not yet loaded, null = file missing
|
|
26
25
|
|
|
27
|
-
// Deduplication state for plan hardening
|
|
28
|
-
// Map<filePath, contentHash> - tracks processed plan file versions
|
|
29
|
-
const processedPlanHashes = new Map();
|
|
30
|
-
const HARDENED_MARKER = "<!-- hardened-plans -->";
|
|
31
|
-
|
|
32
26
|
export const SlowPowersPlugin = async ({ client, directory: _directory }) => {
|
|
27
|
+
// Tracks plan files we've already sent the hardening prompt for, keyed by
|
|
28
|
+
// `${sessionID}:${filePath}` so different sessions with the same plan path
|
|
29
|
+
// still get prompted. Scoped to the plugin instance (one per opencode process).
|
|
30
|
+
const hardeningPromptSentFor = new Set();
|
|
31
|
+
|
|
32
|
+
const log = (level, message) => {
|
|
33
|
+
client.app
|
|
34
|
+
.log({ body: { service: "slow-powers", level, message } })
|
|
35
|
+
.catch(() => {});
|
|
36
|
+
};
|
|
37
|
+
|
|
33
38
|
// Helper to load bootstrap content (cached after first call)
|
|
34
39
|
const getBootstrapContent = () => {
|
|
35
40
|
if (_bootstrapCache !== undefined) return _bootstrapCache;
|
|
@@ -44,41 +49,34 @@ export const SlowPowersPlugin = async ({ client, directory: _directory }) => {
|
|
|
44
49
|
return _bootstrapCache;
|
|
45
50
|
};
|
|
46
51
|
|
|
47
|
-
const hashContent = (content) =>
|
|
48
|
-
createHash("sha256").update(content).digest("hex");
|
|
49
|
-
|
|
50
|
-
const isPlanHardened = (content) => content.includes(HARDENED_MARKER);
|
|
51
|
-
|
|
52
52
|
const handlePlanFileEdit = async (event) => {
|
|
53
53
|
const filePath = event.properties.file;
|
|
54
54
|
const sessionID = event.properties.sessionID;
|
|
55
55
|
|
|
56
|
-
if (!filePath || !sessionID)
|
|
57
|
-
|
|
58
|
-
if (!filePath.match(/\.opencode\/plans\/.*\.md$/)) return;
|
|
59
|
-
|
|
60
|
-
let session;
|
|
61
|
-
try {
|
|
62
|
-
session = await client.session.get({ path: { id: sessionID } });
|
|
63
|
-
} catch {
|
|
56
|
+
if (!filePath || !sessionID) {
|
|
57
|
+
log("debug", `[hardening] skipped: missing filePath or sessionID`);
|
|
64
58
|
return;
|
|
65
59
|
}
|
|
66
|
-
if (session.agent !== "plan") return;
|
|
67
60
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
content = fs.readFileSync(filePath, "utf8");
|
|
71
|
-
} catch {
|
|
61
|
+
if (!filePath.match(/\.opencode\/plans\/.*\.md$/)) {
|
|
62
|
+
log("debug", `[hardening] skipped: ${filePath} not in .opencode/plans/`);
|
|
72
63
|
return;
|
|
73
64
|
}
|
|
74
65
|
|
|
75
|
-
|
|
66
|
+
const promptKey = `${sessionID}:${filePath}`;
|
|
76
67
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (
|
|
68
|
+
// Only prompt once per plan file per session. After we've asked the agent
|
|
69
|
+
// to harden it, we trust them to do so or not; re-prompting causes loops.
|
|
70
|
+
if (hardeningPromptSentFor.has(promptKey)) {
|
|
71
|
+
log("debug", `[hardening] skipped: already prompted for ${promptKey}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
80
74
|
|
|
81
|
-
|
|
75
|
+
hardeningPromptSentFor.add(promptKey);
|
|
76
|
+
log(
|
|
77
|
+
"info",
|
|
78
|
+
`[hardening] prompting agent to harden ${filePath} in session ${sessionID}`,
|
|
79
|
+
);
|
|
82
80
|
|
|
83
81
|
try {
|
|
84
82
|
await client.session.prompt({
|
|
@@ -88,14 +86,14 @@ export const SlowPowersPlugin = async ({ client, directory: _directory }) => {
|
|
|
88
86
|
parts: [
|
|
89
87
|
{
|
|
90
88
|
type: "text",
|
|
91
|
-
text: `The plan at ${filePath} has been written.
|
|
89
|
+
text: `The plan at ${filePath} has been written. If not already done, please run the hardening-plans skill on this plan file to review it before presentation.`,
|
|
92
90
|
},
|
|
93
91
|
],
|
|
94
92
|
},
|
|
95
93
|
});
|
|
96
94
|
} catch (err) {
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
hardeningPromptSentFor.delete(promptKey);
|
|
96
|
+
log("error", `[hardening] failed to trigger hardening-plans: ${err}`);
|
|
99
97
|
}
|
|
100
98
|
};
|
|
101
99
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slowdini/slow-powers-opencode",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Slow-powers — structured development workflows for coding agents (TDD, debugging, verification, git hygiene)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./opencode/plugins/slow-powers.js",
|