@zalom/plastic 1.3.0 → 1.4.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/PLASTIC-reference.md +8 -6
- package/PLASTIC.md +52 -14
- package/hooks/hooks.json +5 -0
- package/hooks/links-gate +3 -0
- package/package.json +1 -1
- package/scripts/codex-hook +122 -8
- package/scripts/dashboard.rb +323 -71
- package/scripts/doctor.rb +393 -58
- package/scripts/end-intent +347 -43
- package/scripts/hook-links-gate +74 -0
- package/scripts/hook-lock-gate +8 -3
- package/scripts/install.rb +51 -6
- package/scripts/lib/bridge.rb +105 -27
- package/scripts/lib/config_asks.rb +110 -0
- package/scripts/lib/graph_rebuild.rb +30 -6
- package/scripts/lib/hook_registry.rb +34 -3
- package/scripts/lib/installer_core.rb +70 -13
- package/scripts/lib/intent_validator.rb +38 -10
- package/scripts/lib/links_gate.rb +140 -0
- package/scripts/lib/links_projection.rb +71 -12
- package/scripts/lib/lock.rb +186 -11
- package/scripts/lib/power_tools.rb +57 -14
- package/scripts/lib/project_validator.rb +113 -0
- package/scripts/lib/qmd_hook.rb +12 -8
- package/scripts/lib/restore_intent_v1.rb +154 -0
- package/scripts/lib/roadmap_queue.rb +1 -1
- package/scripts/lib/roadmap_savepoint.rb +38 -10
- package/scripts/lib/store_discovery.rb +77 -0
- package/scripts/lib/store_provisioning.rb +21 -12
- package/scripts/new-intent +10 -12
- package/scripts/plastic-lock +76 -9
- package/scripts/project-links +132 -35
- package/scripts/provision-project-store +18 -5
- package/scripts/read-config +1 -0
- package/scripts/rebuild-graph +42 -17
- package/scripts/restore-intent-v1 +288 -0
- package/scripts/roadmap-next +9 -2
- package/scripts/roadmap-savepoint +9 -1
- package/scripts/update.rb +50 -1
- package/scripts/validate-intent +3 -1
- package/scripts/validate-project +53 -0
- package/scripts/write-config +105 -0
- package/skills/auto/SKILL.md +45 -16
- package/skills/auto/references/agent-architecture.md +7 -0
- package/skills/auto/references/end-tail.md +27 -13
- package/skills/dashboard/SKILL.md +48 -25
- package/skills/dashboard/evals/evals.json +4 -4
- package/skills/dashboard/templates/dashboard-global.md +3 -5
- package/skills/dashboard/templates/dashboard-project.md +6 -18
- package/skills/install/SKILL.md +4 -4
- package/skills/intent-creating/SKILL.md +5 -0
- package/skills/intent-ending/SKILL.md +49 -36
- package/skills/intent-locking/SKILL.md +20 -2
- package/skills/intent-starting/SKILL.md +6 -4
- package/skills/project-continuing/SKILL.md +10 -0
- package/skills/project-continuing/evals/evals.json +3 -3
- package/skills/project-continuing/references/board-fill.md +13 -11
- package/skills/project-creating/SKILL.md +29 -1
- package/skills/releasing/SKILL.md +37 -19
- package/skills/roadmap/SKILL.md +9 -7
- package/skills/roadmap/references/file-format.md +14 -10
- package/skills/roadmap/references/operations.md +22 -18
- package/skills/roadmap-continuing/SKILL.md +5 -5
- package/skills/roadmap-continuing/evals/evals.json +3 -3
- package/skills/roadmap-continuing/references/liveness-ranking.md +6 -5
- package/skills/tutorial/SKILL.md +4 -4
- package/skills/tutorial/references/track-1-guided.md +2 -1
- package/skills/tutorial/references/track-2-auto.md +2 -1
- package/skills/tutorial/references/track-3-projects-and-roadmaps.md +12 -11
- package/skills/update/SKILL.md +30 -17
- package/templates/roadmap.md +8 -8
package/PLASTIC-reference.md
CHANGED
|
@@ -67,16 +67,18 @@ root, `~/.plastic/projects/{slug}/roadmaps/`, beside `project.yml`; for the glob
|
|
|
67
67
|
in-flight) roadmaps: once a roadmap's goal is reached, it moves to `roadmaps/archived/{slug}.md`,
|
|
68
68
|
a sibling subdirectory scaffolded once with a `.gitkeep`.
|
|
69
69
|
|
|
70
|
-
A roadmap file has four sections, in order: a title/meta header, `## Goal`, `##
|
|
70
|
+
A roadmap file has four sections, in order: a title/meta header, `## Goal`, `## Batches`, and an
|
|
71
71
|
append-only dated `## Log`. `## Goal` is a checkable prose condition read by a human or agent, not
|
|
72
|
-
an executable checker. `##
|
|
73
|
-
|
|
72
|
+
an executable checker. `## Batches` holds ordered batches; entries inside a batch are
|
|
73
|
+
parallel-safe, batches run sequentially, top to bottom. A roadmap written before owner ruling 145
|
|
74
|
+
may instead use the legacy `## Waves` heading; the tooling accepts both, but never renames an
|
|
75
|
+
existing roadmap file to migrate it.
|
|
74
76
|
|
|
75
|
-
Each
|
|
77
|
+
Each batch entry carries a status token (`queued`/`delivering`/`delivered`/`abandoned`/`blocked`)
|
|
76
78
|
that mirrors that intent's status in `INDEX.md`. `INDEX.md` is the single writer of intent status;
|
|
77
79
|
on any conflict INDEX wins and the roadmap entry is corrected to match.
|
|
78
80
|
|
|
79
|
-
**Human-comprehension surface.** A roadmap is also written to be read cold.
|
|
81
|
+
**Human-comprehension surface.** A roadmap is also written to be read cold. Batch entries render as
|
|
80
82
|
checkboxes (checked once delivered, unchecked otherwise) next to the status token, and each `## Log`
|
|
81
83
|
line is one plain-language sentence, starting `YYYY-MM-DD HH:MM UTC`, written the way an
|
|
82
84
|
engineering manager would brief a non-expert executive: what shipped and why it matters, no jargon
|
|
@@ -86,7 +88,7 @@ opening the file with no other context can tell what shipped, what is running no
|
|
|
86
88
|
next in under a minute.
|
|
87
89
|
|
|
88
90
|
**Relationship to loop engineering (intent 69).** A roadmap is the planning half of the work; the
|
|
89
|
-
loop is its runtime.
|
|
91
|
+
loop is its runtime. Batches lay out the parallelism plan: what can run together, and in what order.
|
|
90
92
|
Loop engineering (intent 69, not yet delivered) is expected to consume that plan and supply the
|
|
91
93
|
running parts, the heartbeat, how many dispatches run at once, checking the goal, and resuming
|
|
92
94
|
after a stop. This section only states the relationship and points to intent 69 as the future
|
package/PLASTIC.md
CHANGED
|
@@ -99,6 +99,11 @@ tags: [plastic, architecture]
|
|
|
99
99
|
| **How** | Planning | `plan.md` + `actions/ACTION_N.md` (at least one) + `checklist.md` | `plastic-intent-planning` |
|
|
100
100
|
| **Exec** | Execution | `outcome.md` | `plastic-intent-executing` |
|
|
101
101
|
|
|
102
|
+
Invoke a skill for your harness: Claude Code uses the slash form (`/plastic-intent-creating`);
|
|
103
|
+
Codex CLI uses a dollar prefix instead (`$plastic-intent-creating`), and may also select a skill
|
|
104
|
+
implicitly by matching its description. Skill names elsewhere in this document are given bare
|
|
105
|
+
(`plastic-intent-creating`); add the prefix for your harness.
|
|
106
|
+
|
|
102
107
|
`## Insights` is the append-only log of durable discoveries captured throughout ALL stages.
|
|
103
108
|
An insight is a discovery worth keeping for later reads: novel, or old but newly relevant,
|
|
104
109
|
surfaced at any stage (What, Why, How, Exec). It is the most interesting residue of an
|
|
@@ -288,8 +293,8 @@ Beyond the lifecycle agents, Plastic ships thin skills for day-to-day operation:
|
|
|
288
293
|
- **`plastic-feedback`** (intent 174) turns a described Plastic quirk, bug, or feature idea
|
|
289
294
|
into a redacted local report file and a prefilled GitHub issue URL; only the user can submit
|
|
290
295
|
it. `disable-model-invocation` hides its description from your own context, so if the user
|
|
291
|
-
hits a Plastic quirk, bug, or missing feature, offer to
|
|
292
|
-
instead of waiting to be asked; the user still sends it, you never do.
|
|
296
|
+
hits a Plastic quirk, bug, or missing feature, offer to invoke the plastic-feedback skill
|
|
297
|
+
yourself instead of waiting to be asked; the user still sends it, you never do.
|
|
293
298
|
|
|
294
299
|
## Releases and Versioning
|
|
295
300
|
|
|
@@ -456,9 +461,10 @@ Advisory. Hard gates guard writes, locks, and structure, never reads or searches
|
|
|
456
461
|
Grep, Glob, and bash search are always allowed, including over the stores. When QMD is
|
|
457
462
|
present and fresh, a content search over store markdown receives an advisory hint pointing
|
|
458
463
|
at `qmd search` alongside its result; when QMD is present but stale, a background reindex
|
|
459
|
-
fires so the next turn's hint runs against a fresh index (never synchronous). QMD
|
|
460
|
-
Serena are recommendations, not obligations: the UserPromptSubmit power-tools hook
|
|
461
|
-
one recommendation line per present tool
|
|
464
|
+
fires so the next turn's hint runs against a fresh index (never synchronous). QMD, Enola,
|
|
465
|
+
and Serena are recommendations, not obligations: the UserPromptSubmit power-tools hook
|
|
466
|
+
appends one recommendation line per present tool, naming Enola only, not both, when Enola
|
|
467
|
+
and Serena are both present (Enola-first, one code-navigation slot). The legacy trailing `# qmd-ok` token is still
|
|
462
468
|
accepted on Bash commands and simply silences the hint. Scope stays the agent's own tool
|
|
463
469
|
calls; Ruby `File.read` inside a script is invisible to the hook by design.
|
|
464
470
|
|
|
@@ -504,18 +510,31 @@ Each gate guards one thing. All are hard except the retrieval gate:
|
|
|
504
510
|
|
|
505
511
|
Exactly one session or agent develops an intent's delivery at a time. Ownership is
|
|
506
512
|
session-keyed and durable: arming acquires `delivery.lock` inside the intent directory
|
|
507
|
-
(atomically, O_EXCL)
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
513
|
+
(atomically, O_EXCL). The session id is the authorization identity. Descriptive provenance
|
|
514
|
+
records the controller's explicit `harness`, `agent`, `model`, `thread`, and `mode` values,
|
|
515
|
+
but never grants access and is never inferred from transcripts or filesystem paths. Missing
|
|
516
|
+
fields on legacy locks display as `Unknown`. Liveness is a lease: the owner's hooks refresh
|
|
517
|
+
the lock file's mtime on tool activity, and that mtime is the sole heartbeat truth. The lock
|
|
518
|
+
counts as stale only when the mtime is older than the TTL. No process id is consulted anywhere.
|
|
519
|
+
The /tmp session bridge is a cache
|
|
511
520
|
of this state; on any disagreement, or when the bridge is missing, the lock file wins.
|
|
512
521
|
Another session that finds a fresh lock backs off; a stale lock is reclaimed only by
|
|
513
522
|
explicit takeover, which replaces the lock and appends an audit line to the intent's
|
|
514
|
-
savepoint.md.
|
|
515
|
-
|
|
523
|
+
savepoint.md. Rearming the same session preserves its acquired identity and refreshes known
|
|
524
|
+
provenance; an explicit takeover replaces the controller and starts new provenance.
|
|
525
|
+
Subagents spawned by the owner write under the owner's lock once registered as delegates.
|
|
526
|
+
Delegate activity status (`active`, `finished`, or `failed`) is descriptive and does not revoke
|
|
527
|
+
the session's string-array authorization. A registered delegate remains authorized until a
|
|
528
|
+
separate authorization-removal mechanism exists. Finished and failed delegate activity is
|
|
529
|
+
retained as descriptive history, bounded to the 20 most recent terminal entries. A controller,
|
|
530
|
+
a delegate, and an artifact claim are distinct evidence: controller ownership authorizes the
|
|
531
|
+
delivery, delegate registration authorizes a child session, and a claim selects one current
|
|
532
|
+
writer for one artifact. Disarm clears the lock; the End tail is ordered: verify, merge and remove
|
|
516
533
|
worktrees, clear the lock, and only then is the bridge purge-eligible. Repair is one
|
|
517
|
-
idempotent function with two entry points: the `plastic-lock` command (status, fix,
|
|
518
|
-
release, reclaim, delegate) and `/plastic-intent-starting`, so boarding self-heals.
|
|
534
|
+
idempotent function with two entry points: the `plastic-lock` command (`who`, status, fix,
|
|
535
|
+
release, reclaim, delegate) and `/plastic-intent-starting`, so boarding self-heals. `who` is
|
|
536
|
+
read-only and reports the controller, mtime heartbeat, delegates, and claims from durable files.
|
|
537
|
+
This is
|
|
519
538
|
mandatory, not a convention.
|
|
520
539
|
|
|
521
540
|
Solo-mode gate defaults (intent 128): on a confirmed positive solo determination
|
|
@@ -603,6 +622,14 @@ purge: `outcome.md -> INDEX terminal -> savepoint Done -> commit -> disarm (Work
|
|
|
603
622
|
-> Lock.release -> purge) -> QMD reindex`. Running the reindex last keeps the index from
|
|
604
623
|
ever referencing a bridge or lock that disarm is about to remove.
|
|
605
624
|
|
|
625
|
+
`scripts/end-intent` performs this order's disarm step (verify the code worktree is clean,
|
|
626
|
+
then merge/remove worktrees, then clear the lock) as its own step 5, mechanically, since
|
|
627
|
+
intent 188: a session no longer needs a separate one-liner for it, and the script's own
|
|
628
|
+
exit code (0) is the single fact a caller needs that the intent is closed AND its delivery
|
|
629
|
+
lock is gone. A pre-flight lock guard runs before anything is written (refuses a live
|
|
630
|
+
foreign session, reclaims a stale one with an audit line), and a dirty code worktree
|
|
631
|
+
refuses before removal rather than force-discarding uncommitted changes.
|
|
632
|
+
|
|
606
633
|
The post-done access window is lock-bounded: `[INDEX terminal -> Lock.release]`. Through it
|
|
607
634
|
the completing session keeps full read and write access to the terminal directory and no
|
|
608
635
|
purge can fire (108's lock-held keep-guard keeps the bridge while `delivery.lock` exists).
|
|
@@ -621,6 +648,18 @@ always allowed and unbounded (curator reindex, dashboards, and future intents th
|
|
|
621
648
|
its id or chain), so a done intent stays fully readable forever. Intent 93 states this rule;
|
|
622
649
|
intent 112 builds the gate that enforces it.
|
|
623
650
|
|
|
651
|
+
Restore-to-v1 (the owner rule that a completed intent is immutable: a late ruling goes to a
|
|
652
|
+
new `--parent` branch intent, and the completed intent is restored to v1) is performed ONLY by
|
|
653
|
+
`scripts/restore-intent-v1`, run under the maintenance lock. Its prose (the intent narrative,
|
|
654
|
+
checklist.md, outcome.md, spec.md, plan.md) is immutable and reverts to v1; its frontmatter
|
|
655
|
+
graph (`sources`/`chain`) is metadata about OTHER intents, not content of this one, and is
|
|
656
|
+
APPEND-ONLY: it is preserved as the union of the v1 snapshot and the current snapshot, never
|
|
657
|
+
subtracted. A hand-run whole-file `git checkout`/revert of a completed intent is FORBIDDEN,
|
|
658
|
+
because it cannot distinguish prose from graph metadata and silently destroys backlinks written
|
|
659
|
+
after v1 (proven on intent 124: a legitimately accrued chain edge was destroyed by a hand-run
|
|
660
|
+
restore and went undetected for a week). This governs the restore mechanism only; it does not
|
|
661
|
+
loosen terminal immutability itself.
|
|
662
|
+
|
|
624
663
|
Fail-safe lock doctrine (the contract intent 111 implements): the lock system never traps a
|
|
625
664
|
session or burns credits. When a gate cannot verify lock integrity it fails open, degrading
|
|
626
665
|
to advisory (warn) rather than hard-blocking. Repair is orchestrator-driven: on a lock-issue
|
|
@@ -637,4 +676,3 @@ the lock-bounded post-done window with its keep-guard test. Intent 111 owns the
|
|
|
637
676
|
liveness surface, the lock-issue message, orchestrator auto-repair, and the fail-open
|
|
638
677
|
behavior itself. Intent 112 owns the maintenance lock and the immutability gate (it inherits
|
|
639
678
|
fail-open from 111). Intent 4a1b1 owns deep agent stuck-detection and is not superseded.
|
|
640
|
-
|
package/hooks/hooks.json
CHANGED
|
@@ -52,6 +52,11 @@
|
|
|
52
52
|
"type": "command",
|
|
53
53
|
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" savepoint-pre",
|
|
54
54
|
"statusMessage": "Recording stage start..."
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"type": "command",
|
|
58
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook\" links-gate",
|
|
59
|
+
"statusMessage": "Checking Links gate..."
|
|
55
60
|
}
|
|
56
61
|
]
|
|
57
62
|
},
|
package/hooks/links-gate
ADDED
package/package.json
CHANGED
package/scripts/codex-hook
CHANGED
|
@@ -2,18 +2,57 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
#
|
|
5
|
-
# Usage: codex-hook <gate>
|
|
5
|
+
# Usage: codex-hook <gate>
|
|
6
|
+
# file-mutation gates: code-gate | lock-gate | savepoint-pre | gate-check | create-gate | links-gate
|
|
7
|
+
# live-state hooks (intent 199): session-start | check-update | continue |
|
|
8
|
+
# future-intent-check | auto-arm | qmd-search | savepoint
|
|
9
|
+
# shell-tool gates (intent 203): bash-gate | retrieval-gate
|
|
6
10
|
#
|
|
7
|
-
# The Codex input adapter (intent 102). Reads a Codex
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# The Codex input adapter (intent 102, extended by 199 and 203). Reads a Codex
|
|
12
|
+
# hook stdin payload once. Three shapes exist and the gate name alone selects
|
|
13
|
+
# which path runs (Codex's own hooks.json registration guarantees they never
|
|
14
|
+
# cross): the file-mutation gates read session_id at top level and the
|
|
15
|
+
# apply_patch command in tool_input.command [guide Part 4], parse the diff
|
|
16
|
+
# envelope via ApplyPatchEnvelope, and drive Plastic's payload-agnostic Ruby
|
|
17
|
+
# gate/savepoint cores once per file operation, relaying stdout/stderr/exit
|
|
18
|
+
# unchanged. The live-state hooks carry no tool_input at all (they are not tool
|
|
19
|
+
# calls); the dispatcher execs the SAME launcher file Claude already runs for
|
|
20
|
+
# that hook (hooks/<name>), which is already harness-agnostic (resolves
|
|
21
|
+
# ~/.plastic off $HOME, reads only the common stdin fields the guide confirms
|
|
22
|
+
# Codex shares with Claude for these three events), so the body is reused, not
|
|
23
|
+
# rewritten (D3). The shell-tool gates (bash-gate, retrieval-gate) report
|
|
24
|
+
# tool_name: "Bash" with the command in tool_input.command, but MUST NOT be
|
|
25
|
+
# routed through ApplyPatchEnvelope.parse below: a plain shell command has no
|
|
26
|
+
# *** Begin Patch/*** End Patch envelope, so it would parse to an empty op list
|
|
27
|
+
# and hit this file's own `exit 0 if ops.empty?` line, silently allowing every
|
|
28
|
+
# Bash call and reopening the exact hole intent 203 closes. So these two exec
|
|
29
|
+
# the SAME scripts/hook-bash-gate / scripts/hook-retrieval-gate files Claude
|
|
30
|
+
# already runs, unmodified, the identical "drive the body, relay its output"
|
|
31
|
+
# pattern used for the live-state hooks. The one adaptation shared by both the
|
|
32
|
+
# live-state and shell-tool paths is threading the payload's session_id into
|
|
33
|
+
# CLAUDE_CODE_SESSION_ID, since Codex's own process env never carries it, plus
|
|
34
|
+
# a bounded timeout: hooks/check-update backgrounds a real npm network call
|
|
35
|
+
# without redirecting its output away from the inherited stdout/stderr pipes,
|
|
36
|
+
# and Codex invokes hooks synchronously, so without a bound a slow network call
|
|
37
|
+
# could hold this dispatcher's pipe open past the launcher's own (fast) exit.
|
|
13
38
|
require "json"
|
|
39
|
+
require "open3"
|
|
14
40
|
require "rbconfig"
|
|
41
|
+
require "timeout"
|
|
15
42
|
require_relative "lib/apply_patch_envelope"
|
|
16
43
|
require_relative "lib/intent_validator"
|
|
44
|
+
require_relative "lib/links_gate"
|
|
45
|
+
|
|
46
|
+
# Sync stdout so a relayed launcher's stdout and stderr land on the merged
|
|
47
|
+
# pipe in call order. Ruby buffers STDOUT (but not STDERR) when it is not a
|
|
48
|
+
# TTY; without this, `warn err` below can reach the OS pipe before a
|
|
49
|
+
# preceding `print out` is flushed, corrupting the relayed JSON for any
|
|
50
|
+
# caller that merges the dispatcher's own stdout and stderr.
|
|
51
|
+
$stdout.sync = true
|
|
52
|
+
|
|
53
|
+
STATE_HOOKS = %w[session-start check-update continue future-intent-check auto-arm qmd-search savepoint].freeze
|
|
54
|
+
STATE_TIMEOUT = 5
|
|
55
|
+
SHELL_HOOKS = %w[bash-gate retrieval-gate].freeze
|
|
17
56
|
|
|
18
57
|
gate = ARGV[0].to_s
|
|
19
58
|
raw = ($stdin.read rescue nil)
|
|
@@ -22,7 +61,49 @@ payload = (JSON.parse(raw) rescue nil)
|
|
|
22
61
|
exit 0 unless payload.is_a?(Hash)
|
|
23
62
|
|
|
24
63
|
session = payload["session_id"].to_s
|
|
64
|
+
|
|
65
|
+
if STATE_HOOKS.include?(gate)
|
|
66
|
+
launcher = File.expand_path(File.join(__dir__, "..", "hooks", gate))
|
|
67
|
+
cwd = payload["cwd"].to_s
|
|
68
|
+
cwd = Dir.pwd if cwd.empty? || !Dir.exist?(cwd)
|
|
69
|
+
env = { "CLAUDE_CODE_SESSION_ID" => (session.empty? ? nil : session) }
|
|
70
|
+
out, err, status = begin
|
|
71
|
+
Timeout.timeout(STATE_TIMEOUT) { Open3.capture3(env, launcher, stdin_data: raw, chdir: cwd) }
|
|
72
|
+
rescue StandardError
|
|
73
|
+
["", "", nil] # fail open: launcher missing, unexecutable, crashed, or too slow
|
|
74
|
+
end
|
|
75
|
+
print out unless out.to_s.empty?
|
|
76
|
+
warn err unless err.to_s.empty?
|
|
77
|
+
exit(status ? status.exitstatus : 0)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if SHELL_HOOKS.include?(gate)
|
|
81
|
+
# bash-gate / retrieval-gate (intent 203): a shell command has no apply_patch
|
|
82
|
+
# diff envelope, so this branch execs the SAME scripts/hook-bash-gate /
|
|
83
|
+
# scripts/hook-retrieval-gate files Claude already runs, unmodified, and
|
|
84
|
+
# relays their exit code and stderr. It must never fall through to
|
|
85
|
+
# ApplyPatchEnvelope.parse below (see header comment).
|
|
86
|
+
cwd = payload["cwd"].to_s
|
|
87
|
+
cwd = Dir.pwd if cwd.empty? || !Dir.exist?(cwd)
|
|
88
|
+
env = { "CLAUDE_CODE_SESSION_ID" => (session.empty? ? nil : session) }
|
|
89
|
+
script = File.join(__dir__, "hook-#{gate}")
|
|
90
|
+
argv = [script]
|
|
91
|
+
# hook-retrieval-gate reads plastic_home off ARGV[0] (mirrors hooks/retrieval-gate's
|
|
92
|
+
# own launcher, which passes "$HOME/.plastic"); hook-bash-gate takes no args, it
|
|
93
|
+
# resolves Dir.home directly.
|
|
94
|
+
argv << ENV.fetch("PLASTIC_HOME") { File.join(Dir.home, ".plastic") } if gate == "retrieval-gate"
|
|
95
|
+
out, err, status = begin
|
|
96
|
+
Timeout.timeout(STATE_TIMEOUT) { Open3.capture3(env, RbConfig.ruby, *argv, stdin_data: raw, chdir: cwd) }
|
|
97
|
+
rescue StandardError
|
|
98
|
+
["", "", nil] # fail open: script missing, unexecutable, crashed, or too slow
|
|
99
|
+
end
|
|
100
|
+
print out unless out.to_s.empty?
|
|
101
|
+
warn err unless err.to_s.empty?
|
|
102
|
+
exit(status ? status.exitstatus : 0)
|
|
103
|
+
end
|
|
104
|
+
|
|
25
105
|
command = payload.dig("tool_input", "command") || payload.dig("tool_params", "command")
|
|
106
|
+
exit 0 if command.nil? # fail-open: no tool_input at all (unrecognized gate name or non-tool-call payload)
|
|
26
107
|
ops = ApplyPatchEnvelope.parse(command)
|
|
27
108
|
exit 0 if ops.empty? # fail-open: nothing parseable to gate
|
|
28
109
|
|
|
@@ -55,8 +136,11 @@ when "create-gate"
|
|
|
55
136
|
|
|
56
137
|
when "lock-gate"
|
|
57
138
|
# Deny is signalled by JSON on stdout at exit 0. Relay the first deny.
|
|
139
|
+
# "codex" is threaded as the harness (intent 201 D2): hook-lock-gate renders
|
|
140
|
+
# any deny reason with the $plastic-<name> form instead of Claude's default
|
|
141
|
+
# slash.
|
|
58
142
|
ops.each do |o|
|
|
59
|
-
out, _ = run_core("hook-lock-gate", o.path, session)
|
|
143
|
+
out, _ = run_core("hook-lock-gate", o.path, session, "codex")
|
|
60
144
|
if out.include?('"permissionDecision":"deny"') || out.include?('"permissionDecision": "deny"')
|
|
61
145
|
print out
|
|
62
146
|
exit 0
|
|
@@ -91,6 +175,36 @@ when "savepoint-pre"
|
|
|
91
175
|
ops.each { |o| run_core("hook-savepoint-pre", o.path) }
|
|
92
176
|
exit 0
|
|
93
177
|
|
|
178
|
+
when "links-gate"
|
|
179
|
+
# Write-time belt for the PLASTIC.md ## Links contract (intent 192), reusing
|
|
180
|
+
# the SAME LinksGate.decision Claude's hook-links-gate already drives, so
|
|
181
|
+
# both harnesses share one decision function. before_content is the real
|
|
182
|
+
# on-disk file (File.read, or "" when it does not yet exist); after_content
|
|
183
|
+
# is the proposed result, o.added_content.
|
|
184
|
+
#
|
|
185
|
+
# DISCLOSED, NARROWER judgment than Claude's version (intent 198, Decision
|
|
186
|
+
# D3): ApplyPatchEnvelope.parse only ever captures a diff's ADDED lines,
|
|
187
|
+
# never its removed or context lines, so for an Update whose diff is a
|
|
188
|
+
# partial hunk rather than a full-file rewrite, after_content may not be the
|
|
189
|
+
# complete proposed file. This is the same class of disclosed limitation
|
|
190
|
+
# create-gate already carries for Update/Delete/Move (best effort, never a
|
|
191
|
+
# hard crash). links-gate has no PostToolUse backstop to defer to on Claude
|
|
192
|
+
# either, so running this best-effort compare on Update ops too is strictly
|
|
193
|
+
# more coverage than the prior total fail-open.
|
|
194
|
+
ops.each do |o|
|
|
195
|
+
next unless intent_file?(o.path)
|
|
196
|
+
|
|
197
|
+
before_content = File.exist?(o.path) ? File.read(o.path) : ""
|
|
198
|
+
plastic_home = ENV.fetch("PLASTIC_HOME") { File.join(Dir.home, ".plastic") }
|
|
199
|
+
reason = LinksGate.decision(file_path: o.path, before_content: before_content,
|
|
200
|
+
after_content: o.added_content.to_s, plastic_home: plastic_home)
|
|
201
|
+
next unless reason
|
|
202
|
+
|
|
203
|
+
$stderr.puts reason
|
|
204
|
+
exit 2
|
|
205
|
+
end
|
|
206
|
+
exit 0
|
|
207
|
+
|
|
94
208
|
else
|
|
95
209
|
exit 0
|
|
96
210
|
end
|