@kickythrust/pi-kicky-glue 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 +98 -0
- package/extensions/index.ts +165 -0
- package/package.json +55 -0
- package/skills/kicky-glue/SKILL.md +62 -0
- package/src/classifiers.ts +130 -0
- package/src/config.ts +33 -0
- package/src/context.ts +71 -0
- package/src/state.ts +127 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bogdan Cristian Bucurenciu (Kicky)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# pi-kicky-glue
|
|
2
|
+
|
|
3
|
+
Pi-native agent operating glue for Kicky.
|
|
4
|
+
|
|
5
|
+
This package bundles Kicky's first layer of Pi harness discipline: safety blockers, workflow reminders, and owned-stack context pointers. It is personal-first and publishable; generic extraction can happen later if the patterns earn it.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
- **Blocks dangerous bash** before execution: force-pushes, destructive git resets/cleans, direct secret reads, kill-by-port pipelines, and broad `rm -rf` targets.
|
|
10
|
+
- **Confirms risky bash** when a UI is available: normal `git push`, `gh pr create`, `kata --done`, `sed -i`, `sudo`, `chmod 777`, and recursive deletes.
|
|
11
|
+
- **Nudges full reads** after repeated search/snippet operations. Search locates; reading understands.
|
|
12
|
+
- **Nudges code pride** after code edits. Before final response, reread edited files and check imports, types, control flow, conventions, and owner-grade cleanliness.
|
|
13
|
+
- **Injects owned-stack pointers** when prompts mention Pi, Runline, Dripline, Dripyard, kata, fnox, Omarchy, or Michaelliv.
|
|
14
|
+
- **Adds commands**: `/kicky-glue-status` and `/kicky-glue-config`.
|
|
15
|
+
|
|
16
|
+
No Claude Code dependency. Claude artifacts are source material only; Pi is the harness.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
From npm, after publishing:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pi install npm:@kickythrust/pi-kicky-glue
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Local dogfood:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pi install /home/kicky/work/studio/kicky/pi-kicky-glue
|
|
30
|
+
/reload
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
| Command | Description |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `/kicky-glue-status` | Show enabled features and lightweight runtime state. |
|
|
38
|
+
| `/kicky-glue-config` | Show environment config knobs. |
|
|
39
|
+
|
|
40
|
+
## Environment config
|
|
41
|
+
|
|
42
|
+
| Env var | Effect |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `PI_KICKY_GLUE_DISABLE=1` | Disable the whole package. |
|
|
45
|
+
| `PI_KICKY_GLUE_NO_READ_FULLY=1` | Disable repeated-search reminder. |
|
|
46
|
+
| `PI_KICKY_GLUE_NO_CODE_PRIDE=1` | Disable post-edit code-pride reminder. |
|
|
47
|
+
| `PI_KICKY_GLUE_NO_DANGEROUS_BASH=1` | Disable bash blocker/confirmation gate. |
|
|
48
|
+
| `PI_KICKY_GLUE_NO_CONTEXT=1` | Disable owned-stack pointer injection. |
|
|
49
|
+
| `PI_KICKY_GLUE_READ_THRESHOLD=3` | Change repeated-search threshold. Default: `2`. |
|
|
50
|
+
|
|
51
|
+
## Safety behavior
|
|
52
|
+
|
|
53
|
+
Hard-blocked by default:
|
|
54
|
+
|
|
55
|
+
- `git push --force` / `--force-with-lease`
|
|
56
|
+
- `git reset --hard`
|
|
57
|
+
- forceful `git clean -fd`
|
|
58
|
+
- `lsof -ti:<port> | xargs kill`
|
|
59
|
+
- direct secret reads such as `fnox get`, `psst get`, `op read`, `cat .env`, `env`, `printenv`
|
|
60
|
+
- broad `rm -rf /`, `rm -rf ~`, `rm -rf $HOME`
|
|
61
|
+
|
|
62
|
+
Confirmed in TUI/RPC, blocked in non-interactive modes:
|
|
63
|
+
|
|
64
|
+
- ordinary `git push`
|
|
65
|
+
- `gh pr create`, `kata --done`, `npm version`
|
|
66
|
+
- `sed -i` / sed redirect editing
|
|
67
|
+
- `sudo`
|
|
68
|
+
- `chmod/chown 777`
|
|
69
|
+
- recursive deletes not covered by the hard block
|
|
70
|
+
|
|
71
|
+
## Package shape
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
pi-kicky-glue/
|
|
75
|
+
├── extensions/index.ts # Pi extension entrypoint
|
|
76
|
+
├── src/ # Pure tested modules
|
|
77
|
+
├── skills/kicky-glue/ # Skill docs loaded by Pi
|
|
78
|
+
└── tests/ # Bun tests for pure modules
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bun install
|
|
85
|
+
bun test
|
|
86
|
+
bun run typecheck
|
|
87
|
+
npm pack --dry-run
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Why this exists
|
|
91
|
+
|
|
92
|
+
Michael's dotfiles showed the real lesson: the agent stack needs glue, not more tools. Small reminders and hard blockers keep the model inside the operator's discipline.
|
|
93
|
+
|
|
94
|
+
For Kicky, that glue belongs in Pi.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { classifyBashCommand } from "../src/classifiers";
|
|
3
|
+
import { loadConfig } from "../src/config";
|
|
4
|
+
import { getOwnedStackPointers } from "../src/context";
|
|
5
|
+
import { createGlueState } from "../src/state";
|
|
6
|
+
|
|
7
|
+
const READ_FULLY_REMINDER = [
|
|
8
|
+
"Kicky glue reminder: search locates; reading understands.",
|
|
9
|
+
"You've been using snippet/search surfaces. Before editing based on those results, read the full symbol or file so imports, types, control flow, and local conventions are visible.",
|
|
10
|
+
].join("\n");
|
|
11
|
+
|
|
12
|
+
function getCommand(input: unknown): string {
|
|
13
|
+
if (!input || typeof input !== "object") return "";
|
|
14
|
+
const command = (input as { command?: unknown }).command;
|
|
15
|
+
return typeof command === "string" ? command : "";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function textList(items: string[]): string {
|
|
19
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function codePrideReminder(paths: string[]): string {
|
|
23
|
+
return [
|
|
24
|
+
"Kicky glue reminder: code edited this turn.",
|
|
25
|
+
"Before the final answer, reread the edited code paths and verify imports, types, control flow, conventions, and owner-grade cleanliness.",
|
|
26
|
+
"Edited code paths:",
|
|
27
|
+
textList(paths),
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ownedStackContext(prompt: string): string | undefined {
|
|
32
|
+
const pointers = getOwnedStackPointers(prompt);
|
|
33
|
+
if (pointers.length === 0) return undefined;
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
"\n## Kicky owned-stack pointers",
|
|
37
|
+
"The prompt touches Kicky-owned stack / Pi glue territory. Use progressive disclosure: read only the specific notes needed.",
|
|
38
|
+
...pointers.map((pointer) => `- ${pointer.label}: ${pointer.path} — ${pointer.reason}`),
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function kickyGlue(pi: ExtensionAPI) {
|
|
43
|
+
const config = loadConfig();
|
|
44
|
+
const state = createGlueState({ readFullyThreshold: config.readFullyThreshold });
|
|
45
|
+
|
|
46
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
47
|
+
if (!config.enabled) {
|
|
48
|
+
ctx.ui.setStatus("kicky-glue", "kicky-glue off");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
ctx.ui.setStatus("kicky-glue", "kicky-glue on");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
55
|
+
ctx.ui.setStatus("kicky-glue", undefined);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
59
|
+
if (!config.dangerousBash || event.toolName !== "bash") return undefined;
|
|
60
|
+
|
|
61
|
+
const command = getCommand(event.input);
|
|
62
|
+
const classification = classifyBashCommand(command);
|
|
63
|
+
|
|
64
|
+
if (classification.action === "allow") return undefined;
|
|
65
|
+
|
|
66
|
+
const reason = classification.reason ?? `Matched ${classification.rule ?? "unknown"}`;
|
|
67
|
+
|
|
68
|
+
if (classification.action === "block") {
|
|
69
|
+
if (ctx.hasUI) ctx.ui.notify(`kicky-glue blocked bash: ${classification.rule ?? "unknown"}`, "warning");
|
|
70
|
+
return { block: true, reason };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!ctx.hasUI) {
|
|
74
|
+
return {
|
|
75
|
+
block: true,
|
|
76
|
+
reason: `Command requires confirmation but this Pi mode has no UI. ${reason}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const confirmed = await ctx.ui.confirm(
|
|
81
|
+
`kicky-glue confirmation: ${classification.rule ?? "bash command"}`,
|
|
82
|
+
`${reason}\n\nCommand:\n${command}\n\nAllow?`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (!confirmed) {
|
|
86
|
+
return { block: true, reason: "Blocked by user via kicky-glue confirmation." };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return undefined;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
pi.on("tool_result", async (event) => {
|
|
93
|
+
if (!config.enabled) return undefined;
|
|
94
|
+
|
|
95
|
+
if (config.readFully && state.recordSearch(event.toolName, event.input)) {
|
|
96
|
+
pi.sendMessage(
|
|
97
|
+
{
|
|
98
|
+
customType: "kicky-glue-reminder",
|
|
99
|
+
content: READ_FULLY_REMINDER,
|
|
100
|
+
display: true,
|
|
101
|
+
details: { kind: "read-fully" },
|
|
102
|
+
},
|
|
103
|
+
{ deliverAs: "steer" },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const editedPaths = config.codePride ? state.recordEdit(event.toolName, event.input) : [];
|
|
108
|
+
if (editedPaths.length > 0) {
|
|
109
|
+
const paths = state.drainEditedCodePaths();
|
|
110
|
+
pi.sendMessage(
|
|
111
|
+
{
|
|
112
|
+
customType: "kicky-glue-reminder",
|
|
113
|
+
content: codePrideReminder(paths),
|
|
114
|
+
display: true,
|
|
115
|
+
details: { kind: "code-pride", paths },
|
|
116
|
+
},
|
|
117
|
+
{ deliverAs: "steer" },
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return undefined;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
pi.on("before_agent_start", async (event) => {
|
|
125
|
+
if (!config.ownedStackContext) return undefined;
|
|
126
|
+
const context = ownedStackContext(event.prompt);
|
|
127
|
+
if (!context) return undefined;
|
|
128
|
+
return { systemPrompt: event.systemPrompt + context };
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
pi.registerCommand("kicky-glue-status", {
|
|
132
|
+
description: "Show pi-kicky-glue runtime status",
|
|
133
|
+
handler: async (_args, ctx) => {
|
|
134
|
+
const status = state.status();
|
|
135
|
+
const lines = [
|
|
136
|
+
`enabled: ${config.enabled}`,
|
|
137
|
+
`readFully: ${config.readFully} (threshold ${config.readFullyThreshold})`,
|
|
138
|
+
`codePride: ${config.codePride}`,
|
|
139
|
+
`dangerousBash: ${config.dangerousBash}`,
|
|
140
|
+
`ownedStackContext: ${config.ownedStackContext}`,
|
|
141
|
+
`consecutiveSearches: ${status.consecutiveSearches}`,
|
|
142
|
+
`pendingEditedCodePaths: ${status.pendingEditedCodePaths.length}`,
|
|
143
|
+
];
|
|
144
|
+
ctx.ui.notify(`pi-kicky-glue status\n${lines.join("\n")}`, "info");
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
pi.registerCommand("kicky-glue-config", {
|
|
149
|
+
description: "Show pi-kicky-glue environment config knobs",
|
|
150
|
+
handler: async (_args, ctx) => {
|
|
151
|
+
ctx.ui.notify(
|
|
152
|
+
[
|
|
153
|
+
"pi-kicky-glue env config:",
|
|
154
|
+
"PI_KICKY_GLUE_DISABLE=1",
|
|
155
|
+
"PI_KICKY_GLUE_NO_READ_FULLY=1",
|
|
156
|
+
"PI_KICKY_GLUE_NO_CODE_PRIDE=1",
|
|
157
|
+
"PI_KICKY_GLUE_NO_DANGEROUS_BASH=1",
|
|
158
|
+
"PI_KICKY_GLUE_NO_CONTEXT=1",
|
|
159
|
+
"PI_KICKY_GLUE_READ_THRESHOLD=3",
|
|
160
|
+
].join("\n"),
|
|
161
|
+
"info",
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kickythrust/pi-kicky-glue",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension pack for Kicky's agent operating glue: safety blockers, context nudges, doc/model checks, and owner-grade workflow reminders.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"agent",
|
|
9
|
+
"safety",
|
|
10
|
+
"reminders",
|
|
11
|
+
"kicky",
|
|
12
|
+
"workflow"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Bogdan Cristian Bucurenciu (Kicky)",
|
|
16
|
+
"homepage": "https://github.com/Bucurenciu-Cristian/pi-kicky-glue",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/Bucurenciu-Cristian/pi-kicky-glue.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/Bucurenciu-Cristian/pi-kicky-glue/issues"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"files": [
|
|
26
|
+
"extensions/",
|
|
27
|
+
"src/",
|
|
28
|
+
"skills/",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "bun test",
|
|
34
|
+
"typecheck": "tsc --noEmit"
|
|
35
|
+
},
|
|
36
|
+
"pi": {
|
|
37
|
+
"extensions": [
|
|
38
|
+
"./extensions"
|
|
39
|
+
],
|
|
40
|
+
"skills": [
|
|
41
|
+
"./skills"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
49
|
+
"@types/bun": "latest",
|
|
50
|
+
"typescript": "latest"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kicky-glue
|
|
3
|
+
description: Use when working on Kicky's Pi harness glue package, debugging pi-kicky-glue reminders/blockers, or deciding whether a workflow should become a Pi reminder, blocker, skill, or owned-stack pointer.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Kicky Glue
|
|
7
|
+
|
|
8
|
+
This skill documents the operating rules for `@kickythrust/pi-kicky-glue`.
|
|
9
|
+
|
|
10
|
+
## Core decision
|
|
11
|
+
|
|
12
|
+
Pi is Kicky's coding-agent harness. Claude Code artifacts are source material only.
|
|
13
|
+
|
|
14
|
+
Do not add `.claude` runtime dependencies to this package. If a Claude hook is useful, port the behavior to Pi extension events.
|
|
15
|
+
|
|
16
|
+
## Classification rule of thumb
|
|
17
|
+
|
|
18
|
+
- Frequent but harmless behavior → skill or AGENTS guideline.
|
|
19
|
+
- Frequent and often forgotten behavior → reminder.
|
|
20
|
+
- Dangerous when wrong → blocker or confirmation gate.
|
|
21
|
+
- Broad/low-frequency action surface → Runline.
|
|
22
|
+
- Read/sync/query surface → Dripline.
|
|
23
|
+
- Durable context → Napkin / kicky-brain.
|
|
24
|
+
- Issue state → kata.
|
|
25
|
+
|
|
26
|
+
## Current features
|
|
27
|
+
|
|
28
|
+
- `dangerous-bash`: blocks or confirms risky bash commands.
|
|
29
|
+
- `read-fully`: reminds after repeated search/snippet operations.
|
|
30
|
+
- `code-pride`: reminds after code edits.
|
|
31
|
+
- `owned-stack-context`: injects concise pointers to canonical Kicky notes when relevant.
|
|
32
|
+
|
|
33
|
+
## Development workflow
|
|
34
|
+
|
|
35
|
+
Use TDD for classifier/state/context behavior:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bun test
|
|
39
|
+
bun run typecheck
|
|
40
|
+
npm pack --dry-run
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Keep the extension entrypoint thin. Add pure behavior to `src/` and tests under `tests/`.
|
|
44
|
+
|
|
45
|
+
## Boundaries
|
|
46
|
+
|
|
47
|
+
V1 must not:
|
|
48
|
+
|
|
49
|
+
- auto-edit docs
|
|
50
|
+
- auto-commit
|
|
51
|
+
- create kata issues
|
|
52
|
+
- call LLMs internally
|
|
53
|
+
- install tools
|
|
54
|
+
- modify `pi-fnox`
|
|
55
|
+
- depend on Claude Code
|
|
56
|
+
|
|
57
|
+
## Useful references
|
|
58
|
+
|
|
59
|
+
- `~/personal/kicky-brain/kicky-brain/concepts/michaelliv-system-map.md`
|
|
60
|
+
- `~/personal/kicky-brain/kicky-brain/decisions/2026-07-06-pi-harness-over-claude-code.md`
|
|
61
|
+
- Pi docs: `/home/kicky/.cache/.bun/install/global/node_modules/@earendil-works/pi-coding-agent/docs/extensions.md`
|
|
62
|
+
- Pi package docs: `/home/kicky/.cache/.bun/install/global/node_modules/@earendil-works/pi-coding-agent/docs/packages.md`
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export type BashAction = "allow" | "confirm" | "block";
|
|
2
|
+
|
|
3
|
+
export interface BashClassification {
|
|
4
|
+
action: BashAction;
|
|
5
|
+
rule?: string;
|
|
6
|
+
reason?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface BashClassifierOptions {
|
|
10
|
+
confirmGitPush: boolean;
|
|
11
|
+
blockForcePush: boolean;
|
|
12
|
+
blockSecretReads: boolean;
|
|
13
|
+
blockKillByPort: boolean;
|
|
14
|
+
blockSedEditing: boolean;
|
|
15
|
+
confirmModelDocActions: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_BASH_CLASSIFIER_OPTIONS: BashClassifierOptions = {
|
|
19
|
+
confirmGitPush: true,
|
|
20
|
+
blockForcePush: true,
|
|
21
|
+
blockSecretReads: true,
|
|
22
|
+
blockKillByPort: true,
|
|
23
|
+
blockSedEditing: true,
|
|
24
|
+
confirmModelDocActions: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const BLOCK_RULES: Array<{
|
|
28
|
+
rule: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
pattern: RegExp;
|
|
31
|
+
option?: keyof BashClassifierOptions;
|
|
32
|
+
}> = [
|
|
33
|
+
{
|
|
34
|
+
rule: "git-force-push",
|
|
35
|
+
reason: "Force-push is destructive and requires explicit human scope outside the command.",
|
|
36
|
+
pattern: /\bgit\s+push\b[\s\S]*(--force(?:-with-lease)?|-f(?:\s|$))/i,
|
|
37
|
+
option: "blockForcePush",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
rule: "git-reset-hard",
|
|
41
|
+
reason: "git reset --hard can destroy local work.",
|
|
42
|
+
pattern: /\bgit\s+reset\b[\s\S]*--hard\b/i,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
rule: "git-clean-force",
|
|
46
|
+
reason: "git clean with force can delete untracked work.",
|
|
47
|
+
pattern: /\bgit\s+clean\b[\s\S]*-(?:[a-z]*f[a-z]*d|[a-z]*d[a-z]*f)\b/i,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
rule: "kill-by-port",
|
|
51
|
+
reason: "Kill-by-port can terminate the agent or unrelated services; stop the named service instead.",
|
|
52
|
+
pattern: /\blsof\b[\s\S]*-t[i]?\s*:?[0-9]+[\s\S]*\|[\s\S]*\bxargs\s+kill\b/i,
|
|
53
|
+
option: "blockKillByPort",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
rule: "secret-read",
|
|
57
|
+
reason: "Agents may use secrets through injected environment variables, but must not print secret values.",
|
|
58
|
+
pattern: /(^|[;&|]\s*)(fnox|psst)\s+(get|export|show|cat)\b|\bop\s+read\b|\bcat\s+([^\n;&|]*\/)?\.env(\.[A-Za-z0-9_-]+)?\b|(^|[;&|]\s*)(printenv|env)\s*($|[;&|])/i,
|
|
59
|
+
option: "blockSecretReads",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
rule: "rm-rf-root",
|
|
63
|
+
reason: "Broad rm -rf target is too destructive for agent execution.",
|
|
64
|
+
pattern: /\brm\s+-(?:[a-z]*r[a-z]*f|[a-z]*f[a-z]*r)\s+(\/|~|\$HOME)(?:\s|$)/i,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const CONFIRM_RULES: Array<{
|
|
69
|
+
rule: string;
|
|
70
|
+
reason: string;
|
|
71
|
+
pattern: RegExp;
|
|
72
|
+
option?: keyof BashClassifierOptions;
|
|
73
|
+
}> = [
|
|
74
|
+
{
|
|
75
|
+
rule: "model-doc-check",
|
|
76
|
+
reason: "Before finalizing, check whether domain docs, AGENTS.md, runbooks, or kata evidence need updates.",
|
|
77
|
+
pattern: /\bgh\s+pr\s+create\b|\bkata\b[\s\S]*(--done|\bdone\b)|\bnpm\s+version\b/i,
|
|
78
|
+
option: "confirmModelDocActions",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
rule: "git-push",
|
|
82
|
+
reason: "Push publishes work; confirm durable docs/model/evidence are current.",
|
|
83
|
+
pattern: /\bgit\s+push\b/i,
|
|
84
|
+
option: "confirmGitPush",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
rule: "sed-editing",
|
|
88
|
+
reason: "Brittle sed file editing is discouraged; use Pi's edit tool for exact replacements.",
|
|
89
|
+
pattern: /\bsed\b[\s\S]*(-i\b|>\s*[^|;&]+)/i,
|
|
90
|
+
option: "blockSedEditing",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
rule: "sudo",
|
|
94
|
+
reason: "sudo crosses privilege boundaries.",
|
|
95
|
+
pattern: /\bsudo\b/i,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
rule: "chmod-777",
|
|
99
|
+
reason: "chmod/chown 777 is usually an unsafe permission change.",
|
|
100
|
+
pattern: /\b(chmod|chown)\b[\s\S]*\b777\b/i,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
rule: "rm-rf",
|
|
104
|
+
reason: "Recursive deletion requires exact human confirmation.",
|
|
105
|
+
pattern: /\brm\s+-(?:[a-z]*r[a-z]*f|[a-z]*f[a-z]*r)\b/i,
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
export function classifyBashCommand(
|
|
110
|
+
command: string,
|
|
111
|
+
options: Partial<BashClassifierOptions> = {},
|
|
112
|
+
): BashClassification {
|
|
113
|
+
const resolved = { ...DEFAULT_BASH_CLASSIFIER_OPTIONS, ...options };
|
|
114
|
+
|
|
115
|
+
for (const rule of BLOCK_RULES) {
|
|
116
|
+
if (rule.option && !resolved[rule.option]) continue;
|
|
117
|
+
if (rule.pattern.test(command)) {
|
|
118
|
+
return { action: "block", rule: rule.rule, reason: rule.reason };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const rule of CONFIRM_RULES) {
|
|
123
|
+
if (rule.option && !resolved[rule.option]) continue;
|
|
124
|
+
if (rule.pattern.test(command)) {
|
|
125
|
+
return { action: "confirm", rule: rule.rule, reason: rule.reason };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { action: "allow" };
|
|
130
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface KickyGlueConfig {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
readFully: boolean;
|
|
4
|
+
codePride: boolean;
|
|
5
|
+
dangerousBash: boolean;
|
|
6
|
+
ownedStackContext: boolean;
|
|
7
|
+
readFullyThreshold: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function envFlag(name: string, defaultValue: boolean): boolean {
|
|
11
|
+
const value = process.env[name];
|
|
12
|
+
if (value === undefined) return defaultValue;
|
|
13
|
+
return !["0", "false", "no", "off"].includes(value.toLowerCase());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function envNumber(name: string, defaultValue: number): number {
|
|
17
|
+
const value = process.env[name];
|
|
18
|
+
if (value === undefined) return defaultValue;
|
|
19
|
+
const parsed = Number(value);
|
|
20
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadConfig(): KickyGlueConfig {
|
|
24
|
+
const enabled = envFlag("PI_KICKY_GLUE_ENABLED", true) && !envFlag("PI_KICKY_GLUE_DISABLE", false);
|
|
25
|
+
return {
|
|
26
|
+
enabled,
|
|
27
|
+
readFully: enabled && !envFlag("PI_KICKY_GLUE_NO_READ_FULLY", false),
|
|
28
|
+
codePride: enabled && !envFlag("PI_KICKY_GLUE_NO_CODE_PRIDE", false),
|
|
29
|
+
dangerousBash: enabled && !envFlag("PI_KICKY_GLUE_NO_DANGEROUS_BASH", false),
|
|
30
|
+
ownedStackContext: enabled && !envFlag("PI_KICKY_GLUE_NO_CONTEXT", false),
|
|
31
|
+
readFullyThreshold: envNumber("PI_KICKY_GLUE_READ_THRESHOLD", 2),
|
|
32
|
+
};
|
|
33
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface OwnedStackPointer {
|
|
2
|
+
label: string;
|
|
3
|
+
path: string;
|
|
4
|
+
reason: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface PointerRule extends OwnedStackPointer {
|
|
8
|
+
terms: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const POINTER_RULES: PointerRule[] = [
|
|
12
|
+
{
|
|
13
|
+
label: "Michaelliv system map",
|
|
14
|
+
path: "~/personal/kicky-brain/kicky-brain/concepts/michaelliv-system-map.md",
|
|
15
|
+
reason: "agent OS pattern language, Pi glue candidates, and do/don't-copy list",
|
|
16
|
+
terms: ["michaelliv", "miclivs", "dotpi", "dotskills", "agent glue", "pi glue"],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: "Michaelliv framework",
|
|
20
|
+
path: "~/personal/kicky-brain/kicky-brain/concepts/michaelliv-framework.md",
|
|
21
|
+
reason: "why behind Code Mode, Dripline, Runline, and owned runtimes",
|
|
22
|
+
terms: ["runline", "dripline", "dripyard", "code mode", "query mode", "michaelliv"],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
label: "Verified personal stack",
|
|
26
|
+
path: "~/personal/kicky-brain/kicky-brain/context/personal-stack.md",
|
|
27
|
+
reason: "canonical list of what Kicky actually uses vs drop-list",
|
|
28
|
+
terms: ["runline", "dripline", "dripyard", "dokku", "tailscale", "fnox", "kata", "omarchy", "stack"],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: "Pi documentation",
|
|
32
|
+
path: "/home/kicky/.cache/.bun/install/global/node_modules/@earendil-works/pi-coding-agent/README.md and docs/",
|
|
33
|
+
reason: "current Pi extension/package/skill API reference",
|
|
34
|
+
terms: ["pi", "extension", "pi package", "pi-package", "harness"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: "Pi-over-Claude decision",
|
|
38
|
+
path: "~/personal/kicky-brain/kicky-brain/decisions/2026-07-06-pi-harness-over-claude-code.md",
|
|
39
|
+
reason: "Claude Code artifacts are source material only; Pi is the harness",
|
|
40
|
+
terms: ["claude", "claude code", "harness", "pi glue"],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "Omarchy operating guide",
|
|
44
|
+
path: "~/dev/kicky-brain/infra/omarchy-agent-operating-guide.md",
|
|
45
|
+
reason: "desktop/OS workflow for this Omarchy machine",
|
|
46
|
+
terms: ["omarchy", "hyprland", "waybar"],
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function includesTerm(text: string, term: string): boolean {
|
|
51
|
+
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
return new RegExp(`(^|[^a-z0-9-])${escaped}([^a-z0-9-]|$)`, "i").test(text);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getOwnedStackPointers(
|
|
56
|
+
prompt: string,
|
|
57
|
+
options: { maxPointers?: number } = {},
|
|
58
|
+
): OwnedStackPointer[] {
|
|
59
|
+
const maxPointers = options.maxPointers ?? 6;
|
|
60
|
+
const normalized = prompt.toLowerCase();
|
|
61
|
+
const pointers: OwnedStackPointer[] = [];
|
|
62
|
+
|
|
63
|
+
for (const rule of POINTER_RULES) {
|
|
64
|
+
if (rule.terms.some((term) => includesTerm(normalized, term.toLowerCase()))) {
|
|
65
|
+
pointers.push({ label: rule.label, path: rule.path, reason: rule.reason });
|
|
66
|
+
}
|
|
67
|
+
if (pointers.length >= maxPointers) break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return pointers;
|
|
71
|
+
}
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export interface GlueStateOptions {
|
|
2
|
+
readFullyThreshold: number;
|
|
3
|
+
codeExtensions: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface GlueStatus {
|
|
7
|
+
pendingEditedCodePaths: string[];
|
|
8
|
+
consecutiveSearches: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GlueState {
|
|
12
|
+
recordSearch(toolName: string, input?: unknown): boolean;
|
|
13
|
+
recordNonSearch(): void;
|
|
14
|
+
recordEdit(toolName: string, input?: unknown): string[];
|
|
15
|
+
drainEditedCodePaths(): string[];
|
|
16
|
+
status(): GlueStatus;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_CODE_EXTENSIONS = [
|
|
20
|
+
".ts",
|
|
21
|
+
".tsx",
|
|
22
|
+
".js",
|
|
23
|
+
".jsx",
|
|
24
|
+
".mjs",
|
|
25
|
+
".cjs",
|
|
26
|
+
".py",
|
|
27
|
+
".rs",
|
|
28
|
+
".go",
|
|
29
|
+
".rb",
|
|
30
|
+
".java",
|
|
31
|
+
".kt",
|
|
32
|
+
".swift",
|
|
33
|
+
".c",
|
|
34
|
+
".cpp",
|
|
35
|
+
".h",
|
|
36
|
+
".hpp",
|
|
37
|
+
".cs",
|
|
38
|
+
".lua",
|
|
39
|
+
".zig",
|
|
40
|
+
".sh",
|
|
41
|
+
".bash",
|
|
42
|
+
".zsh",
|
|
43
|
+
".vue",
|
|
44
|
+
".svelte",
|
|
45
|
+
".astro",
|
|
46
|
+
".css",
|
|
47
|
+
".scss",
|
|
48
|
+
".html",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
const SEARCH_COMMAND_PATTERN = /\b(rg|grep|egrep|fgrep|zgrep|bzgrep|xzgrep|ag|ack|pt|sift|ucg)\b/;
|
|
52
|
+
const SEARCH_TOOL_NAMES = new Set([
|
|
53
|
+
"grep",
|
|
54
|
+
"find",
|
|
55
|
+
"ls",
|
|
56
|
+
"ast_grep_search",
|
|
57
|
+
"lsp_navigation",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
function getStringPath(input: unknown): string | undefined {
|
|
61
|
+
if (!input || typeof input !== "object") return undefined;
|
|
62
|
+
const maybePath = (input as { path?: unknown }).path;
|
|
63
|
+
return typeof maybePath === "string" ? maybePath : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getCommand(input: unknown): string {
|
|
67
|
+
if (!input || typeof input !== "object") return "";
|
|
68
|
+
const maybeCommand = (input as { command?: unknown }).command;
|
|
69
|
+
return typeof maybeCommand === "string" ? maybeCommand : "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isCodePath(path: string, extensions: readonly string[]): boolean {
|
|
73
|
+
return extensions.some((extension) => path.endsWith(extension));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isSearch(toolName: string, input: unknown): boolean {
|
|
77
|
+
if (toolName === "bash") return SEARCH_COMMAND_PATTERN.test(getCommand(input));
|
|
78
|
+
return SEARCH_TOOL_NAMES.has(toolName);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createGlueState(options: Partial<GlueStateOptions> = {}): GlueState {
|
|
82
|
+
const readFullyThreshold = options.readFullyThreshold ?? 2;
|
|
83
|
+
const codeExtensions = options.codeExtensions ?? DEFAULT_CODE_EXTENSIONS;
|
|
84
|
+
let consecutiveSearches = 0;
|
|
85
|
+
const pendingEditedCodePaths = new Set<string>();
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
recordSearch(toolName: string, input?: unknown): boolean {
|
|
89
|
+
if (!isSearch(toolName, input)) {
|
|
90
|
+
consecutiveSearches = 0;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
consecutiveSearches += 1;
|
|
95
|
+
if (consecutiveSearches >= readFullyThreshold) {
|
|
96
|
+
consecutiveSearches = 0;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
recordNonSearch(): void {
|
|
103
|
+
consecutiveSearches = 0;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
recordEdit(toolName: string, input?: unknown): string[] {
|
|
107
|
+
if (toolName !== "edit" && toolName !== "write") return [];
|
|
108
|
+
const path = getStringPath(input);
|
|
109
|
+
if (!path || !isCodePath(path, codeExtensions)) return [];
|
|
110
|
+
pendingEditedCodePaths.add(path);
|
|
111
|
+
return [path];
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
drainEditedCodePaths(): string[] {
|
|
115
|
+
const paths = [...pendingEditedCodePaths].sort();
|
|
116
|
+
pendingEditedCodePaths.clear();
|
|
117
|
+
return paths;
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
status(): GlueStatus {
|
|
121
|
+
return {
|
|
122
|
+
pendingEditedCodePaths: [...pendingEditedCodePaths].sort(),
|
|
123
|
+
consecutiveSearches,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|