@zalom/plastic 1.0.0-beta.20 → 1.0.0-beta.22
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/hooks/hooks.json +5 -0
- package/hooks/lock-gate +12 -0
- package/package.json +1 -1
- package/scripts/hook-lock-gate +37 -0
- package/scripts/lib/bridge.rb +85 -5
- package/scripts/lib/installer_core.rb +5 -0
- package/skills/creating-skills/SKILL.md +9 -0
- package/skills/creating-skills/evals/evals.json +43 -10
- package/skills/creating-skills/references/hooks.md +55 -0
- package/skills/intent-starting/SKILL.md +106 -0
- package/skills/intent-starting/evals/evals.json +117 -0
- package/skills/intent-starting/references/boarding-matrix.md +35 -0
package/hooks/hooks.json
CHANGED
|
@@ -37,6 +37,11 @@
|
|
|
37
37
|
"type": "command",
|
|
38
38
|
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" code-gate",
|
|
39
39
|
"statusMessage": "Checking lifecycle gate..."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" lock-gate",
|
|
44
|
+
"statusMessage": "Checking lock gate..."
|
|
40
45
|
}
|
|
41
46
|
]
|
|
42
47
|
},
|
package/hooks/lock-gate
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
SESSION_ID=$(echo "$INPUT" | ruby -rjson -e 'data = JSON.parse(STDIN.read); puts data.dig("session_id") || ""' 2>/dev/null)
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
ruby "$SCRIPT_DIR/../scripts/hook-lock-gate" "$FILE_PATH" "$SESSION_ID"
|
package/package.json
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
#
|
|
5
|
+
# Usage: hook-lock-gate <file_path> [session_id]
|
|
6
|
+
# Fail-CLOSED PreToolUse gate (intent 96). Blocks a mutating write to an active
|
|
7
|
+
# intent's lifecycle dir when THIS session holds no live lock. Project code is NOT
|
|
8
|
+
# gated here (D2). Emits the PreToolUse JSON deny contract at exit 0; reserves
|
|
9
|
+
# non-zero exit for hook-internal errors only. Matcher Write|Edit|NotebookEdit, so
|
|
10
|
+
# reads never reach here.
|
|
11
|
+
require "json"
|
|
12
|
+
require_relative "lib/bridge"
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
file_path = ARGV[0]
|
|
16
|
+
exit 0 unless file_path && !file_path.empty?
|
|
17
|
+
session = (ARGV[1] unless ARGV[1].to_s.empty?) || ENV["CLAUDE_CODE_SESSION_ID"]
|
|
18
|
+
|
|
19
|
+
bridge_data = Bridge.discover_bridge(session: session, cwd: Dir.pwd) # nil = no lock held
|
|
20
|
+
reason = Bridge.lock_gate_decision(bridge_data, file_path)
|
|
21
|
+
exit 0 unless reason
|
|
22
|
+
|
|
23
|
+
print JSON.generate(
|
|
24
|
+
"hookSpecificOutput" => {
|
|
25
|
+
"hookEventName" => "PreToolUse",
|
|
26
|
+
"permissionDecision" => "deny",
|
|
27
|
+
"permissionDecisionReason" => reason # "run /plastic-intent-starting to lock and begin"
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
exit 0
|
|
31
|
+
rescue => e
|
|
32
|
+
# Hook-internal failure only: do NOT impersonate a user-deny. Log and fail open
|
|
33
|
+
# here so a gate bug never bricks editing; the fail-CLOSED intent is about MISSING
|
|
34
|
+
# locks, not crashes.
|
|
35
|
+
$stderr.puts "plastic lock-gate error: #{e.message}"
|
|
36
|
+
exit 0
|
|
37
|
+
end
|
package/scripts/lib/bridge.rb
CHANGED
|
@@ -585,15 +585,19 @@ module Bridge
|
|
|
585
585
|
|
|
586
586
|
# --- Auto mode (intent 27) ---
|
|
587
587
|
|
|
588
|
-
#
|
|
589
|
-
#
|
|
590
|
-
|
|
588
|
+
# Shared arming spine (intent 96): resolve the session key, derive intent state,
|
|
589
|
+
# set the caller-controlled auto flag, acquire the delivery lock, provision the
|
|
590
|
+
# per-intent worktrees, persist, and purge terminal bridges. arm_auto (auto: true)
|
|
591
|
+
# and arm_guided (auto: false) are thin delegators so the lock-stamp + provision
|
|
592
|
+
# behaviour stays identical across both modes. Works even when no bridge exists
|
|
593
|
+
# yet (mid-session intent creation).
|
|
594
|
+
def self.arm(session, intent_id:, intent_dir:, store:, name:, auto:)
|
|
591
595
|
key = resolve_session(session, intent_id: intent_id, store: store)
|
|
592
596
|
if blank?(session) && blank?(ENV["CLAUDE_CODE_SESSION_ID"])
|
|
593
|
-
$stderr.puts "plastic: no session id available; arming
|
|
597
|
+
$stderr.puts "plastic: no session id available; arming with derived bridge key #{key}"
|
|
594
598
|
end
|
|
595
599
|
data = derive(key, intent_id: intent_id, intent_dir: intent_dir, store: store, name: name)
|
|
596
|
-
data["build"]["auto"] =
|
|
600
|
+
data["build"]["auto"] = auto
|
|
597
601
|
|
|
598
602
|
# Acquire the delivery lock: this armed bridge is now the single owner of the
|
|
599
603
|
# intent's delivery (intent 73c). Stamp owner + pid liveness fields.
|
|
@@ -617,6 +621,20 @@ module Bridge
|
|
|
617
621
|
purge_done_bridges(session: key)
|
|
618
622
|
data
|
|
619
623
|
end
|
|
624
|
+
private_class_method :arm
|
|
625
|
+
|
|
626
|
+
# Arm auto mode for a session+intent. Works even when no bridge exists yet
|
|
627
|
+
# (mid-session intent creation). Re-derives intent state, then sets build.auto.
|
|
628
|
+
def self.arm_auto(session, intent_id:, intent_dir:, store:, name:)
|
|
629
|
+
arm(session, intent_id: intent_id, intent_dir: intent_dir, store: store, name: name, auto: true)
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Acquire the delivery lock WITHOUT auto mode (intent 96 / Start guided branch).
|
|
633
|
+
# Mirrors arm_auto's lock-stamp + worktree provision but leaves build.auto = false.
|
|
634
|
+
# Same signature as arm_auto; disarm_auto (mode-agnostic) releases a guided lock.
|
|
635
|
+
def self.arm_guided(session, intent_id:, intent_dir:, store:, name:)
|
|
636
|
+
arm(session, intent_id: intent_id, intent_dir: intent_dir, store: store, name: name, auto: false)
|
|
637
|
+
end
|
|
620
638
|
|
|
621
639
|
# Disarm auto mode. No-op if no bridge exists for the session.
|
|
622
640
|
def self.disarm_auto(session)
|
|
@@ -673,6 +691,68 @@ module Bridge
|
|
|
673
691
|
"(blocked edit: #{file_abs})"
|
|
674
692
|
end
|
|
675
693
|
|
|
694
|
+
# --- Fail-closed lock gate (intent 96) -------------------------------------
|
|
695
|
+
|
|
696
|
+
# Returns a reason String to BLOCK, or nil to ALLOW. bridge_data is whatever
|
|
697
|
+
# discover_bridge(session:, cwd:) resolved for THIS session (own-session strict,
|
|
698
|
+
# or the lone headless/derived-key bridge), or nil = no lock held.
|
|
699
|
+
#
|
|
700
|
+
# BLOCK iff the target is an ACTIVE intent's lifecycle dir AND this session does
|
|
701
|
+
# NOT hold a LIVE lock FOR THAT SAME INTENT. ALLOW otherwise: a lock held for the
|
|
702
|
+
# target intent; a not-yet-active intent (creation, What); project code; anything
|
|
703
|
+
# that is not an active intent dir. The lock must match the TARGET intent: holding
|
|
704
|
+
# a live lock on intent A never green-lights a mutating write into intent B's dir.
|
|
705
|
+
# Project code is NOT gated here (D2) - it is isolated per-intent by worktree +
|
|
706
|
+
# branch, not by a write-time lock.
|
|
707
|
+
def self.lock_gate_decision(bridge_data, file_path)
|
|
708
|
+
return nil if blank?(file_path)
|
|
709
|
+
|
|
710
|
+
# Resolve the TARGET intent first so allow-by-lock is scoped to that intent.
|
|
711
|
+
target_dir = intent_dir_for(file_path)
|
|
712
|
+
return nil unless target_dir # not an intent dir (project code, scratch) -> ALLOW
|
|
713
|
+
id = intent_id_from_dir(target_dir) # "<id>" from "<id>--<slug>"
|
|
714
|
+
store = File.dirname(target_dir) # the store/ dir holding the intent
|
|
715
|
+
return nil unless id && intent_active?(id, store: store) # not-yet-active (creation/What) -> ALLOW
|
|
716
|
+
|
|
717
|
+
# ALLOW only when this session holds a live lock for THIS SAME intent. A live
|
|
718
|
+
# lock on a different intent does not authorize writing into this intent's dir.
|
|
719
|
+
return nil if holds_live_lock?(bridge_data) &&
|
|
720
|
+
bridge_data.is_a?(Hash) &&
|
|
721
|
+
bridge_data.dig("intent", "id").to_s == id.to_s
|
|
722
|
+
|
|
723
|
+
"run /plastic-intent-starting to lock and begin"
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
# A discovered bridge counts as a held lock only if its lock is stamped AND the
|
|
727
|
+
# owner process is live on THIS host. Staleness/identity lives HERE (gate read
|
|
728
|
+
# path), never in arm_auto's write path. On host-mismatch we cannot probe the
|
|
729
|
+
# pid, so fall back to "held" (a remote live owner should still block).
|
|
730
|
+
def self.holds_live_lock?(bridge_data)
|
|
731
|
+
return false unless bridge_data.is_a?(Hash)
|
|
732
|
+
lock = bridge_data["lock"] || {}
|
|
733
|
+
return false if blank?(lock["owner_session"])
|
|
734
|
+
pid = lock["pid"]
|
|
735
|
+
host = lock["host"]
|
|
736
|
+
this_host = (Socket.gethostname rescue nil)
|
|
737
|
+
if host && this_host && host == this_host && pid.is_a?(Integer)
|
|
738
|
+
begin
|
|
739
|
+
Process.kill(0, pid) # raises Errno::ESRCH if dead
|
|
740
|
+
return true
|
|
741
|
+
rescue Errno::ESRCH
|
|
742
|
+
return false # dead owner -> not a held lock -> gate blocks (re-lock)
|
|
743
|
+
rescue Errno::EPERM
|
|
744
|
+
return true # exists, not ours to signal -> live
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
true # different/unknown host: treat the stamped lock as held
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# "<id>" from a ".../store/<id>--<slug>" dir, else nil.
|
|
751
|
+
def self.intent_id_from_dir(dir)
|
|
752
|
+
base = File.basename(dir.to_s)
|
|
753
|
+
base.include?("--") ? base.split("--", 2).first : nil
|
|
754
|
+
end
|
|
755
|
+
|
|
676
756
|
# --- Worktree isolation gate (intent 73c2) ---
|
|
677
757
|
|
|
678
758
|
# Returns a reason String to BLOCK, or nil to ALLOW. Two independent rules,
|
|
@@ -208,6 +208,7 @@ class InstallerCore
|
|
|
208
208
|
"scripts/lib/qmd_hook.rb" => "scripts/lib/qmd_hook.rb",
|
|
209
209
|
"scripts/lib/power_tools.rb" => "scripts/lib/power_tools.rb",
|
|
210
210
|
"scripts/hook-code-gate" => "scripts/hook-code-gate",
|
|
211
|
+
"scripts/hook-lock-gate" => "scripts/hook-lock-gate",
|
|
211
212
|
"scripts/hook-bash-gate" => "scripts/hook-bash-gate",
|
|
212
213
|
"scripts/hook-retrieval-gate" => "scripts/hook-retrieval-gate",
|
|
213
214
|
"scripts/lib/retrieval_gate.rb" => "scripts/lib/retrieval_gate.rb",
|
|
@@ -562,6 +563,10 @@ class InstallerCore
|
|
|
562
563
|
"matcher" => "Write|Edit|NotebookEdit",
|
|
563
564
|
"hooks" => [
|
|
564
565
|
{ "type" => "command", "command" => "#{hook_dir}/plastic-code-gate", "statusMessage" => "Checking lifecycle gate..." },
|
|
566
|
+
# Fail-closed lock gate (intent 96): a 2nd ordered command INSIDE the
|
|
567
|
+
# code-gate group (NOT a new same-matcher group, which the merge loop
|
|
568
|
+
# below would collapse). Blocks no-lock writes to an active intent's dir.
|
|
569
|
+
{ "type" => "command", "command" => "#{hook_dir}/plastic-lock-gate", "statusMessage" => "Checking lock gate..." },
|
|
565
570
|
],
|
|
566
571
|
},
|
|
567
572
|
{
|
|
@@ -48,6 +48,15 @@ routes each authoring task to the reference that holds the depth.
|
|
|
48
48
|
| Deciding script versus prose, or writing a script | `references/scripts.md` |
|
|
49
49
|
| Building evals for a skill | `references/evals.md` |
|
|
50
50
|
|
|
51
|
+
## Shrink context, or let a skill self-improve
|
|
52
|
+
|
|
53
|
+
- When prompts or tool output blow the context budget, open `references/hooks.md` (E7) for
|
|
54
|
+
the global token levers: a PostToolUse hook that trims noisy tool output before it enters
|
|
55
|
+
context, and Programmatic Tool Calling that keeps looped tool results in code, not context.
|
|
56
|
+
- When a skill should learn from its own real runs, open `references/hooks.md` (E8) for the
|
|
57
|
+
propose-only Stop or SubagentStop loop (transcript to proposed edits to human approval to
|
|
58
|
+
git, effort-gated). The dedicated skill is the future `improving-skills` skill.
|
|
59
|
+
|
|
51
60
|
## Scaffolder and evals
|
|
52
61
|
|
|
53
62
|
- To start a new skill, agent, or hook from a born-slim file, run
|
|
@@ -6,70 +6,103 @@
|
|
|
6
6
|
"prompt": "I want to author a new Plastic skill, how should I structure it?",
|
|
7
7
|
"expected_output": "The skill should activate. This is a direct authoring request that names the domain. Guide the user through progressive disclosure: metadata around 100 tokens, a slim body under 5000 tokens and 500 lines that routes, and references on demand. Point to references/skills.md for frontmatter, description, and voice, and route load-level questions through references/progressive-disclosure.md first.",
|
|
8
8
|
"files": [],
|
|
9
|
-
"assertions": [
|
|
9
|
+
"assertions": [
|
|
10
|
+
"The skill activates (direct authoring request that names the domain).",
|
|
11
|
+
"The response routes to references rather than dumping deep how-to inline.",
|
|
12
|
+
"The response states the load-level budgets (around 100 token metadata, body under 5000 tokens and 500 lines)."
|
|
13
|
+
]
|
|
10
14
|
},
|
|
11
15
|
{
|
|
12
16
|
"id": 2,
|
|
13
17
|
"prompt": "The agent keeps ignoring my instructions and the file is huge, help me restructure it",
|
|
14
18
|
"expected_output": "The skill should activate. This is an indirect trigger that never names skills or progressive disclosure, but the symptoms (bloated file, agent missing or skipping steps) are exactly the over-budget body case the skill covers. Guide toward splitting the body into references, keeping the body a thin router, and binding each reference to an observable trigger condition.",
|
|
15
19
|
"files": [],
|
|
16
|
-
"assertions": [
|
|
20
|
+
"assertions": [
|
|
21
|
+
"The skill activates on the indirect trigger (no skill or progressive-disclosure keyword in the prompt).",
|
|
22
|
+
"The response guides toward splitting the body into references and keeping a thin router."
|
|
23
|
+
]
|
|
17
24
|
},
|
|
18
25
|
{
|
|
19
26
|
"id": 3,
|
|
20
27
|
"prompt": "Scaffold a new lifecycle hook for Plastic",
|
|
21
28
|
"expected_output": "The skill should activate. Authoring a lifecycle hook is in scope. Route to references/hooks.md and offer scripts/scaffold.rb to start the hook from a born-slim file.",
|
|
22
29
|
"files": [],
|
|
23
|
-
"assertions": [
|
|
30
|
+
"assertions": [
|
|
31
|
+
"The skill activates (authoring a lifecycle hook is in scope).",
|
|
32
|
+
"The response routes to references/hooks.md and offers scripts/scaffold.rb."
|
|
33
|
+
]
|
|
24
34
|
},
|
|
25
35
|
{
|
|
26
36
|
"id": 4,
|
|
27
37
|
"prompt": "Our prompts are bloated and we keep blowing the context budget",
|
|
28
38
|
"expected_output": "The skill should activate. This is an indirect trigger: the user describes bloated prompts and a context budget problem without naming skills. The skill restructures instructions into a thin router over deep references with hard load-level budgets, which is the fix for this symptom.",
|
|
29
39
|
"files": [],
|
|
30
|
-
"assertions": [
|
|
40
|
+
"assertions": [
|
|
41
|
+
"The skill activates on the indirect context-budget trigger (no skill keyword in the prompt).",
|
|
42
|
+
"The response treats the fix as a thin router over deep references with hard load-level budgets."
|
|
43
|
+
]
|
|
31
44
|
},
|
|
32
45
|
{
|
|
33
46
|
"id": 5,
|
|
34
47
|
"prompt": "Run the evals on my skill and check the pass rate",
|
|
35
48
|
"expected_output": "The skill should NOT trigger. Running evals and reading pass rates is the job of plastic-evaluating-skills, not authoring. Near-miss negative: shares the words 'skill' and 'evals' but the operation is grading an existing skill, not creating or revising one.",
|
|
36
49
|
"files": [],
|
|
37
|
-
"assertions": [
|
|
50
|
+
"assertions": [
|
|
51
|
+
"The skill does not activate (running and grading evals is plastic-evaluating-skills, not authoring)."
|
|
52
|
+
]
|
|
38
53
|
},
|
|
39
54
|
{
|
|
40
55
|
"id": 6,
|
|
41
56
|
"prompt": "Create a new intent for the dashboard work",
|
|
42
57
|
"expected_output": "The skill should NOT trigger. Creating an intent is plastic-creating-intent. Near-miss negative: shares the verb 'create' but the object is an intent, not a skill, agent, or hook.",
|
|
43
58
|
"files": [],
|
|
44
|
-
"assertions": [
|
|
59
|
+
"assertions": [
|
|
60
|
+
"The skill does not activate (creating an intent is plastic-creating-intent)."
|
|
61
|
+
]
|
|
45
62
|
},
|
|
46
63
|
{
|
|
47
64
|
"id": 7,
|
|
48
65
|
"prompt": "I keep losing every team fight because of a skill issue in this game, any tips?",
|
|
49
66
|
"expected_output": "The skill should NOT trigger. This is a gaming question with no relation to authoring instructions. Near-miss negative: shares the word 'skill' in an unrelated sense.",
|
|
50
67
|
"files": [],
|
|
51
|
-
"assertions": [
|
|
68
|
+
"assertions": [
|
|
69
|
+
"The skill does not activate (gaming question, unrelated sense of 'skill')."
|
|
70
|
+
]
|
|
52
71
|
},
|
|
53
72
|
{
|
|
54
73
|
"id": 8,
|
|
55
74
|
"prompt": "Write unit tests for my Ruby model that validates email addresses",
|
|
56
75
|
"expected_output": "The skill should NOT trigger. Writing test code is a Ruby testing task. Near-miss negative: shares 'write' and 'test' but has nothing to do with authoring a skill, agent, or hook.",
|
|
57
76
|
"files": [],
|
|
58
|
-
"assertions": [
|
|
77
|
+
"assertions": [
|
|
78
|
+
"The skill does not activate (writing test code is a Ruby testing task, not authoring a skill)."
|
|
79
|
+
]
|
|
59
80
|
},
|
|
60
81
|
{
|
|
61
82
|
"id": 9,
|
|
62
83
|
"prompt": "Author a slim SKILL.md for a PDF-extraction skill",
|
|
63
84
|
"expected_output": "Activation is assumed for this output-quality case. Correct output is a SKILL.md whose frontmatter description is third person, front-loads concrete trigger keywords, states WHEN to use rather than the workflow, and includes at least one indirect trigger. The body stays under 5000 tokens and under 500 lines and routes to references rather than carrying deep how-to. Each reference link names an observable trigger condition with no bare pointer like 'see references/'. References stay one level deep, and no string contains an em-dash or en-dash.",
|
|
64
85
|
"files": [],
|
|
65
|
-
"assertions": [
|
|
86
|
+
"assertions": [
|
|
87
|
+
"The frontmatter description is written in the third person (no 'you' or 'I').",
|
|
88
|
+
"The description front-loads concrete trigger keywords in its opening clause.",
|
|
89
|
+
"The description includes at least one indirect trigger (a symptom phrasing that never names PDF extraction or skills).",
|
|
90
|
+
"The body is under 5000 tokens and under 500 lines.",
|
|
91
|
+
"Every reference link is bound to an observable trigger condition (no bare pointer such as 'see references/').",
|
|
92
|
+
"References stay one level deep (no reference points to a deeper reference).",
|
|
93
|
+
"No string in the SKILL.md or its references contains an em-dash or en-dash."
|
|
94
|
+
]
|
|
66
95
|
},
|
|
67
96
|
{
|
|
68
97
|
"id": 10,
|
|
69
98
|
"prompt": "Make this subagent role file slim, the description is vague and the body is too long",
|
|
70
99
|
"expected_output": "The skill should activate. Authoring or revising a subagent or Agent role file is in scope, and the symptoms (vague description, over-long body) are the progressive-disclosure case. Route to references/agents.md, tighten the description to state WHEN, and move depth into references. Output should keep the body under budget and use no em-dashes.",
|
|
71
100
|
"files": [],
|
|
72
|
-
"assertions": [
|
|
101
|
+
"assertions": [
|
|
102
|
+
"The skill activates (revising a subagent or Agent role file is in scope).",
|
|
103
|
+
"The response routes to references/agents.md and tightens the description to state WHEN.",
|
|
104
|
+
"The proposed output keeps the body under budget and uses no em-dash or en-dash."
|
|
105
|
+
]
|
|
73
106
|
}
|
|
74
107
|
]
|
|
75
108
|
}
|
|
@@ -17,6 +17,8 @@ handler grows past a few lines. This file covers only hook authoring.
|
|
|
17
17
|
- No-op by default, opt-in (E4)
|
|
18
18
|
- Exit codes and output channels (E5)
|
|
19
19
|
- Verify the harness engaged (E6)
|
|
20
|
+
- Token levers: global hooks that shrink context (E7)
|
|
21
|
+
- Propose-only self-improving loop (E8)
|
|
20
22
|
- Authoring checklist
|
|
21
23
|
|
|
22
24
|
## When to reach for a hook (E1)
|
|
@@ -177,6 +179,59 @@ Verify by observation, not assumption: [E6]
|
|
|
177
179
|
- For a gate, attempt the action the gate should block and confirm it is refused. A gate that
|
|
178
180
|
never refuses in testing is a gate that is not engaged.
|
|
179
181
|
|
|
182
|
+
## Token levers: global hooks that shrink context (E7)
|
|
183
|
+
|
|
184
|
+
Progressive disclosure trims what a skill loads. Two GLOBAL hooks trim what tool traffic
|
|
185
|
+
costs at runtime, independent of any skill body. Both are token-reduction levers. Reach for
|
|
186
|
+
them when prompts or tool output blow the context budget. [E7]
|
|
187
|
+
|
|
188
|
+
- PostToolUse output preprocessing. Register a GLOBAL PostToolUse hook that trims noisy tool
|
|
189
|
+
output before it enters context: collapse repeated lines, cut a thousand-line log to its
|
|
190
|
+
head and tail, strip ANSI control codes, drop progress chatter. The model never sees the
|
|
191
|
+
noise, so it never pays tokens for it. Scope the hook with a matcher over the loud tools
|
|
192
|
+
(for example `Bash`), keep it lossless on signal (trim volume, never the line that carries
|
|
193
|
+
the answer), and make it no-op by default per E4. This is one GLOBAL hook, not a per-skill
|
|
194
|
+
or frontmatter hook. [E7]
|
|
195
|
+
- Programmatic Tool Calling. When a tool runs in a loop and only the final result matters,
|
|
196
|
+
keep the intermediate results in code and return just the answer, instead of letting every
|
|
197
|
+
call land in context. Reach for it when a skill drives a tool in a loop and the per-call
|
|
198
|
+
output is throwaway. Pointer only: this is a Claude Code runtime feature, not a hook you
|
|
199
|
+
author here. [E7]
|
|
200
|
+
|
|
201
|
+
The first lever trims output already produced; the second avoids producing the context at
|
|
202
|
+
all. Pair either with sub-agent isolation (see `agents.md`) when a whole noisy sub-task can
|
|
203
|
+
run off to the side and return only its conclusion. [E7]
|
|
204
|
+
|
|
205
|
+
Both levers cut the CONTEXT axis (the input the model reads). The model's own OUTPUT tokens
|
|
206
|
+
(what it writes) are a separate axis with no hook: cut them with terse instructions, tool
|
|
207
|
+
responses that offer a concise mode, and sub-agent offloading that returns a short summary
|
|
208
|
+
instead of the full trace. [E7]
|
|
209
|
+
|
|
210
|
+
## Propose-only self-improving loop (E8)
|
|
211
|
+
|
|
212
|
+
A skill can learn from its own real runs without ever editing itself unattended. Reach for
|
|
213
|
+
this shape when a skill should improve from what actually happened in its runs. [E8]
|
|
214
|
+
|
|
215
|
+
Wire a GLOBAL Stop or SubagentStop hook that, once an effort threshold is met, reads the
|
|
216
|
+
just-finished transcript, drafts proposed edits to the skill, and stops. A human reviews the
|
|
217
|
+
proposal, approves it, and the approved change lands in git. The hook never writes the skill
|
|
218
|
+
directly. [E8]
|
|
219
|
+
|
|
220
|
+
The guardrails that make this safe: [E8]
|
|
221
|
+
|
|
222
|
+
- Propose only. The hook emits a diff or a suggestion, never an applied edit. Approval is a
|
|
223
|
+
human step, so a bad proposal costs a review, not a regression.
|
|
224
|
+
- Effort-gated. Run the analysis only past a threshold (a long enough transcript, a real
|
|
225
|
+
failure observed), so cheap runs spend nothing.
|
|
226
|
+
- Git-landed. The approved edit goes through the normal commit path, so every
|
|
227
|
+
self-improvement is reviewable and revertible.
|
|
228
|
+
- Global, not scoped. This is one GLOBAL Stop or SubagentStop hook, not a per-skill
|
|
229
|
+
frontmatter hook.
|
|
230
|
+
|
|
231
|
+
Pointer only. The real self-improving skill is the future `improving-skills` skill; this
|
|
232
|
+
section records the safe shape so a skill author knows the loop exists and keeps it
|
|
233
|
+
propose-only. [E8]
|
|
234
|
+
|
|
180
235
|
## Authoring checklist
|
|
181
236
|
|
|
182
237
|
- [ ] Behavior must hold deterministically; a stronger skill description was tried first (E1).
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plastic-intent-starting
|
|
3
|
+
description: >-
|
|
4
|
+
Board a session onto an intent: take the lock FIRST, confirm savepoint state, ask auto or
|
|
5
|
+
guided ONCE, then resume at the latest delivered station and run the cycle to Done. Use on
|
|
6
|
+
`continuing --intent {id}`, when a new intent is registered and the user asks to work it,
|
|
7
|
+
or when the user picks an intent to work. Requires the intent in INDEX `## Active`.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Intent Starting — board a session onto an intent
|
|
11
|
+
|
|
12
|
+
Announce: "Boarding intent [ID] — [name]."
|
|
13
|
+
|
|
14
|
+
`plastic-intent-starting` is the Start procedure. It boards a session onto one intent: take
|
|
15
|
+
the lock FIRST, confirm the delivery state, ask **auto or guided ONCE**, board at the latest
|
|
16
|
+
delivered station, then run the cycle to Done. The What → Why → How → Exec stations are the
|
|
17
|
+
train track; Start boards the train, the ending procedure (~93) exits it.
|
|
18
|
+
|
|
19
|
+
## Precondition + trigger
|
|
20
|
+
|
|
21
|
+
Fires when the user picks an intent to work, when an agent is told to continue a SPECIFIC
|
|
22
|
+
intent, or on `continuing --intent {id}` (the `continuing` → `starting` router is 106's job;
|
|
23
|
+
this skill is invokable standalone now).
|
|
24
|
+
|
|
25
|
+
If the intent is **terminal** (Done / Abandoned in INDEX): report only. Take NO lock, run NO
|
|
26
|
+
resume, do NOT reopen it. Summarize the outcome and ask what is next. Stop here.
|
|
27
|
+
|
|
28
|
+
## Lock FIRST (the spine)
|
|
29
|
+
|
|
30
|
+
The lock is non-negotiable and comes before any mutating work. The ACTION-3 lock-gate
|
|
31
|
+
enforces it: without a held lock, mutating writes to this active intent's dir are denied with
|
|
32
|
+
"run /plastic-intent-starting to lock and begin".
|
|
33
|
+
|
|
34
|
+
1. **Ensure the intent is in INDEX `## Active`.** If it sits in `## Future`, activate it
|
|
35
|
+
(move it to `## Active`, auto-commit) before arming. Creation precedes activation, so a
|
|
36
|
+
brand-new What intent is activated here, then locked.
|
|
37
|
+
2. **Arm the bridge.** Which arm is chosen by the mode answer (below), but the lock itself is
|
|
38
|
+
taken first. Reuse the arm one-liner shape from `plastic-auto`:
|
|
39
|
+
```bash
|
|
40
|
+
# guided (lock only):
|
|
41
|
+
ruby -r ~/.plastic/scripts/lib/bridge -e \
|
|
42
|
+
'Bridge.arm_guided(ENV["CLAUDE_CODE_SESSION_ID"], intent_id: "<ID>", intent_dir: "<STORE>/<dir>", store: "<STORE>", name: "<name>")'
|
|
43
|
+
# auto (lock + auto), then hand to plastic-auto:
|
|
44
|
+
ruby -r ~/.plastic/scripts/lib/bridge -e \
|
|
45
|
+
'Bridge.arm_auto(ENV["CLAUDE_CODE_SESSION_ID"], intent_id: "<ID>", intent_dir: "<STORE>/<dir>", store: "<STORE>", name: "<name>")'
|
|
46
|
+
```
|
|
47
|
+
Replace `<ID>`, `<STORE>` (`~/.plastic/projects/<slug>/store` or `~/.plastic/store`),
|
|
48
|
+
`<dir>` (the `ID--slug` directory), and `<name>`.
|
|
49
|
+
|
|
50
|
+
**Session id resolution (verbatim from `plastic-auto`).** The first argument is the session
|
|
51
|
+
id the bridge is keyed by: pass the hook stdin `session_id` when you have it, otherwise
|
|
52
|
+
`ENV["CLAUDE_CODE_SESSION_ID"]`, otherwise `nil`. Both arms call `resolve_session`, which
|
|
53
|
+
picks the first non-empty of: the explicit id you pass → `CLAUDE_CODE_SESSION_ID` → a
|
|
54
|
+
deterministic derived key (a hash of the store and intent id). It never returns nil, so the
|
|
55
|
+
lock is taken even when every session env var is empty; arming prints a one-line stderr
|
|
56
|
+
notice when it falls through to the derived key.
|
|
57
|
+
|
|
58
|
+
Idempotent re-arm: arming again with the same owner just refreshes the lock (re-derives and
|
|
59
|
+
rewrites the bridge); it is not an error to re-board an intent this session already owns.
|
|
60
|
+
|
|
61
|
+
## Confirm delivery state
|
|
62
|
+
|
|
63
|
+
Read `savepoint.md` and classify from the **last line** alone, then verify ONLY that line's
|
|
64
|
+
artifact is real (sentinel-aware via `Bridge.stage_file_present?`). On drift (the last line
|
|
65
|
+
disagrees with files on disk), rebuild the ledger from disk and note the correction. Do not
|
|
66
|
+
inline the rebuild; the `plastic-savepoint` skill owns it:
|
|
67
|
+
```bash
|
|
68
|
+
ruby -r ~/.plastic/scripts/lib/bridge -e 'Bridge.rebuild_savepoint("<intent_dir>")'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Report + ask "auto or guided?" ONCE
|
|
72
|
+
|
|
73
|
+
Report: the intent, the station it lands at (the matrix below), what is delivered, the next
|
|
74
|
+
step. Then ask the user **"auto or guided?"** — exactly ONCE, whatever station it lands at.
|
|
75
|
+
Never re-ask at a later station.
|
|
76
|
+
|
|
77
|
+
- **guided** → `arm_guided` (lock only); continue step by step with the user through the
|
|
78
|
+
station's work below.
|
|
79
|
+
- **auto** → `arm_auto` (lock + auto), then hand off to `plastic-auto`. The auto branch's
|
|
80
|
+
only remaining job is the handoff; `plastic-auto` runs the cycle from here.
|
|
81
|
+
|
|
82
|
+
## Board at the latest delivered station
|
|
83
|
+
|
|
84
|
+
The station is derived from `savepoint.md` last line + real artifacts on disk. See
|
|
85
|
+
`references/boarding-matrix.md` for the full table (last line → latest delivered → boards at →
|
|
86
|
+
continue with) and the per-station notes. Summary of what "continue" means per station:
|
|
87
|
+
|
|
88
|
+
- **What** → do what What requires (106-expanded), then brainstorm → `spec.md`.
|
|
89
|
+
- **Why** → continue brainstorming → `spec.md`.
|
|
90
|
+
- **How** → continue `plan.md` + `actions/` + `checklist.md`.
|
|
91
|
+
- **Exec** → verify what has been delivered, then continue (or restart) the delivery /
|
|
92
|
+
research; tick the checklist.
|
|
93
|
+
- **ready to complete** (`Exec outcome.md created`) → exit at Done.
|
|
94
|
+
- **Done** → report only, ask what is next, never reopen.
|
|
95
|
+
|
|
96
|
+
## Disarm / release on done
|
|
97
|
+
|
|
98
|
+
When delivery finishes, disarm and release per the `plastic-auto` disarm/release prose (do
|
|
99
|
+
not duplicate it here). The guided branch releases the lock via `disarm_auto`, which is
|
|
100
|
+
mode-agnostic (it sets `auto = false` and calls `Worktree.release`), so it releases a guided
|
|
101
|
+
lock too. When the work ships through a release, the release path merges the branch before the
|
|
102
|
+
worktree is removed; the plain disarm remove is only for the no-release case.
|
|
103
|
+
|
|
104
|
+
## References
|
|
105
|
+
|
|
106
|
+
- `references/boarding-matrix.md` — the full boarding table and per-station behaviour.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "plastic-intent-starting",
|
|
3
|
+
"notes": "Intent 96. Scopes: description triggering (1-5) and behavior (6-10: lock-first spine, ask-mode-once, terminal report-only, mode->arm mapping, boarding matrix). The lock-first and gate behaviors are also proven by Ruby tests (test/bridge_guided_test.rb, test/lock_gate_test.rb, test/lock_gate_hook_test.rb).",
|
|
4
|
+
"evals": [
|
|
5
|
+
{
|
|
6
|
+
"id": 1,
|
|
7
|
+
"scope": "triggering",
|
|
8
|
+
"set": "train",
|
|
9
|
+
"prompt": "start work on intent 96",
|
|
10
|
+
"expected_output": "Activates plastic-intent-starting (board a session onto the named intent).",
|
|
11
|
+
"files": [],
|
|
12
|
+
"assertions": [
|
|
13
|
+
{ "type": "code", "check": "router CHOICE == plastic-intent-starting", "result": "expect-pass" }
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": 2,
|
|
18
|
+
"scope": "triggering",
|
|
19
|
+
"set": "train",
|
|
20
|
+
"prompt": "continuing --intent 96",
|
|
21
|
+
"expected_output": "Activates plastic-intent-starting (the --intent form boards a specific intent).",
|
|
22
|
+
"files": [],
|
|
23
|
+
"assertions": [
|
|
24
|
+
{ "type": "code", "check": "router CHOICE == plastic-intent-starting", "result": "expect-pass" }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": 3,
|
|
29
|
+
"scope": "triggering",
|
|
30
|
+
"set": "validation",
|
|
31
|
+
"prompt": "work this intent",
|
|
32
|
+
"expected_output": "Activates plastic-intent-starting when an intent is in context to be worked.",
|
|
33
|
+
"files": [],
|
|
34
|
+
"assertions": [
|
|
35
|
+
{ "type": "code", "check": "router CHOICE == plastic-intent-starting", "result": "expect-pass" }
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": 4,
|
|
40
|
+
"scope": "triggering",
|
|
41
|
+
"set": "validation",
|
|
42
|
+
"prompt": "continue",
|
|
43
|
+
"expected_output": "Does NOT activate plastic-intent-starting; bare 'continue' is plastic-continuing (no specific intent named).",
|
|
44
|
+
"files": [],
|
|
45
|
+
"assertions": [
|
|
46
|
+
{ "type": "code", "check": "router CHOICE != plastic-intent-starting", "result": "expect-pass" }
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": 5,
|
|
51
|
+
"scope": "triggering",
|
|
52
|
+
"set": "validation",
|
|
53
|
+
"prompt": "create a new intent for the uploader retry policy",
|
|
54
|
+
"expected_output": "Does NOT activate plastic-intent-starting; activates plastic-creating-intent.",
|
|
55
|
+
"files": [],
|
|
56
|
+
"assertions": [
|
|
57
|
+
{ "type": "code", "check": "router CHOICE != plastic-intent-starting", "result": "expect-pass" }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": 6,
|
|
62
|
+
"scope": "behavior",
|
|
63
|
+
"set": "train",
|
|
64
|
+
"prompt": "Active intent X exists at the Why station. Start work on it (guided).",
|
|
65
|
+
"expected_output": "Takes the lock FIRST via Bridge.arm_guided (auto stays false) before any mutating work, confirms savepoint, then boards at Why and continues brainstorming toward spec.md.",
|
|
66
|
+
"files": [],
|
|
67
|
+
"assertions": [
|
|
68
|
+
{ "type": "human", "check": "lock acquired before any mutating write; SKILL.md 'Lock FIRST' section precedes station work", "result": "expect-pass" },
|
|
69
|
+
{ "type": "code", "check": "Bridge.arm_guided stamps the lock with auto=false (test/bridge_guided_test.rb green); lock-gate denies a no-lock write to the active intent dir (test/lock_gate_hook_test.rb green)", "result": "pass" }
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": 7,
|
|
74
|
+
"scope": "behavior",
|
|
75
|
+
"set": "train",
|
|
76
|
+
"prompt": "Start work on an active intent without saying auto or guided.",
|
|
77
|
+
"expected_output": "Asks 'auto or guided?' exactly once after reporting the station; never re-asks at later stations.",
|
|
78
|
+
"files": [],
|
|
79
|
+
"assertions": [
|
|
80
|
+
{ "type": "human", "check": "SKILL.md asks the mode exactly once and states 'never re-asked at a later station'", "result": "expect-pass" }
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": 8,
|
|
85
|
+
"scope": "behavior",
|
|
86
|
+
"set": "train",
|
|
87
|
+
"prompt": "Start work on an intent that is Done in INDEX.",
|
|
88
|
+
"expected_output": "Reports only: no lock, no resume, no reopen. Summarizes the outcome and asks what is next.",
|
|
89
|
+
"files": [],
|
|
90
|
+
"assertions": [
|
|
91
|
+
{ "type": "human", "check": "terminal intent -> report only, no arm call", "result": "expect-pass" }
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": 9,
|
|
96
|
+
"scope": "behavior",
|
|
97
|
+
"set": "validation",
|
|
98
|
+
"prompt": "Start work and choose auto.",
|
|
99
|
+
"expected_output": "guided -> Bridge.arm_guided; auto -> Bridge.arm_auto then hand off to plastic-auto (auto branch's only remaining job is the handoff).",
|
|
100
|
+
"files": [],
|
|
101
|
+
"assertions": [
|
|
102
|
+
{ "type": "human", "check": "SKILL.md maps guided->arm_guided and auto->arm_auto+handoff to plastic-auto", "result": "expect-pass" }
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": 10,
|
|
107
|
+
"scope": "behavior",
|
|
108
|
+
"set": "validation",
|
|
109
|
+
"prompt": "Start an intent whose savepoint last line is 'How checklist.md created'.",
|
|
110
|
+
"expected_output": "Boards at Exec (verify plan + checklist real), per references/boarding-matrix.md, and continues delivery/ticks the checklist.",
|
|
111
|
+
"files": [],
|
|
112
|
+
"assertions": [
|
|
113
|
+
{ "type": "human", "check": "boarding matrix lands 'How checklist.md created' / 'Exec started' at Exec", "result": "expect-pass" }
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Boarding matrix — which station Start drops you at
|
|
2
|
+
|
|
3
|
+
The station is derived from `savepoint.md`'s last line plus the real artifacts on disk.
|
|
4
|
+
Classify from the last line alone, then verify ONLY that line's artifact is real
|
|
5
|
+
(sentinel-aware). On drift, rebuild the ledger from disk and note it.
|
|
6
|
+
|
|
7
|
+
| savepoint last line | latest delivered | boards at | continue with |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| `What {id}--{slug}.md` (born) | What | **What / Why** | What work (106-expanded), then brainstorm → `spec.md` |
|
|
10
|
+
| `Why started` (spec still sentinel) | What | **Why** | continue brainstorming → `spec.md` |
|
|
11
|
+
| `Why spec.md created` | Why | **How** | `plan.md` + `actions/` + `checklist.md` |
|
|
12
|
+
| `How started` / `How plan.md created` | (How in progress) | **How** | finish `plan.md` → `checklist.md` |
|
|
13
|
+
| `How checklist.md created` / `Exec started` | How | **Exec** | implement, tick the checklist |
|
|
14
|
+
| `Exec outcome.md created` | Exec | **ready to complete** | exit at Done |
|
|
15
|
+
| `Done delivered` / `Done abandoned` | terminal | **report only** | immutable; ask what is next |
|
|
16
|
+
|
|
17
|
+
## Per-station behaviour (what "continue" means)
|
|
18
|
+
|
|
19
|
+
- **What** → do what What requires (to be expanded in 106), then brainstorm → `spec.md`.
|
|
20
|
+
- **Why** → continue brainstorming; deliver `spec.md`.
|
|
21
|
+
- **How** → continue `plan.md` + `actions/` + `checklist.md`.
|
|
22
|
+
- **Exec** → verify what has been delivered, then continue (or restart) the delivery /
|
|
23
|
+
research. The first unchecked `checklist.md` item is the next step; the newest `## Insights`
|
|
24
|
+
entry supplies human-readable context.
|
|
25
|
+
- **ready to complete** → `outcome.md` is real; run the ending procedure (~93).
|
|
26
|
+
- **Done** → terminal. Report the outcome, ask what is next. Never reopen; INDEX is
|
|
27
|
+
authoritative.
|
|
28
|
+
|
|
29
|
+
## Notes
|
|
30
|
+
|
|
31
|
+
- The mode (auto / guided) is asked exactly ONCE, whatever station Start lands at. It is never
|
|
32
|
+
re-asked at a later station. The lock is taken FIRST regardless of station (terminal intents
|
|
33
|
+
excepted: they get no lock and no resume).
|
|
34
|
+
- An `## Insights` entry marked `(autonomous)` means the intent was being delivered
|
|
35
|
+
autonomously; in guided mode, surface that and offer to hand back to `plastic-auto`.
|