@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 CHANGED
@@ -60,12 +60,8 @@ You can also browse and install it interactively: run `codex`, open
60
60
 
61
61
  ### OpenCode
62
62
 
63
- Add Slow-powers to the `plugin` array in `~/.config/opencode/opencode.json`:
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) return;
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
- let content;
69
- try {
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
- if (isPlanHardened(content)) return;
66
+ const promptKey = `${sessionID}:${filePath}`;
76
67
 
77
- const contentHash = hashContent(content);
78
- const previousHash = processedPlanHashes.get(filePath);
79
- if (previousHash === contentHash) return;
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
- processedPlanHashes.set(filePath, contentHash);
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. Please run the hardening-plans skill on this plan file to review it for hallucinations, missing file references, vague steps, and coverage gaps before presenting it. Update the file in place with the hardened version. Add ${HARDENED_MARKER} marker when done.`,
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
- processedPlanHashes.delete(filePath);
98
- console.error("[slow-powers] Failed to trigger hardening-plans:", err);
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.1",
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",