@zalom/plastic 1.14.1 → 2.0.0-alpha.10

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.
Files changed (213) hide show
  1. package/PLASTIC.md +101 -294
  2. package/README.md +5 -6
  3. package/agents/plastic-advisor.md +18 -17
  4. package/agents/plastic-enforcer.md +57 -69
  5. package/agents/plastic-executor.md +34 -20
  6. package/agents/plastic-faux-advisor.md +18 -18
  7. package/bin/lib/context_budget.rb +453 -0
  8. package/bin/plastic-bench +78 -0
  9. package/config_asks.yml +38 -0
  10. package/hooks/capture +4 -0
  11. package/hooks/close +5 -0
  12. package/hooks/hooks.json +14 -42
  13. package/hooks/message-display +81 -0
  14. package/hooks/record +4 -0
  15. package/hooks/savepoint +5 -5
  16. package/package.json +3 -2
  17. package/scripts/agent-report +15 -10
  18. package/scripts/append-ledger +208 -0
  19. package/scripts/codex-hook +78 -90
  20. package/scripts/dashboard.rb +43 -14
  21. package/scripts/day-summary +53 -0
  22. package/scripts/doctor.rb +269 -43
  23. package/scripts/end-intent +170 -76
  24. package/scripts/exec-worktree +11 -20
  25. package/scripts/file-session-intent +61 -0
  26. package/scripts/hook-capture +255 -0
  27. package/scripts/hook-close +36 -0
  28. package/scripts/hook-message-display +74 -0
  29. package/scripts/hook-record +157 -0
  30. package/scripts/hook-savepoint +45 -0
  31. package/scripts/hook-session-start +128 -12
  32. package/scripts/install.rb +8 -6
  33. package/scripts/intent-screen +77 -0
  34. package/scripts/lib/agent_models.rb +1 -7
  35. package/scripts/lib/arm.rb +308 -0
  36. package/scripts/lib/backfill_intent.rb +316 -0
  37. package/scripts/lib/bridge.rb +24 -1597
  38. package/scripts/lib/compact_instructions.rb +56 -0
  39. package/scripts/lib/day_summary.rb +211 -0
  40. package/scripts/lib/doctor_core.rb +66 -88
  41. package/scripts/lib/doctor_session_ledger.rb +158 -0
  42. package/scripts/lib/exec_worktree.rb +14 -60
  43. package/scripts/lib/handoff.rb +184 -0
  44. package/scripts/lib/harness_text.rb +5 -4
  45. package/scripts/lib/hook_registry.rb +57 -106
  46. package/scripts/lib/insights.rb +3 -4
  47. package/scripts/lib/installer_core.rb +136 -41
  48. package/scripts/lib/intent_screen.rb +309 -0
  49. package/scripts/lib/intent_screen_ansi.rb +262 -0
  50. package/scripts/lib/lock.rb +6 -26
  51. package/scripts/lib/message_display.rb +290 -0
  52. package/scripts/lib/outcome_guard.rb +7 -5
  53. package/scripts/lib/project_validator.rb +30 -1
  54. package/scripts/lib/report_screen.rb +648 -0
  55. package/scripts/lib/roadmap_queue.rb +3 -3
  56. package/scripts/lib/roadmap_savepoint.rb +1 -1
  57. package/scripts/lib/rule_catalog.rb +6 -3
  58. package/scripts/lib/savepoint.rb +334 -0
  59. package/scripts/lib/scaffold_intent.rb +13 -179
  60. package/scripts/lib/screen_paint.rb +276 -0
  61. package/scripts/lib/session_backfill.rb +184 -0
  62. package/scripts/lib/session_close.rb +104 -0
  63. package/scripts/lib/session_git.rb +500 -0
  64. package/scripts/lib/session_ledger.rb +621 -0
  65. package/scripts/lib/store_discovery.rb +1 -1
  66. package/scripts/lib/store_provisioning.rb +1 -1
  67. package/scripts/maintenance-run +7 -7
  68. package/scripts/new-intent +63 -4
  69. package/scripts/plastic-lock +101 -41
  70. package/scripts/promote-session-item +150 -0
  71. package/scripts/read-config +3 -0
  72. package/scripts/rebuild-graph +1 -1
  73. package/scripts/report-screen +120 -0
  74. package/scripts/roadmap-next +2 -2
  75. package/scripts/rollback.rb +48 -3
  76. package/scripts/savepoint-note +67 -0
  77. package/scripts/scaffold-intent +37 -63
  78. package/scripts/session-commit +116 -0
  79. package/scripts/spawn-preamble +14 -7
  80. package/scripts/update.rb +7 -3
  81. package/scripts/write-handoff +60 -0
  82. package/skills/agent-advisor/SKILL.md +3 -3
  83. package/skills/agent-advisor/references/advisor-protocol.md +20 -20
  84. package/skills/auto/SKILL.md +223 -317
  85. package/skills/auto/evals/evals.json +10 -10
  86. package/skills/auto/references/agent-architecture.md +102 -90
  87. package/skills/auto/references/agent-report-contract.md +4 -21
  88. package/skills/auto/references/end-tail.md +47 -53
  89. package/skills/auto/references/human-report-contract.md +62 -56
  90. package/skills/conventions/SKILL.md +1 -3
  91. package/skills/conventions/references/lifecycle-and-savepoints.md +35 -1
  92. package/skills/conventions/references/locks-and-worktrees.md +62 -58
  93. package/skills/conventions/references/maintenance-and-revisions.md +12 -12
  94. package/skills/dashboard/SKILL.md +1 -1
  95. package/skills/direct/SKILL.md +66 -0
  96. package/skills/direct/references/request-signals.md +59 -0
  97. package/skills/doctor/SKILL.md +58 -11
  98. package/skills/doctor/report.md +3 -1
  99. package/skills/install/SKILL.md +11 -9
  100. package/skills/intent-continuing/SKILL.md +142 -91
  101. package/skills/intent-continuing/references/boarding-matrix.md +34 -0
  102. package/skills/intent-continuing/references/context-management.md +3 -3
  103. package/skills/intent-creating/SKILL.md +2 -2
  104. package/skills/intent-creating/references/lifecycle.md +1 -1
  105. package/skills/intent-ending/SKILL.md +34 -37
  106. package/skills/intent-ending/evals/evals.json +4 -4
  107. package/skills/intent-executing/SKILL.md +15 -46
  108. package/skills/intent-executing/plan-reviewer-prompt.md +39 -0
  109. package/skills/intent-speccing/SKILL.md +108 -51
  110. package/skills/intent-speccing/evals/evals.json +5 -5
  111. package/skills/intent-speccing/references/per-section-fill-rules.md +4 -4
  112. package/skills/intent-speccing/references/self-verify-checklist.md +1 -2
  113. package/skills/project-creating/SKILL.md +1 -1
  114. package/skills/releasing/SKILL.md +7 -8
  115. package/skills/tutorial/SKILL.md +2 -2
  116. package/skills/tutorial/references/track-1-guided.md +16 -18
  117. package/skills/tutorial/references/track-2-auto.md +12 -13
  118. package/skills/tutorial/references/track-3-projects-and-roadmaps.md +2 -2
  119. package/templates/agents.md +4 -7
  120. package/templates/config.yml +5 -0
  121. package/templates/intent-screen.md +17 -0
  122. package/templates/outcome.md +14 -1
  123. package/templates/project.yml +7 -0
  124. package/templates/report-state.md +11 -0
  125. package/templates/roadmap.md +1 -1
  126. package/templates/savepoint.md +2 -2
  127. package/templates/session-intent.md +29 -0
  128. package/agents/plastic-brainstorming.md +0 -48
  129. package/agents/plastic-future-intent-researcher.md +0 -35
  130. package/agents/plastic-intent-curator.md +0 -47
  131. package/agents/plastic-intent-discovery.md +0 -43
  132. package/agents/plastic-planner.md +0 -56
  133. package/agents/plastic-spec-specialist.md +0 -53
  134. package/hooks/auto-arm +0 -5
  135. package/hooks/bash-gate +0 -3
  136. package/hooks/continue +0 -31
  137. package/hooks/edit-gates +0 -3
  138. package/hooks/future-intent-check +0 -25
  139. package/hooks/gate-check +0 -12
  140. package/hooks/power-tools +0 -8
  141. package/scripts/hook-auto-arm +0 -51
  142. package/scripts/hook-bash-gate +0 -78
  143. package/scripts/hook-code-gate +0 -38
  144. package/scripts/hook-continue +0 -48
  145. package/scripts/hook-create-gate +0 -51
  146. package/scripts/hook-edit-gates +0 -58
  147. package/scripts/hook-future-intent-check +0 -90
  148. package/scripts/hook-gate-check +0 -169
  149. package/scripts/hook-links-gate +0 -54
  150. package/scripts/hook-lock-gate +0 -47
  151. package/scripts/hook-power-tools +0 -38
  152. package/scripts/hook-savepoint-pre +0 -32
  153. package/scripts/lib/codex_edit_gates.rb +0 -138
  154. package/scripts/lib/edit_gates.rb +0 -398
  155. package/scripts/lib/links_gate.rb +0 -140
  156. package/scripts/lib/qmd_hook.rb +0 -41
  157. package/scripts/lib/spec_header.rb +0 -83
  158. package/scripts/lib/start_intent.rb +0 -296
  159. package/scripts/start-intent +0 -89
  160. package/skills/_active-intent-gate.md +0 -26
  161. package/skills/auto/references/tiers.md +0 -100
  162. package/skills/continuing/SKILL.md +0 -34
  163. package/skills/continuing/evals/evals.json +0 -91
  164. package/skills/conventions/references/gates-and-enforcement.md +0 -53
  165. package/skills/conventions/references/tiers-and-dispatch.md +0 -135
  166. package/skills/doctor/references/gates-stuck-detection.md +0 -43
  167. package/skills/intent-brainstorming/SKILL.md +0 -118
  168. package/skills/intent-brainstorming/evals/evals.json +0 -67
  169. package/skills/intent-continuing/evals/evals.json +0 -145
  170. package/skills/intent-discovering/SKILL.md +0 -56
  171. package/skills/intent-grilling/SKILL.md +0 -108
  172. package/skills/intent-linking/SKILL.md +0 -128
  173. package/skills/intent-linking/evals/evals.json +0 -22
  174. package/skills/intent-linking/references/zettelkasten.md +0 -45
  175. package/skills/intent-locking/SKILL.md +0 -64
  176. package/skills/intent-planning/SKILL.md +0 -145
  177. package/skills/intent-planning/evals/evals.json +0 -81
  178. package/skills/intent-planning/references/plan-format.md +0 -130
  179. package/skills/intent-researching/SKILL.md +0 -123
  180. package/skills/intent-researching/evals/evals.json +0 -22
  181. package/skills/intent-savepoint/SKILL.md +0 -83
  182. package/skills/intent-savepoint/references/context-management.md +0 -32
  183. package/skills/intent-starting/SKILL.md +0 -151
  184. package/skills/intent-starting/evals/evals.json +0 -117
  185. package/skills/intent-starting/references/boarding-matrix.md +0 -35
  186. package/skills/project-continuing/SKILL.md +0 -119
  187. package/skills/project-continuing/evals/evals.json +0 -100
  188. package/skills/roadmap-continuing/SKILL.md +0 -89
  189. package/skills/roadmap-continuing/evals/evals.json +0 -82
  190. package/skills/skill-creating/SKILL.md +0 -75
  191. package/skills/skill-creating/evals/evals.json +0 -108
  192. package/skills/skill-creating/references/agents.md +0 -168
  193. package/skills/skill-creating/references/defaults-first.md +0 -23
  194. package/skills/skill-creating/references/evals.md +0 -41
  195. package/skills/skill-creating/references/hooks.md +0 -251
  196. package/skills/skill-creating/references/progressive-disclosure.md +0 -176
  197. package/skills/skill-creating/references/scripts.md +0 -166
  198. package/skills/skill-creating/references/skills.md +0 -169
  199. package/skills/skill-creating/scripts/scaffold.rb +0 -313
  200. package/skills/skill-evaluating/SKILL.md +0 -141
  201. package/skills/skill-evaluating/assets/eval-template.json +0 -12
  202. package/skills/skill-evaluating/evals/evals.json +0 -75
  203. package/skills/skill-evaluating/references/convention-checks.md +0 -76
  204. package/skills/skill-evaluating/references/eval-methodology.md +0 -154
  205. package/skills/store-curating/SKILL.md +0 -64
  206. package/skills/store-curating/evals/evals.json +0 -38
  207. package/skills/store-indexing/SKILL.md +0 -93
  208. package/skills/store-indexing/evals/evals.json +0 -22
  209. package/skills/store-indexing/references/zettelkasten-linking.md +0 -32
  210. package/skills/store-provisioning/SKILL.md +0 -55
  211. /package/skills/{project-continuing → intent-continuing}/references/board-fill.md +0 -0
  212. /package/skills/{roadmap-continuing → intent-continuing}/references/liveness-ranking.md +0 -0
  213. /package/skills/{intent-brainstorming → intent-speccing}/references/design-principles.md +0 -0
