@zalom/plastic 1.0.0-alpha.1
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/PLASTIC.md +534 -0
- package/README.md +88 -0
- package/agents/future-intent-researcher.md +38 -0
- package/agents/intent-curator.md +40 -0
- package/bin/install.js +29 -0
- package/deprecations.yml +23 -0
- package/hooks/check-update +42 -0
- package/hooks/continue +31 -0
- package/hooks/future-intent-check +25 -0
- package/hooks/gate-check +10 -0
- package/hooks/hooks.json +78 -0
- package/hooks/run-hook +7 -0
- package/hooks/savepoint +6 -0
- package/hooks/session-start +9 -0
- package/hooks/statusline +16 -0
- package/package.json +43 -0
- package/scripts/folgezettel-id +40 -0
- package/scripts/hash-intent +29 -0
- package/scripts/hook-continue +130 -0
- package/scripts/hook-future-intent-check +90 -0
- package/scripts/hook-gate-check +136 -0
- package/scripts/hook-session-start +224 -0
- package/scripts/install.rb +474 -0
- package/scripts/lib/bridge.rb +139 -0
- package/scripts/migrate-folgezettel +535 -0
- package/scripts/migrate-to-global +96 -0
- package/scripts/read-config +129 -0
- package/skills/auto/SKILL.md +127 -0
- package/skills/brainstorming-grill-me/SKILL.md +105 -0
- package/skills/continuing/SKILL.md +104 -0
- package/skills/creating-intent/SKILL.md +122 -0
- package/skills/creating-project/SKILL.md +166 -0
- package/skills/executing-plan/SKILL.md +120 -0
- package/skills/executing-plan/code-quality-reviewer-prompt.md +32 -0
- package/skills/executing-plan/implementer-prompt.md +42 -0
- package/skills/executing-plan/spec-reviewer-prompt.md +27 -0
- package/skills/install/SKILL.md +134 -0
- package/skills/intent-curator/SKILL.md +41 -0
- package/skills/linking-intents/SKILL.md +72 -0
- package/skills/managing-index/SKILL.md +66 -0
- package/skills/managing-index/references/zettelkasten-linking.md +27 -0
- package/skills/releasing/SKILL.md +124 -0
- package/skills/savepoint/SKILL.md +57 -0
- package/skills/uninstall/SKILL.md +48 -0
- package/skills/update/SKILL.md +69 -0
- package/templates/agents.md +46 -0
- package/templates/checklist.md +11 -0
- package/templates/config.yml +13 -0
- package/templates/index.md +13 -0
- package/templates/intent.md +24 -0
- package/templates/plan.md +11 -0
- package/templates/projects.yml +3 -0
- package/templates/savepoint.md +13 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plastic:intent-curator
|
|
3
|
+
description: |
|
|
4
|
+
Use this agent when completing or reviewing intents, reorganizing the index,
|
|
5
|
+
or when the intent store needs maintenance. Examples:
|
|
6
|
+
<example>Context: User has finished implementing a feature.
|
|
7
|
+
user: "This intent is done, clean up the index"
|
|
8
|
+
assistant: "I'll use the intent-curator agent to update the intent status and reorganize INDEX.md"
|
|
9
|
+
<commentary>Intent lifecycle change triggers curator for index maintenance.</commentary></example>
|
|
10
|
+
<example>Context: The intent store has grown and clusters need review.
|
|
11
|
+
user: "Organize the intents"
|
|
12
|
+
assistant: "I'll use the intent-curator to review clusters, flag orphans, and suggest connections"
|
|
13
|
+
<commentary>Periodic maintenance of the Zettelkasten structure.</commentary></example>
|
|
14
|
+
model: inherit
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
You are the Plastic Intent Curator. Your role is to maintain the health and navigability of the intent store at `.plastic/`.
|
|
18
|
+
|
|
19
|
+
## Your Responsibilities
|
|
20
|
+
|
|
21
|
+
1. **Intent lifecycle management** — move intents between Active/Future/Completed in INDEX.md, fill in `## Outcome` sections
|
|
22
|
+
2. **INDEX.md maintenance** — keep Active/Future/Clusters/Completed sections accurate and well-organized
|
|
23
|
+
3. **Link discovery** — suggest connections between intents that share topics but aren't linked
|
|
24
|
+
4. **Cluster management** — create new clusters when 3+ unlinked intents share tags, merge or rename clusters as topics evolve
|
|
25
|
+
5. **Orphan detection** — flag intents with no links and no cluster membership
|
|
26
|
+
|
|
27
|
+
## How You Work
|
|
28
|
+
|
|
29
|
+
1. Scan `~/.plastic/store/*/ID--slug.md` (or project store) to understand the full intent landscape
|
|
30
|
+
2. Read `~/.plastic/INDEX.md` (or project INDEX.md) to understand current organization
|
|
31
|
+
3. Compare: are there intents not in any cluster? Missing from Active/Completed? Status mismatches?
|
|
32
|
+
4. Make targeted edits to INDEX.md and intent frontmatter/links
|
|
33
|
+
5. Report what you changed
|
|
34
|
+
|
|
35
|
+
## Constraints
|
|
36
|
+
|
|
37
|
+
- You only edit `~/.plastic/INDEX.md` (or project INDEX.md) and `~/.plastic/store/*/ID--slug.md` (or project store) files
|
|
38
|
+
- You never create new intents — that's the creating-intent skill's job
|
|
39
|
+
- You never modify `## Insights`, `## Context`, or `## Outcome` content sections — those belong to the worker
|
|
40
|
+
- You use Read and grep/find for discovery, Edit for targeted changes
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Thin shim — npx entry point that delegates to the Ruby installer.
|
|
4
|
+
// All logic lives in scripts/install.rb. JS is only the distribution mechanism.
|
|
5
|
+
|
|
6
|
+
import { execFileSync } from 'node:child_process'
|
|
7
|
+
import { resolve } from 'node:path'
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
9
|
+
|
|
10
|
+
const packageRoot = new URL('..', import.meta.url).pathname
|
|
11
|
+
const installer = resolve(packageRoot, 'scripts', 'install.rb')
|
|
12
|
+
|
|
13
|
+
if (!existsSync(installer)) {
|
|
14
|
+
console.error('Error: scripts/install.rb not found in package.')
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
execFileSync('ruby', [installer, ...process.argv.slice(2)], {
|
|
20
|
+
stdio: 'inherit',
|
|
21
|
+
env: { ...process.env, PLASTIC_PACKAGE_ROOT: packageRoot },
|
|
22
|
+
})
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err.status) process.exit(err.status)
|
|
25
|
+
console.error('Error: Ruby is required to install Plastic.')
|
|
26
|
+
console.error(' macOS: Ruby is pre-installed')
|
|
27
|
+
console.error(' Linux: sudo apt install ruby / dnf install ruby')
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
package/deprecations.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Plastic deprecation notices
|
|
2
|
+
# Each entry is shown to the user during SessionStart.
|
|
3
|
+
# See PLASTIC.md "Deprecation Process" for the full convention.
|
|
4
|
+
#
|
|
5
|
+
# Schema:
|
|
6
|
+
# id: unique-slug (used for dismissal tracking)
|
|
7
|
+
# severity: info | warning | critical
|
|
8
|
+
# summary: one-line human description
|
|
9
|
+
# migration_steps: ordered list of what user must do
|
|
10
|
+
# introduced: semver where notice was added
|
|
11
|
+
# removal: semver where feature is removed
|
|
12
|
+
# link: optional URL to migration guide
|
|
13
|
+
|
|
14
|
+
deprecations:
|
|
15
|
+
- id: cc-plugin-to-npx
|
|
16
|
+
severity: info
|
|
17
|
+
summary: "Plastic distribution is moving from Claude Code plugin to npx install"
|
|
18
|
+
migration_steps:
|
|
19
|
+
- "Install new distribution: npx @zalom/plastic@latest"
|
|
20
|
+
- "Verify: check that ~/.plastic/ is intact and hooks work"
|
|
21
|
+
- "Remove old plugin: /plugin remove plastic"
|
|
22
|
+
introduced: "1.0.0"
|
|
23
|
+
removal: "2.0.0"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# plastic-hook-version: 1.0.0
|
|
3
|
+
# Runs on SessionStart — checks npm for newer version, writes cache
|
|
4
|
+
|
|
5
|
+
PLASTIC_HOME="$HOME/.plastic"
|
|
6
|
+
CACHE_DIR="$PLASTIC_HOME/.cache"
|
|
7
|
+
CACHE_FILE="$CACHE_DIR/update-check.json"
|
|
8
|
+
VERSION_FILE="$PLASTIC_HOME/VERSION"
|
|
9
|
+
|
|
10
|
+
mkdir -p "$CACHE_DIR"
|
|
11
|
+
|
|
12
|
+
if [ ! -f "$VERSION_FILE" ]; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
CURRENT=$(cat "$VERSION_FILE" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$')
|
|
17
|
+
|
|
18
|
+
if [ -z "$CURRENT" ]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Background check — don't block session start
|
|
23
|
+
(
|
|
24
|
+
RAW=$(npm view @zalom/plastic version 2>/dev/null)
|
|
25
|
+
LATEST=$(echo "$RAW" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$')
|
|
26
|
+
|
|
27
|
+
if [ -z "$LATEST" ]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
CHECKED=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
32
|
+
|
|
33
|
+
if [ "$LATEST" != "$CURRENT" ]; then
|
|
34
|
+
printf '{"current":"%s","latest":"%s","checked":"%s","updateAvailable":true}\n' \
|
|
35
|
+
"$CURRENT" "$LATEST" "$CHECKED" > "$CACHE_FILE"
|
|
36
|
+
else
|
|
37
|
+
printf '{"current":"%s","latest":"%s","checked":"%s","updateAvailable":false}\n' \
|
|
38
|
+
"$CURRENT" "$LATEST" "$CHECKED" > "$CACHE_FILE"
|
|
39
|
+
fi
|
|
40
|
+
) &
|
|
41
|
+
|
|
42
|
+
exit 0
|
package/hooks/continue
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
INPUT=$(cat)
|
|
3
|
+
MESSAGE=$(echo "$INPUT" | ruby -rjson -e 'puts JSON.parse(STDIN.read)["user_prompt"]' 2>/dev/null || echo "")
|
|
4
|
+
|
|
5
|
+
if echo "$MESSAGE" | grep -iq '\bcontinue\b'; then
|
|
6
|
+
GLOBAL_INDEX="$HOME/.plastic/INDEX.md"
|
|
7
|
+
|
|
8
|
+
if [ ! -f "$GLOBAL_INDEX" ]; then
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
INDEX="$GLOBAL_INDEX"
|
|
13
|
+
STORE_ROOT="$HOME/.plastic"
|
|
14
|
+
MODE="global"
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
+
OUTPUT=$(ruby "$SCRIPT_DIR/../scripts/hook-continue" "$INDEX" "$STORE_ROOT" "$MODE" 2>/dev/null)
|
|
18
|
+
|
|
19
|
+
if [ -n "$OUTPUT" ]; then
|
|
20
|
+
echo "$OUTPUT"
|
|
21
|
+
else
|
|
22
|
+
cat <<'HOOKJSON'
|
|
23
|
+
{
|
|
24
|
+
"hookSpecificOutput": {
|
|
25
|
+
"hookEventName": "UserPromptSubmit",
|
|
26
|
+
"additionalContext": "PLASTIC CONTINUE — The user wants to resume previous work.\n\nRead ~/.plastic/INDEX.md and follow the continuing skill workflow."
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
HOOKJSON
|
|
30
|
+
fi
|
|
31
|
+
fi
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
INPUT=$(cat)
|
|
3
|
+
MESSAGE=$(echo "$INPUT" | ruby -rjson -e 'puts JSON.parse(STDIN.read)["user_prompt"].downcase' 2>/dev/null || echo "")
|
|
4
|
+
|
|
5
|
+
if [ ${#MESSAGE} -lt 10 ]; then
|
|
6
|
+
exit 0
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
if echo "$MESSAGE" | grep -qx '\s*continue\s*'; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
GLOBAL_INDEX="$HOME/.plastic/INDEX.md"
|
|
14
|
+
LOCAL_INDEX=".plastic/INDEX.md"
|
|
15
|
+
|
|
16
|
+
if [ -f "$GLOBAL_INDEX" ]; then
|
|
17
|
+
STORE_ROOT="$HOME/.plastic"
|
|
18
|
+
elif [ -f "$LOCAL_INDEX" ]; then
|
|
19
|
+
STORE_ROOT=".plastic"
|
|
20
|
+
else
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
25
|
+
ruby "$SCRIPT_DIR/../scripts/hook-future-intent-check" "$STORE_ROOT" "$MESSAGE"
|
package/hooks/gate-check
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
INPUT=$(cat)
|
|
3
|
+
FILE_PATH=$(echo "$INPUT" | ruby -rjson -e 'data = JSON.parse(STDIN.read); puts data.dig("tool_params", "file_path") || data.dig("tool_input", "file_path") || ""' 2>/dev/null)
|
|
4
|
+
|
|
5
|
+
if [ -z "$FILE_PATH" ]; then
|
|
6
|
+
exit 0
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
|
+
ruby "$SCRIPT_DIR/../scripts/hook-gate-check" "$FILE_PATH"
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" session-start",
|
|
10
|
+
"statusMessage": "Loading Plastic context..."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/check-update\"",
|
|
15
|
+
"statusMessage": ""
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"PreCompact": [
|
|
21
|
+
{
|
|
22
|
+
"matcher": "manual|auto",
|
|
23
|
+
"hooks": [
|
|
24
|
+
{
|
|
25
|
+
"type": "command",
|
|
26
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" savepoint",
|
|
27
|
+
"statusMessage": "Saving Plastic intent state..."
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"PostToolUse": [
|
|
33
|
+
{
|
|
34
|
+
"matcher": "Write|Edit",
|
|
35
|
+
"hooks": [
|
|
36
|
+
{
|
|
37
|
+
"type": "command",
|
|
38
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" gate-check",
|
|
39
|
+
"statusMessage": "Checking lifecycle gates..."
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"UserPromptSubmit": [
|
|
45
|
+
{
|
|
46
|
+
"matcher": "",
|
|
47
|
+
"hooks": [
|
|
48
|
+
{
|
|
49
|
+
"type": "command",
|
|
50
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" continue",
|
|
51
|
+
"statusMessage": "Checking for continue..."
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"matcher": "",
|
|
57
|
+
"hooks": [
|
|
58
|
+
{
|
|
59
|
+
"type": "command",
|
|
60
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/future-intent-check\"",
|
|
61
|
+
"statusMessage": "Checking future intents..."
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"statusLine": [
|
|
67
|
+
{
|
|
68
|
+
"matcher": "",
|
|
69
|
+
"hooks": [
|
|
70
|
+
{
|
|
71
|
+
"type": "command",
|
|
72
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/statusline\""
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
package/hooks/run-hook
ADDED
package/hooks/savepoint
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
cat <<'HOOKJSON'
|
|
3
|
+
{
|
|
4
|
+
"systemMessage": "PLASTIC SAVEPOINT — Context is being compacted.\n\nYou MUST perform the savepoint procedure NOW:\n1. Read .plastic/INDEX.md to find active intent(s)\n2. For each active intent in .plastic/store/ID--slug/:\n a. Update checklist.md (check off completed items)\n b. Create/update savepoint.md (in-progress work, next steps, blockers, discoveries)\n c. Update intent.md Build/Observe sections with session work\n3. Update .plastic/INDEX.md active section\n4. Commit: git add .plastic/ && git commit -m 'chore: savepoint — [intent name]'\n5. Tell the user: 'Context is getting large. I have saved progress to the active intent. Please run /clear and say continue to resume.'\n\nDO NOT skip this. The user's work depends on it."
|
|
5
|
+
}
|
|
6
|
+
HOOKJSON
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
GLOBAL_INDEX="$HOME/.plastic/INDEX.md"
|
|
3
|
+
|
|
4
|
+
if [ ! -f "$GLOBAL_INDEX" ]; then
|
|
5
|
+
exit 0
|
|
6
|
+
fi
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
ruby "$SCRIPT_DIR/../scripts/hook-session-start" "$GLOBAL_INDEX" "$HOME/.plastic" "global" "${CLAUDE_PLUGIN_ROOT:-}"
|
package/hooks/statusline
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# plastic-hook-version: 1.0.0
|
|
3
|
+
# StatusLine hook — shows update available warning
|
|
4
|
+
|
|
5
|
+
CACHE_FILE="$HOME/.plastic/.cache/update-check.json"
|
|
6
|
+
|
|
7
|
+
if [ ! -f "$CACHE_FILE" ]; then
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
UPDATE_AVAILABLE=$(grep -o '"updateAvailable":true' "$CACHE_FILE" 2>/dev/null)
|
|
12
|
+
if [ -n "$UPDATE_AVAILABLE" ]; then
|
|
13
|
+
LATEST=$(grep -o '"latest":"[^"]*"' "$CACHE_FILE" | cut -d'"' -f4)
|
|
14
|
+
CURRENT=$(grep -o '"current":"[^"]*"' "$CACHE_FILE" | cut -d'"' -f4)
|
|
15
|
+
echo "Plastic update: $CURRENT → $LATEST — run /plastic:update"
|
|
16
|
+
fi
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zalom/plastic",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"description": "Intent-driven idea development system for AI coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"plastic": "bin/install.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18.0.0"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"intent-driven",
|
|
14
|
+
"idea-development",
|
|
15
|
+
"zettelkasten",
|
|
16
|
+
"ai-agent",
|
|
17
|
+
"multi-agent",
|
|
18
|
+
"neuroplasticity",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"codex-cli",
|
|
21
|
+
"hermes"
|
|
22
|
+
],
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "Zlatko Alomerovic",
|
|
25
|
+
"email": "zlatko.alomerovic@gmail.com"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/zalom/plastic"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/zalom/plastic",
|
|
33
|
+
"files": [
|
|
34
|
+
"bin/",
|
|
35
|
+
"scripts/",
|
|
36
|
+
"skills/",
|
|
37
|
+
"hooks/",
|
|
38
|
+
"agents/",
|
|
39
|
+
"templates/",
|
|
40
|
+
"PLASTIC.md",
|
|
41
|
+
"deprecations.yml"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# Generate the next available Folgezettel ID for a new intent.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# folgezettel-id <store_path> # next root ID
|
|
7
|
+
# folgezettel-id <store_path> <parent_id> # next child of parent
|
|
8
|
+
|
|
9
|
+
store_path = ARGV[0]
|
|
10
|
+
parent_id = ARGV[1] # nil for root
|
|
11
|
+
|
|
12
|
+
unless store_path && Dir.exist?(store_path)
|
|
13
|
+
$stderr.puts "Usage: folgezettel-id <store_path> [parent_id]"
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
existing_dirs = Dir.glob("#{store_path}/*--*").map { |d| File.basename(d).split("--").first }
|
|
18
|
+
|
|
19
|
+
if parent_id.nil? || parent_id.empty?
|
|
20
|
+
roots = existing_dirs.select { |id| id.match?(/\A\d+\z/) }.map(&:to_i)
|
|
21
|
+
next_root = roots.empty? ? 1 : roots.max + 1
|
|
22
|
+
puts next_root.to_s
|
|
23
|
+
else
|
|
24
|
+
depth = parent_id.length
|
|
25
|
+
use_letter = parent_id[-1].match?(/\d/)
|
|
26
|
+
|
|
27
|
+
children = existing_dirs.select do |id|
|
|
28
|
+
id.start_with?(parent_id) && id.length == depth + 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
suffixes = children.map { |id| id[-1] }
|
|
32
|
+
|
|
33
|
+
if use_letter
|
|
34
|
+
last = suffixes.select { |s| s.match?(/[a-z]/) }.max || "`"
|
|
35
|
+
puts "#{parent_id}#{last.succ}"
|
|
36
|
+
else
|
|
37
|
+
last = suffixes.select { |s| s.match?(/\d/) }.map(&:to_i).max || 0
|
|
38
|
+
puts "#{parent_id}#{last + 1}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Generate a 6-char deterministic hash for an intent name.
|
|
3
|
+
# Auto-detects Ruby or Node.js. Exits with error if neither available.
|
|
4
|
+
#
|
|
5
|
+
# Usage: hash-intent "intent name words"
|
|
6
|
+
|
|
7
|
+
INTENT_NAME="$1"
|
|
8
|
+
|
|
9
|
+
if [ -z "$INTENT_NAME" ]; then
|
|
10
|
+
echo "Usage: hash-intent \"intent name\"" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# Try Ruby first (macOS/Linux default)
|
|
15
|
+
if command -v ruby &>/dev/null; then
|
|
16
|
+
ruby -r digest -e 'puts Digest::SHA256.hexdigest(ARGV[0]).to_i(16).to_s(36)[0,6]' "$INTENT_NAME"
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Fallback to Node.js (common on Windows)
|
|
21
|
+
if command -v node &>/dev/null; then
|
|
22
|
+
node -e "const c=require('crypto');console.log(BigInt('0x'+c.createHash('sha256').update(process.argv[1]).digest('hex')).toString(36).slice(0,6))" "$INTENT_NAME"
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
echo "Error: Neither ruby nor node found. Install one of them." >&2
|
|
27
|
+
echo " macOS/Linux: Ruby is usually pre-installed" >&2
|
|
28
|
+
echo " Windows: Install Node.js or use WSL" >&2
|
|
29
|
+
exit 1
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# Usage: hook-continue <index_path> <store_root> <mode>
|
|
4
|
+
# Outputs hook JSON for the continue hook. Exits silently if nothing to show.
|
|
5
|
+
|
|
6
|
+
require "json"
|
|
7
|
+
require "date"
|
|
8
|
+
require "yaml"
|
|
9
|
+
|
|
10
|
+
index_path, store_root, mode = ARGV
|
|
11
|
+
exit 0 unless index_path && store_root && mode
|
|
12
|
+
|
|
13
|
+
# --- Parse INDEX.md ---
|
|
14
|
+
|
|
15
|
+
lines = File.readlines(index_path)
|
|
16
|
+
active = []
|
|
17
|
+
future = []
|
|
18
|
+
section = nil
|
|
19
|
+
|
|
20
|
+
lines.each do |line|
|
|
21
|
+
if line.start_with?("## Active")
|
|
22
|
+
section = :active
|
|
23
|
+
next
|
|
24
|
+
elsif line.start_with?("## Future")
|
|
25
|
+
section = :future
|
|
26
|
+
next
|
|
27
|
+
elsif line.start_with?("## ")
|
|
28
|
+
section = nil
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
next unless section && line.strip.start_with?("- [")
|
|
33
|
+
|
|
34
|
+
if section == :active
|
|
35
|
+
active << line.strip
|
|
36
|
+
elsif section == :future
|
|
37
|
+
future << line.strip
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# --- Detect stale future intents ---
|
|
42
|
+
|
|
43
|
+
stale = []
|
|
44
|
+
stale_days = 3
|
|
45
|
+
config_path = "#{store_root}/config.yml"
|
|
46
|
+
if File.exist?(config_path)
|
|
47
|
+
config = YAML.safe_load(File.read(config_path)) rescue {}
|
|
48
|
+
stale_days = config["stale_threshold_days"] || 3
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
future.each do |f|
|
|
52
|
+
if f =~ /store\/([\w-]+)\//
|
|
53
|
+
dir_name = $1
|
|
54
|
+
intent_file = "#{store_root}/store/#{dir_name}/#{dir_name}.md"
|
|
55
|
+
next unless File.exist?(intent_file)
|
|
56
|
+
|
|
57
|
+
content = File.read(intent_file)
|
|
58
|
+
if content =~ /^created:\s*['"]?(\d{4}-\d{2}-\d{2})/
|
|
59
|
+
age = (Date.today - Date.parse($1)).to_i
|
|
60
|
+
if age >= stale_days
|
|
61
|
+
name = f[/\[([^\]]+)\]/, 1] || "unknown"
|
|
62
|
+
stale << { name: name, age: age, entry: f }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# --- Detect current project ---
|
|
69
|
+
|
|
70
|
+
current_project = nil
|
|
71
|
+
if mode == "global"
|
|
72
|
+
projects_path = "#{store_root}/projects.yml"
|
|
73
|
+
if File.exist?(projects_path)
|
|
74
|
+
projects = YAML.safe_load(File.read(projects_path)) rescue {}
|
|
75
|
+
cwd = Dir.pwd
|
|
76
|
+
(projects["projects"] || {}).each do |slug, info|
|
|
77
|
+
project_path = File.expand_path(info["path"])
|
|
78
|
+
if cwd.start_with?(project_path)
|
|
79
|
+
current_project = { "slug" => slug, "parent" => info["parent"], "path" => project_path }
|
|
80
|
+
break
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# --- Build context ---
|
|
87
|
+
|
|
88
|
+
parts = []
|
|
89
|
+
parts << "PLASTIC CONTINUE — The user wants to resume work.\n"
|
|
90
|
+
|
|
91
|
+
if mode == "global" && current_project
|
|
92
|
+
parts << "Project: #{current_project["slug"]} (#{current_project["path"]})\n"
|
|
93
|
+
parts << "Governing intent: #{current_project["parent"]}\n" if current_project["parent"]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if active.any?
|
|
97
|
+
parts << "## Active Intents (work on these first)\n"
|
|
98
|
+
active.each { |a| parts << a }
|
|
99
|
+
parts << ""
|
|
100
|
+
else
|
|
101
|
+
parts << "## No Active Intents\n"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if future.any?
|
|
105
|
+
parts << "## Future Intents (offer as next work)\n"
|
|
106
|
+
future.each { |f| parts << f }
|
|
107
|
+
parts << ""
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if stale.any?
|
|
111
|
+
parts << "## Stale Future Intents\n"
|
|
112
|
+
parts << "These future intents have not been actioned. Ask the user what to do with each:\n"
|
|
113
|
+
stale.each do |s|
|
|
114
|
+
parts << "- #{s[:name]} (#{s[:age]} days old) — options: activate, abandon, or defer to agent (implement / research / ideate)"
|
|
115
|
+
end
|
|
116
|
+
parts << ""
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
parts << "Follow the plastic:continuing skill workflow.\n"
|
|
120
|
+
parts << "1. If active intents exist: read their state and resume\n"
|
|
121
|
+
parts << "2. If no active intents: present future intents as options\n"
|
|
122
|
+
parts << "3. If stale intents exist: surface them and ask user to decide"
|
|
123
|
+
|
|
124
|
+
payload = {
|
|
125
|
+
"hookSpecificOutput" => {
|
|
126
|
+
"hookEventName" => "UserPromptSubmit",
|
|
127
|
+
"additionalContext" => parts.join("\n")
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
puts JSON.generate(payload)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# Usage: hook-future-intent-check <store_root> <message>
|
|
4
|
+
# Outputs hook JSON when the user's message matches future intent keywords.
|
|
5
|
+
# Exits silently if no matches.
|
|
6
|
+
|
|
7
|
+
require "json"
|
|
8
|
+
require "yaml"
|
|
9
|
+
|
|
10
|
+
store_root, message = ARGV
|
|
11
|
+
exit 0 unless store_root && message
|
|
12
|
+
|
|
13
|
+
index_path = "#{store_root}/INDEX.md"
|
|
14
|
+
exit 0 unless File.exist?(index_path)
|
|
15
|
+
|
|
16
|
+
# --- Find future intent dirs ---
|
|
17
|
+
|
|
18
|
+
lines = File.readlines(index_path)
|
|
19
|
+
future_dirs = []
|
|
20
|
+
section = nil
|
|
21
|
+
|
|
22
|
+
lines.each do |line|
|
|
23
|
+
section = :future if line.start_with?("## Future")
|
|
24
|
+
section = nil if line.start_with?("## ") && !line.start_with?("## Future")
|
|
25
|
+
next unless section == :future && line.strip.start_with?("- [")
|
|
26
|
+
|
|
27
|
+
if line =~ /store\/([\w-]+)\//
|
|
28
|
+
future_dirs << $1
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
exit 0 if future_dirs.empty?
|
|
33
|
+
|
|
34
|
+
# --- Match keywords against message ---
|
|
35
|
+
|
|
36
|
+
matches = []
|
|
37
|
+
message_words = message.split(/\W+/).reject { |w| w.length < 4 }
|
|
38
|
+
|
|
39
|
+
future_dirs.each do |dir|
|
|
40
|
+
intent_path = "#{store_root}/store/#{dir}/#{dir}.md"
|
|
41
|
+
next unless File.exist?(intent_path)
|
|
42
|
+
|
|
43
|
+
content = File.read(intent_path)
|
|
44
|
+
|
|
45
|
+
tags = []
|
|
46
|
+
if content =~ /^tags:\s*\[([^\]]+)\]/
|
|
47
|
+
tags = $1.split(",").map(&:strip).map(&:downcase)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
intent_name = ""
|
|
51
|
+
if content =~ /^intent:\s*["']?(.+?)["']?\s*$/
|
|
52
|
+
intent_name = $1.downcase
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
intent_id = ""
|
|
56
|
+
if content =~ /^id:\s*["']?([a-z0-9]+)["']?\s*$/
|
|
57
|
+
intent_id = $1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
keywords = tags + intent_name.split(/\W+/).reject { |w| w.length < 4 }
|
|
61
|
+
keywords = keywords.map(&:downcase).uniq
|
|
62
|
+
|
|
63
|
+
matched_keywords = keywords.select { |kw| message.include?(kw) }
|
|
64
|
+
name_matches = message_words.select { |w| intent_name.include?(w) }
|
|
65
|
+
matched_keywords = (matched_keywords + name_matches).uniq
|
|
66
|
+
|
|
67
|
+
if matched_keywords.length >= 1
|
|
68
|
+
index_line = lines.find { |l| l.include?(dir) }&.strip || "#{intent_id} — #{intent_name}"
|
|
69
|
+
matches << { "index_line" => index_line, "keywords" => matched_keywords }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
exit 0 if matches.empty?
|
|
74
|
+
|
|
75
|
+
# --- Build context ---
|
|
76
|
+
|
|
77
|
+
parts = []
|
|
78
|
+
parts << "PLASTIC — Future intents related to this message:\n"
|
|
79
|
+
matches.each do |m|
|
|
80
|
+
parts << "#{m["index_line"]} (matched: #{m["keywords"].join(", ")})"
|
|
81
|
+
end
|
|
82
|
+
parts << "\nConsider asking the user if they want to activate any of these, or note the connection."
|
|
83
|
+
|
|
84
|
+
payload = {
|
|
85
|
+
"hookSpecificOutput" => {
|
|
86
|
+
"hookEventName" => "UserPromptSubmit",
|
|
87
|
+
"additionalContext" => parts.join("\n")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
puts JSON.generate(payload)
|