@@ -1,53 +0,0 @@
1
- ---
2
- name: plastic-spec-specialist
3
- description: |
4
- Use at the Why-to-How boundary in auto mode: consolidate the enriched Why into
5
- spec.md from the spec template.
6
- model: sonnet
7
- ---
8
-
9
- You are the Plastic Spec Specialist. You own the Why-to-How boundary in the What->Why->How->Exec cycle.
10
-
11
- When dispatched in auto mode you receive the standard Plastic spawn preamble (from `scripts/spawn-preamble`) prepended to your prompt: it states the active intent id, intent line, current stage, your role, and the instruction to emit valid lifecycle artifacts. Honor it as your live state; do not re-derive or contradict it.
12
-
13
- ## Your Responsibilities
14
-
15
- 1. **Consolidate the Why**: turn the enriched `## Context` and `### Decisions` into one spec
16
- 2. **Follow the template**: produce `spec.md` with all 8 sections in template order: Problem, Goals,
17
- Non-Goals, Approach, Alternatives Considered, Decisions, Acceptance Criteria, Open Questions.
18
- The shared fill method for each section is `skills/intent-speccing/references/per-section-fill-rules.md`;
19
- read it when filling the template, it is the single owner of the method, do not fork it here.
20
- 3. **Stamp the tier**: write a `Tier: S|M|L` line as the TOP line of spec.md, above the `#
21
- Spec:` heading, taking the value from the orchestrator's sizing / the brainstorming
22
- recommendation. This line is convention-only: read by the orchestrator, never validated by
23
- any gate or by doctor. It is an extra top line, not a template field, so no template changes.
24
- 4. **Accept collapsed sections at S/M**: a spec.md whose Problem, Goals, Non-Goals, Approach,
25
- Decisions, or Acceptance Criteria sections are a single line each is still a complete
26
- spec.md under the same-structure invariant; do not pad it. The gates check file existence
27
- and realness, not artifact depth, so a collapsed-but-real spec passes.
28
- 5. **Make it the contract**: the spec is what the planner and executor build against
29
- 6. **Hand off**: leave a complete `spec.md` ready for the planner
30
-
31
- ## How You Work
32
-
33
- 1. Receive (input handoff): the enriched `## Context` plus `### Decisions` from the brainstorming stage
34
- 2. Read the spec template (`templates/spec.md`) and match its FORM exactly
35
- 3. Write `spec.md` into the intent directory, resolving the decisions into a coherent approach
36
- 4. Produce (output handoff): a complete `spec.md`
37
- 5. Report for gating (see `## Completion Report`); the enforcer checks the spec against its exit criteria before How begins
38
-
39
- ## Completion Report
40
-
41
- END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers, insights) plus the spec payload:
42
-
43
- - The spec sections produced (Problem, Goals, Non-Goals, Approach, Decisions, Acceptance Criteria)
44
- - How the recorded decisions resolved into the chosen approach
45
- - The acceptance-criteria count, so the planner knows the surface to cover
46
- - The stamped `Tier:` value
47
-
48
- ## Constraints
49
-
50
- - You are dispatched by the plastic-enforcer and your deliverable is gated before How begins
51
- - You write `spec.md` only; you do not write `plan.md`, `actions/`, or project code
52
- - You write only intent-store files, never project code
53
- - You do not re-open exploration; if decisions are missing, flag the gap rather than inventing scope
package/hooks/auto-arm DELETED
@@ -1,5 +0,0 @@
1
- #!/bin/bash
2
- INPUT=$(cat)
3
- MESSAGE=$(echo "$INPUT" | env -u RUBYOPT ruby -rjson -e 'puts JSON.parse(STDIN.read)["user_prompt"].to_s' 2>/dev/null || echo "")
4
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
- env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-auto-arm" "$MESSAGE"
package/hooks/bash-gate DELETED
@@ -1,3 +0,0 @@
1
- #!/bin/bash
2
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
3
- exec env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-bash-gate"
package/hooks/continue DELETED
@@ -1,31 +0,0 @@
1
- #!/bin/bash
2
- INPUT=$(cat)
3
- MESSAGE=$(echo "$INPUT" | env -u RUBYOPT 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=$(env -u RUBYOPT 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 plastic-continuing skill workflow."
27
- }
28
- }
29
- HOOKJSON
30
- fi
31
- fi
package/hooks/edit-gates DELETED
@@ -1,3 +0,0 @@
1
- #!/bin/bash
2
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
3
- exec env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-edit-gates"
@@ -1,25 +0,0 @@
1
- #!/bin/bash
2
- INPUT=$(cat)
3
- MESSAGE=$(echo "$INPUT" | env -u RUBYOPT 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
- env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-future-intent-check" "$STORE_ROOT" "$MESSAGE"
package/hooks/gate-check DELETED
@@ -1,12 +0,0 @@
1
- #!/bin/bash
2
- INPUT=$(cat)
3
- FILE_PATH=$(echo "$INPUT" | env -u RUBYOPT 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" | env -u RUBYOPT 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
- env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-gate-check" "$FILE_PATH" "$SESSION_ID"
package/hooks/power-tools DELETED
@@ -1,8 +0,0 @@
1
- #!/bin/bash
2
- # power-tools mandate hook launcher (intents 66, 66b, 187, 246). No-op when there is no global store.
3
- GLOBAL_INDEX="$HOME/.plastic/INDEX.md"
4
- if [ ! -f "$GLOBAL_INDEX" ]; then
5
- exit 0
6
- fi
7
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
- exec env -u RUBYOPT ruby "$SCRIPT_DIR/../scripts/hook-power-tools"
@@ -1,51 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # encoding: UTF-8
3
- # frozen_string_literal: true
4
-
5
- # Usage: hook-auto-arm <message>
6
- # UserPromptSubmit arming (intent 27). Two jobs, both additive context — never blocks:
7
- # 1. Detect auto-trigger phrases -> steer the agent to invoke plastic-auto.
8
- # 2. If auto mode is armed and the active intent hasn't reached How -> remind the agent
9
- # that project-code edits are gate-blocked until plan.md + checklist.md exist.
10
- # Outputs UserPromptSubmit hookSpecificOutput JSON, or exits silently.
11
-
12
- require "json"
13
- require_relative "lib/bridge"
14
-
15
- message = (ARGV[0] || "").downcase
16
- parts = []
17
-
18
- # --- 1. Auto-trigger steering ---
19
- AUTO_TRIGGERS = ["take it from here", "deliver this", "/plastic-auto", "auto mode"].freeze
20
- # bare "auto" only as a standalone word to avoid matching "automatic", "automation", etc.
21
- triggered = AUTO_TRIGGERS.any? { |p| message.include?(p) } || message.match?(/\bauto\b/)
22
- if triggered
23
- parts << "Auto-mode request detected. Invoke the plastic-auto skill to deliver the " \
24
- "active intent autonomously — it arms the lifecycle gate so project code " \
25
- "cannot be edited before plan.md + checklist.md exist."
26
- end
27
-
28
- # --- 2. Standing gate reminder when armed and pre-How ---
29
- bridge_data = Bridge.discover_bridge(session: ENV["CLAUDE_CODE_SESSION_ID"], cwd: Dir.pwd)
30
- if bridge_data && bridge_data.dig("build", "auto") == true
31
- intent = bridge_data["intent"] || {}
32
- store = intent["store"]; dir = intent["dir"]
33
- if store && dir
34
- intent_dir = File.expand_path("#{store}/#{dir}")
35
- reached_how = File.exist?("#{intent_dir}/plan.md") && File.exist?("#{intent_dir}/checklist.md")
36
- unless reached_how
37
- parts << "Auto mode armed on intent #{intent["id"]} — produce spec -> plan -> " \
38
- "checklist before editing project code. Code edits are gate-blocked until then."
39
- end
40
- end
41
- end
42
-
43
- exit 0 if parts.empty?
44
-
45
- payload = {
46
- "hookSpecificOutput" => {
47
- "hookEventName" => "UserPromptSubmit",
48
- "additionalContext" => parts.join("\n")
49
- }
50
- }
51
- puts JSON.generate(payload)
@@ -1,78 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # encoding: UTF-8
3
- # frozen_string_literal: true
4
-
5
- # PreToolUse gate for Bash (intent 27a): close the Bash-edit bypass. When auto
6
- # mode is armed and the active intent has not reached How (plan.md + checklist.md),
7
- # block Bash commands that WRITE project code outside the store.
8
- #
9
- # Reads the Claude Code PreToolUse payload as JSON on STDIN:
10
- # { "tool_input": { "command": "..." }, "cwd": "..." }
11
- # Empty / unparseable / no command => exit 0 (allow). Conservatism is the prime
12
- # directive: when a command's write intent is ambiguous, ALLOW.
13
- #
14
- # Exit 0 = allow. Exit 2 = block (reason on stderr, shown to the agent).
15
-
16
- require "json"
17
- require_relative "lib/bridge"
18
-
19
- raw = $stdin.read rescue nil
20
- exit 0 if raw.nil? || raw.strip.empty?
21
-
22
- payload = JSON.parse(raw) rescue nil
23
- exit 0 unless payload.is_a?(Hash)
24
-
25
- command = payload.dig("tool_input", "command") || payload.dig("tool_params", "command")
26
- exit 0 if command.nil? || command.to_s.strip.empty?
27
-
28
- cwd = payload["cwd"]
29
- cwd = Dir.pwd if cwd.nil? || cwd.to_s.empty?
30
-
31
- session = payload["session_id"]
32
- session = ENV["CLAUDE_CODE_SESSION_ID"] if session.nil? || session.to_s.empty?
33
-
34
- # Auditable escape (intent 108, D7): a trailing `# plastic-ok` allows the
35
- # command and logs it, so sanctioned writes are visible, not silent.
36
- if Bridge.bash_escape?(command)
37
- begin
38
- require "fileutils"
39
- log = File.join(Dir.home, ".plastic", ".cache", "gate-escapes.log")
40
- FileUtils.mkdir_p(File.dirname(log))
41
- File.open(log, "a") do |io|
42
- io.puts("#{Time.now.utc.iso8601}\t#{session}\t#{command.gsub(/\s+/, ' ').strip}")
43
- end
44
- rescue StandardError
45
- # the escape still applies; logging is best-effort
46
- end
47
- exit 0
48
- end
49
-
50
- # --- Load bridge (shared resolution; stdin session_id -> CLAUDE_CODE_SESSION_ID -> /tmp scan) ---
51
- # A nil bridge does NOT short-circuit (intent 108): the lock gate decides from
52
- # the durable delivery.lock file, so it must run even without a bridge cache.
53
- bridge_data = Bridge.discover_bridge(session: session, cwd: cwd)
54
-
55
- reason = Bridge.bash_gate_decision(bridge_data, command, cwd: cwd, session: session)
56
- exit 0 unless reason
57
-
58
- # Block log (intent 229): one six-field TSV line, hand-rolled here exactly as
59
- # the escape write above is, so this script keeps requiring only lib/bridge and
60
- # pays no extra load on every Bash tool call. Best-effort: the block applies
61
- # regardless.
62
- begin
63
- require "fileutils"
64
- require "time"
65
- blocklog = File.join(Dir.home, ".plastic", ".cache", "gate-blocks.log")
66
- FileUtils.mkdir_p(File.dirname(blocklog))
67
- intent_id = bridge_data.is_a?(Hash) ? bridge_data.dig("intent", "id").to_s : ""
68
- File.open(blocklog, "a") do |io|
69
- io.puts([Time.now.utc.iso8601, "bash-gate", session.to_s, intent_id,
70
- command.to_s.gsub(/\s+/, " ").strip,
71
- reason.to_s.gsub(/\s+/, " ").strip].join("\t"))
72
- end
73
- rescue StandardError
74
- # the block still applies; logging is best-effort
75
- end
76
-
77
- $stderr.puts "PLASTIC GATE — #{reason}"
78
- exit 2
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # encoding: UTF-8
3
- # frozen_string_literal: true
4
-
5
- # Usage: hook-code-gate <file_path> [session_id] [content]
6
- # PreToolUse gate. Composes two independent block rules; EITHER blocks the edit:
7
- # - Stage rule (intent 27): when auto mode is armed and the active intent has not
8
- # reached How (plan.md + checklist.md), block edits to project code outside the store.
9
- # - Worktree isolation rule (intent 73c2): when the intent has a provisioned code
10
- # worktree, block project-code edits outside it; and block edits to another
11
- # intent's store dir whose delivery lock is held by a live non-owner session.
12
- #
13
- # Exit 0 = allow. Exit 2 = block (reason on stderr, shown to the agent).
14
- # No bridge = allow. Each rule fails open on its own conditions (see bridge.rb).
15
- #
16
- # Thin CLI wrapper (intent 244): the gate logic itself lives in
17
- # scripts/lib/edit_gates.rb, shared with the merged Claude dispatcher
18
- # (scripts/hook-edit-gates) and, since intent 251, the merged Codex dispatcher
19
- # (scripts/lib/codex_edit_gates.rb). This wrapper has no production caller on
20
- # either harness anymore; it remains as the per-gate isolation surface for the
21
- # hook test suite.
22
-
23
- require_relative "lib/edit_gates"
24
-
25
- file_path = ARGV[0]
26
- exit 0 unless file_path && !file_path.empty?
27
-
28
- session = (ARGV[1] unless ARGV[1].to_s.empty?) || ENV["CLAUDE_CODE_SESSION_ID"]
29
-
30
- ctx = EditGates::Context.new(
31
- tool_name: "Write", session: session, gate_path: file_path, file_path: file_path,
32
- content: ARGV[2], harness: "claude",
33
- )
34
-
35
- outcome = EditGates.code_gate(ctx)
36
- exit 0 unless outcome
37
- outcome.lines.each { |line| $stderr.puts line }
38
- exit 2
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # encoding: UTF-8
3
- # Usage: hook-continue <index_path> <store_root> <mode>
4
- # Renders the dashboard `continue` cockpit as UserPromptSubmit additionalContext.
5
- # Exits silently (no output) if the dashboard produces nothing — the bash caller
6
- # then falls back to a minimal notice.
7
-
8
- require "json"
9
- require "open3"
10
- require_relative "lib/dashboard_banner"
11
-
12
- index_path, _store_root, _mode = ARGV
13
- exit 0 unless index_path && File.exist?(index_path)
14
-
15
- dashboard = File.expand_path("dashboard.rb", __dir__)
16
- exit 0 unless File.exist?(dashboard)
17
-
18
- cockpit, _err, status = Open3.capture3({"RUBYOPT" => nil}, "ruby", dashboard, "continue")
19
- exit 0 unless status.success? && !cockpit.strip.empty?
20
-
21
- context = cockpit.rstrip +
22
- "\n\nFollow the plastic-continuing skill workflow to resume: " \
23
- "if active intents exist, read their state and resume; otherwise surface the " \
24
- "most-valuable next work and any stale or triage intents from the board above."
25
-
26
- payload = {
27
- "hookSpecificOutput" => {
28
- "hookEventName" => "UserPromptSubmit",
29
- "additionalContext" => context
30
- }
31
- }
32
-
33
- # Hook-owned systemMessage floor (intent 125, Task 6): a hard fallback summary
34
- # independent of the agent's reply, mirroring hook-session-start's BootBanner
35
- # pattern. Best-effort only — any failure here (subprocess, JSON, renderer)
36
- # degrades silently, omitting systemMessage; this hook must never crash
37
- # UserPromptSubmit over a summary line.
38
- begin
39
- data_json, _data_err, data_status = Open3.capture3({"RUBYOPT" => nil}, "ruby", dashboard, "continue", "--data")
40
- if data_status.success?
41
- banner = DashboardBanner.render(JSON.parse(data_json))
42
- payload["systemMessage"] = banner if banner
43
- end
44
- rescue StandardError
45
- nil
46
- end
47
-
48
- puts JSON.generate(payload)
@@ -1,51 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # encoding: UTF-8
3
- # frozen_string_literal: true
4
-
5
- # PreToolUse create gate (intent 60b): validate the PROPOSED content of a Write
6
- # to an intent file before it lands. Fires when the target path is an intent file
7
- # inside its own equally-named directory (store/<id>--<slug>/<id>--<slug>.md). It
8
- # validates the proposed content from the hook payload (tool_input.content), NOT
9
- # the on-disk file (which does not exist yet at PreToolUse), using IntentValidator
10
- # for born-complete frontmatter plus the sanctioned section structure.
11
- #
12
- # It depends ONLY on the stdin path + content, never on the auto-bridge or any
13
- # session id, so it runs unconditionally (headless / background sessions).
14
- # It validates ONLY the intent file, never sentinel placeholder lifecycle files
15
- # (spec.md/plan.md/etc.); the path matcher excludes them.
16
- #
17
- # Exit 0 = allow. Exit 2 = block (reason on stderr, shown to the agent).
18
- #
19
- # Reads the Claude Code PreToolUse payload as JSON on STDIN:
20
- # { "tool_input": { "file_path": "...", "content": "..." } }
21
- # Empty / unparseable / non-matching path => exit 0 (cannot judge, allow).
22
- # Three payload shapes on a matching path (intent 108, D7):
23
- # Write (content): validate the proposed content.
24
- # Edit (old_string): simulate the replacement on the on-disk file and
25
- # validate the RESULT; a missing file blocks.
26
- # Pathless MCP mutation: validate the CURRENT on-disk file (the PostToolUse
27
- # gate-check backstop validates the result); a
28
- # missing file blocks (fail-safe).
29
- #
30
- # Thin CLI wrapper (intent 244): the gate logic itself lives in
31
- # scripts/lib/edit_gates.rb, shared with the merged Claude dispatcher
32
- # (scripts/hook-edit-gates) and, since intent 251, the merged Codex dispatcher
33
- # (scripts/lib/codex_edit_gates.rb). This wrapper has no production caller on
34
- # either harness anymore; it remains as the per-gate isolation surface for the
35
- # hook test suite. Deliberately WITHOUT a top-level rescue, matching this
36
- # script's behavior before the extraction: an internal exception here has
37
- # never been caught.
38
-
39
- require "json"
40
- require_relative "lib/edit_gates"
41
-
42
- raw = $stdin.read rescue nil
43
- exit 0 if raw.nil? || raw.strip.empty?
44
-
45
- payload = JSON.parse(raw) rescue nil
46
- exit 0 unless payload.is_a?(Hash)
47
-
48
- outcome = EditGates.create_gate(EditGates.context_from(payload))
49
- exit 0 unless outcome
50
- outcome.lines.each { |line| $stderr.puts line }
51
- exit 2
@@ -1,58 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # encoding: UTF-8
3
- # frozen_string_literal: true
4
-
5
- # The merged Claude PreToolUse edit-path dispatcher (intent 244). One registered
6
- # hook, one Ruby process, one stdin parse, five gates in-process, in the fixed
7
- # order savepoint-pre, lock-gate, code-gate, links-gate, create-gate (spec D-a).
8
- # The first deny ends evaluation and its own shape is reproduced exactly: stderr
9
- # plus exit 2 for code-gate/links-gate/create-gate, stdout permissionDecision
10
- # JSON plus exit 0 for lock-gate (spec D-b). savepoint-pre runs first so its
11
- # `started` ledger append stays unconditional, including on a call another gate
12
- # then blocks (spec D-c).
13
- #
14
- # Fail-open everywhere (spec D-i): each gate runs inside its own rescue in
15
- # EditGates.dispatch, and this file's top level rescues to exit 0, so a gate bug
16
- # never denies and never silences a sibling gate.
17
- #
18
- # LOAD-BEARING SHAPE: doctor's claude_hooks_implemented_check reads THIS FILE as
19
- # plain text and extracts the `when "<gate>"` labels of the `case gate` statement
20
- # below, because a script with top-level side effects can never be required to
21
- # introspect it (the same technique intent 200 used for scripts/codex-hook). If
22
- # the case statement is reshaped, update that check in scripts/doctor.rb in the
23
- # same change, or it will fail loudly by design.
24
-
25
- require "json"
26
- require_relative "lib/edit_gates"
27
-
28
- def route(gate, ctx)
29
- case gate
30
- when "savepoint-pre" then EditGates.savepoint_pre(ctx)
31
- when "lock-gate" then EditGates.lock_gate(ctx)
32
- when "code-gate" then EditGates.code_gate(ctx)
33
- when "links-gate" then EditGates.links_gate(ctx)
34
- when "create-gate" then EditGates.create_gate(ctx)
35
- end
36
- end
37
-
38
- begin
39
- raw = begin
40
- $stdin.read
41
- rescue StandardError
42
- nil
43
- end
44
- exit 0 if raw.nil? || raw.strip.empty?
45
-
46
- payload = begin
47
- JSON.parse(raw)
48
- rescue StandardError
49
- nil
50
- end
51
- exit 0 unless payload.is_a?(Hash)
52
-
53
- exit EditGates.dispatch(ctx: EditGates.context_from(payload), route: method(:route))
54
- rescue StandardError => e
55
- # Dispatcher-internal failure: never impersonate a deny (spec D-i).
56
- $stderr.puts "plastic edit-gates error: #{e.message}"
57
- exit 0
58
- end
@@ -1,90 +0,0 @@
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)