@zalom/plastic 2.0.0-alpha.9 → 2.0.2
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.md +13 -136
- package/README.md +357 -133
- package/agents/plastic-enforcer.md +21 -16
- package/agents/plastic-executor.md +15 -4
- package/agents/plastic-node-research.md +30 -0
- package/agents/plastic-node-verify.md +28 -0
- package/agents/plastic-node-work.md +33 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/assets/plastic-logo.svg +1 -0
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +43 -6
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +8 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/test +24 -4
- package/bin/verify-change +345 -0
- package/config_asks.yml +4 -4
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
- package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
- package/docs/help/completion-and-done.md +53 -0
- package/docs/help/human-report-contract.md +152 -0
- package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
- package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +11 -13
- package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
- package/{skills/conventions/references → docs/help}/roadmaps.md +5 -4
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/hooks/call-budget +4 -0
- package/hooks/hooks.json +24 -0
- package/hooks/message-display +55 -2
- package/hooks/session-start +5 -1
- package/hooks/statusline +32 -27
- package/hooks/stop +5 -0
- package/package.json +5 -3
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +267 -16
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +680 -39
- package/scripts/end-intent +378 -56
- package/scripts/exec-worktree +5 -5
- package/scripts/file-session-intent +2 -1
- package/scripts/graph-measure +249 -0
- package/scripts/hook-call-budget +222 -0
- package/scripts/hook-capture +24 -123
- package/scripts/hook-close +2 -1
- package/scripts/hook-message-display +7 -0
- package/scripts/hook-record +24 -16
- package/scripts/hook-savepoint +27 -3
- package/scripts/hook-session-start +359 -321
- package/scripts/hook-stop +58 -0
- package/scripts/index-projection +74 -0
- package/scripts/insight-append +17 -4
- package/scripts/install.rb +9 -7
- package/scripts/lib/action_graph_shim.rb +279 -0
- package/scripts/lib/active_delivery.rb +105 -0
- package/scripts/lib/agent_models.rb +63 -25
- package/scripts/lib/arm.rb +74 -91
- package/scripts/lib/atomic_write.rb +31 -0
- package/scripts/lib/backup.rb +65 -0
- package/scripts/lib/cli/command.rb +92 -0
- package/scripts/lib/cli/commands/auto.rb +18 -0
- package/scripts/lib/cli/commands/auto_brief.rb +44 -0
- package/scripts/lib/cli/commands/auto_lock.rb +111 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +68 -0
- package/scripts/lib/cli/commands/backup.rb +43 -0
- package/scripts/lib/cli/commands/checkout.rb +25 -0
- package/scripts/lib/cli/commands/continue.rb +66 -0
- package/scripts/lib/cli/commands/doctor.rb +81 -0
- package/scripts/lib/cli/commands/feedback.rb +40 -0
- package/scripts/lib/cli/commands/help.rb +69 -0
- package/scripts/lib/cli/commands/hook.rb +32 -0
- package/scripts/lib/cli/commands/index.rb +23 -0
- package/scripts/lib/cli/commands/install.rb +21 -0
- package/scripts/lib/cli/commands/installer_verb.rb +37 -0
- package/scripts/lib/cli/commands/intent.rb +19 -0
- package/scripts/lib/cli/commands/intent_answer.rb +37 -0
- package/scripts/lib/cli/commands/intent_command.rb +58 -0
- package/scripts/lib/cli/commands/intent_end.rb +75 -0
- package/scripts/lib/cli/commands/intent_new.rb +62 -0
- package/scripts/lib/cli/commands/intent_note.rb +43 -0
- package/scripts/lib/cli/commands/intent_rule.rb +36 -0
- package/scripts/lib/cli/commands/intent_show.rb +32 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +79 -0
- package/scripts/lib/cli/commands/intent_verify.rb +26 -0
- package/scripts/lib/cli/commands/migrate.rb +16 -0
- package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
- package/scripts/lib/cli/commands/next.rb +54 -0
- package/scripts/lib/cli/commands/project.rb +19 -0
- package/scripts/lib/cli/commands/project_links.rb +46 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +95 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +35 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +77 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +65 -0
- package/scripts/lib/cli/commands/rollback.rb +20 -0
- package/scripts/lib/cli/commands/search.rb +60 -0
- package/scripts/lib/cli/commands/session.rb +18 -0
- package/scripts/lib/cli/commands/session_commit.rb +42 -0
- package/scripts/lib/cli/commands/session_handoff.rb +34 -0
- package/scripts/lib/cli/commands/session_summary.rb +35 -0
- package/scripts/lib/cli/commands/status.rb +68 -0
- package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
- package/scripts/lib/cli/commands/sync.rb +55 -0
- package/scripts/lib/cli/commands/uninstall.rb +20 -0
- package/scripts/lib/cli/commands/update.rb +20 -0
- package/scripts/lib/cli/commands/version.rb +53 -0
- package/scripts/lib/cli/frontier.rb +86 -0
- package/scripts/lib/cli/intent_progress.rb +36 -0
- package/scripts/lib/cli/legacy.rb +80 -0
- package/scripts/lib/cli/output.rb +137 -0
- package/scripts/lib/cli/scope.rb +134 -0
- package/scripts/lib/cli/table.rb +65 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +198 -0
- package/scripts/lib/compact_instructions.rb +13 -5
- package/scripts/lib/core_integrity.rb +71 -0
- package/scripts/lib/dashboard_screen.rb +40 -0
- package/scripts/lib/data_boundary.rb +132 -0
- package/scripts/lib/day_summary.rb +23 -14
- package/scripts/lib/doctor_core.rb +114 -38
- package/scripts/lib/doctor_session_ledger.rb +5 -53
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_edges.rb +137 -0
- package/scripts/lib/graph_file.rb +246 -0
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +409 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +413 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +98 -0
- package/scripts/lib/guarded_append.rb +155 -0
- package/scripts/lib/handoff.rb +40 -13
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +27 -3
- package/scripts/lib/hook_replay.rb +235 -0
- package/scripts/lib/index_entry.rb +62 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +543 -71
- package/scripts/lib/intent_screen.rb +4 -4
- package/scripts/lib/intent_screen_ansi.rb +73 -12
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/lock.rb +10 -11
- package/scripts/lib/message_display.rb +350 -54
- package/scripts/lib/meter_watch.rb +185 -0
- package/scripts/lib/node_file.rb +234 -0
- package/scripts/lib/node_ids.rb +99 -0
- package/scripts/lib/node_input.rb +921 -0
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +386 -0
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/node_return.rb +204 -0
- package/scripts/lib/node_worktree.rb +337 -0
- package/scripts/lib/outcome_report.rb +440 -0
- package/scripts/lib/preflight.rb +4 -6
- package/scripts/lib/project_config.rb +46 -0
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/ready_set.rb +462 -0
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +18 -0
- package/scripts/lib/report_screen.rb +1371 -47
- package/scripts/lib/rlm/corpus.rb +13 -0
- package/scripts/lib/rlm/probe.rb +29 -0
- package/scripts/lib/rlm/query.rb +22 -0
- package/scripts/lib/roadmap_graph.rb +210 -0
- package/scripts/lib/roadmap_migration.rb +95 -0
- package/scripts/lib/roadmap_queue.rb +170 -12
- package/scripts/lib/roadmap_render.rb +150 -0
- package/scripts/lib/roadmap_savepoint.rb +64 -14
- package/scripts/lib/runner_absorb.rb +703 -0
- package/scripts/lib/runner_answer.rb +206 -0
- package/scripts/lib/runner_core.rb +194 -0
- package/scripts/lib/runner_dispatch.rb +526 -0
- package/scripts/lib/runner_policy.rb +191 -0
- package/scripts/lib/runner_proposals.rb +275 -0
- package/scripts/lib/runner_rewind.rb +201 -0
- package/scripts/lib/runner_sweep.rb +231 -0
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +141 -19
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/screen_paint.rb +365 -28
- package/scripts/lib/screens/dashboard.rb +20 -0
- package/scripts/lib/screens/plan.rb +18 -0
- package/scripts/lib/screens/roadmap.rb +15 -0
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +67 -19
- package/scripts/lib/session_ledger.rb +48 -4
- package/scripts/lib/session_usage.rb +190 -0
- package/scripts/lib/sqlite.rb +22 -0
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/store_discovery.rb +7 -6
- package/scripts/lib/store_layout.rb +54 -0
- package/scripts/lib/store_provisioning.rb +2 -1
- package/scripts/lib/store_sync.rb +85 -0
- package/scripts/lib/stores_move.rb +93 -0
- package/scripts/lib/untouched_scaffold.rb +51 -0
- package/scripts/lib/verify_intent.rb +36 -8
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/lib/worktree.rb +27 -32
- package/scripts/lib/worktree_sweep.rb +6 -5
- package/scripts/link-suggest +2 -1
- package/scripts/meter-watch +57 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +4 -13
- package/scripts/node-input +92 -0
- package/scripts/node-run +225 -0
- package/scripts/node-transition +291 -0
- package/scripts/outcome-report +74 -0
- package/scripts/plastic-lock +65 -60
- package/scripts/project-links +6 -21
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +53 -9
- package/scripts/ready-set +126 -0
- package/scripts/release-check +123 -0
- package/scripts/report-screen +177 -16
- package/scripts/roadmap-graph +125 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/rollback.rb +5 -1
- package/scripts/runner +581 -0
- package/scripts/savepoint-note +11 -9
- package/scripts/session-commit +2 -1
- package/scripts/session-usage +56 -0
- package/scripts/skill-lint +115 -6
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +31 -4
- package/scripts/validate-work-graph +39 -0
- package/scripts/verify-intent +3 -2
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +7 -7
- package/templates/config.yml +16 -9
- package/templates/dashboard-screen.md +22 -0
- package/templates/display-fixture.md +21 -0
- package/templates/graph.md +16 -0
- package/templates/index.md +1 -1
- package/templates/intent-screen.md +1 -1
- package/templates/node-decision.md +11 -0
- package/templates/node-research.md +13 -0
- package/templates/node-verify.md +13 -0
- package/templates/node-work.md +22 -0
- package/templates/outcome.md +8 -3
- package/templates/project.yml +1 -1
- package/templates/render.css +10 -0
- package/templates/report-plan.md +15 -0
- package/templates/report-roadmap-delivered.md +10 -0
- package/templates/report-roadmap-plan.md +9 -0
- package/templates/report-roadmap-state.md +9 -0
- package/templates/report-state.md +1 -1
- package/templates/roadmap.md +13 -0
- package/bin/plastic.js +0 -70
- package/scripts/lib/bridge.rb +0 -116
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -296
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -66
- package/skills/auto/references/human-report-contract.md +0 -78
- package/skills/conventions/SKILL.md +0 -29
- package/skills/conventions/references/completion-and-done.md +0 -43
- package/skills/dashboard/SKILL.md +0 -169
- package/skills/dashboard/evals/evals.json +0 -38
- package/skills/dashboard/references/classification.md +0 -22
- package/skills/dashboard/templates/dashboard-global.md +0 -20
- package/skills/dashboard/templates/dashboard-project.md +0 -19
- package/skills/direct/SKILL.md +0 -66
- package/skills/direct/references/request-signals.md +0 -59
- package/skills/doctor/SKILL.md +0 -299
- package/skills/doctor/report.md +0 -102
- package/skills/feedback/SKILL.md +0 -98
- package/skills/feedback/references/transport-and-privacy.md +0 -65
- package/skills/feedback/report.md +0 -36
- package/skills/install/SKILL.md +0 -217
- package/skills/intent-continuing/SKILL.md +0 -154
- package/skills/intent-continuing/references/board-fill.md +0 -43
- package/skills/intent-continuing/references/boarding-matrix.md +0 -34
- package/skills/intent-continuing/references/context-management.md +0 -28
- package/skills/intent-continuing/references/liveness-ranking.md +0 -57
- package/skills/intent-creating/SKILL.md +0 -164
- package/skills/intent-creating/evals/evals.json +0 -72
- package/skills/intent-creating/references/lifecycle.md +0 -81
- package/skills/intent-creating/references/wikilinks.md +0 -8
- package/skills/intent-ending/SKILL.md +0 -176
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -170
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -42
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -130
- package/skills/intent-speccing/evals/evals.json +0 -126
- package/skills/intent-speccing/references/design-principles.md +0 -44
- package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
- package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
- package/skills/project-creating/SKILL.md +0 -162
- package/skills/project-creating/references/hubs-projects.md +0 -55
- package/skills/project-creating/references/project-scaffolding.md +0 -97
- package/skills/releasing/SKILL.md +0 -337
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -66
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -64
- package/skills/roadmap/references/file-format.md +0 -124
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -65
- package/skills/tutorial/evals/evals.json +0 -186
- package/skills/uninstall/SKILL.md +0 -75
- package/skills/update/SKILL.md +0 -126
- /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
- /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "json"
|
|
5
6
|
require "yaml"
|
|
6
7
|
require "fileutils"
|
|
7
8
|
require "digest"
|
|
8
9
|
require "time"
|
|
10
|
+
require "tmpdir"
|
|
9
11
|
require_relative "hook_registry"
|
|
10
12
|
require_relative "agent_models"
|
|
11
13
|
require_relative "harness_text"
|
|
12
14
|
require_relative "compact_instructions"
|
|
15
|
+
require_relative "engine_permissions"
|
|
16
|
+
require_relative "qmd_sync"
|
|
13
17
|
|
|
14
18
|
# Shared installer machinery, instantiable with injected package root / store / agent
|
|
15
19
|
# map so the verb scripts (install/update/uninstall/rollback) and their tests can run
|
|
@@ -43,23 +47,21 @@ class InstallerCore
|
|
|
43
47
|
CLAUDE_SECTION_END = "<!-- END PLASTIC COMPACT -->"
|
|
44
48
|
CLAUDE_SECTION_RE = /^<!-- BEGIN PLASTIC COMPACT.*?-->\n.*?\n<!-- END PLASTIC COMPACT -->\n?/m
|
|
45
49
|
|
|
46
|
-
# Curated essentials plus a pointer to ~/.plastic/PLASTIC.md and the
|
|
47
|
-
#
|
|
48
|
-
# a shared file Codex merges from multiple sources,
|
|
49
|
-
# hand-curated pointer rather than embedding the core
|
|
50
|
-
# because it only ever points, never duplicates.
|
|
50
|
+
# Curated essentials plus a pointer to ~/.plastic/PLASTIC.md and the help topics
|
|
51
|
+
# `plastic help TOPIC` prints, injected into ~/.codex/AGENTS.md. Not a slice of
|
|
52
|
+
# PLASTIC.md itself: AGENTS.md is a shared file Codex merges from multiple sources,
|
|
53
|
+
# so this block stays a small, hand-curated pointer rather than embedding the core
|
|
54
|
+
# wholesale, and it never drifts because it only ever points, never duplicates.
|
|
51
55
|
CODEX_AGENTS_MD_BODY = <<~MD.freeze
|
|
52
|
-
Plastic is installed for this agent. Plastic is intent-driven state management:
|
|
53
|
-
|
|
54
|
-
straight to code.
|
|
56
|
+
Plastic is installed for this agent. Plastic is intent-driven state management: work runs
|
|
57
|
+
in one of three modes, direct, thinking, or auto (a team drives the runner loop:
|
|
58
|
+
`runner step`, `status`, `answer`). Do not jump straight to code.
|
|
55
59
|
|
|
56
60
|
Standing rules:
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- Operational procedures are
|
|
61
|
-
plastic-<name>/SKILL.md). Invoke one explicitly as $plastic-<name> (for example
|
|
62
|
-
$plastic-doctor), or let Codex pick one implicitly by matching its description.
|
|
61
|
+
- The command line is ~/.plastic/PLASTIC.md. Read it and follow it exactly. The
|
|
62
|
+
conventions are chapters `plastic help TOPIC` prints on demand. Both are
|
|
63
|
+
generated and overwritten on Plastic updates, so never edit them.
|
|
64
|
+
- Operational procedures are the `plastic` command line itself, run directly.
|
|
63
65
|
- Intents, specs, plans, checklists, and outcomes live under ~/.plastic/, never in
|
|
64
66
|
the project tree.
|
|
65
67
|
|
|
@@ -69,6 +71,17 @@ class InstallerCore
|
|
|
69
71
|
|
|
70
72
|
attr_reader :package_root, :plastic_home, :version, :agents
|
|
71
73
|
|
|
74
|
+
def store_layout_compatible?(target)
|
|
75
|
+
!Plastic::StoreLayout.moved?(plastic_home) || semver_compare(target, "2.0.0-alpha.28") >= 0
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def refuse_incompatible_store_layout(target)
|
|
79
|
+
return if store_layout_compatible?(target)
|
|
80
|
+
|
|
81
|
+
warn "Plastic #{target} cannot read stores/ in #{plastic_home}. Keep a version that supports this layout."
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
72
85
|
def initialize(package_root:, plastic_home: DEFAULT_PLASTIC_HOME, version: nil, agents: DEFAULT_AGENTS)
|
|
73
86
|
@package_root = package_root
|
|
74
87
|
@plastic_home = plastic_home
|
|
@@ -253,7 +266,10 @@ class InstallerCore
|
|
|
253
266
|
|
|
254
267
|
# --- Distribution phase ---
|
|
255
268
|
|
|
256
|
-
|
|
269
|
+
# `tmp_dirs:` defaults to the real system tmp directory plus `/tmp` (where a
|
|
270
|
+
# retired session-state leftover would sit); a test overrides it (often to
|
|
271
|
+
# `[]`) so the removal never scans the real filesystem's tmp directories.
|
|
272
|
+
def distribute(mode, tmp_dirs: [Dir.tmpdir, "/tmp"].uniq)
|
|
257
273
|
puts " \u{1f4e6} #{mode == :update ? "Updating" : "Installing"} core files to #{plastic_home}"
|
|
258
274
|
|
|
259
275
|
manifest_path = File.join(plastic_home, "manifest.json")
|
|
@@ -295,6 +311,8 @@ class InstallerCore
|
|
|
295
311
|
write_manifest(global_files, manifest_path)
|
|
296
312
|
|
|
297
313
|
pruned = prune_removed_files(old_files - global_files, root: plastic_home)
|
|
314
|
+
removed = remove_retired_session_state(plastic_home: plastic_home, tmp_dirs: tmp_dirs)
|
|
315
|
+
puts " \u{1f9f9} #{removed} retired session state file(s) removed" if removed.positive?
|
|
298
316
|
|
|
299
317
|
puts " \u{2705} Core files synced (v#{version})#{pruned.positive? ? ", #{pruned} stale file(s) pruned" : ""}"
|
|
300
318
|
end
|
|
@@ -331,12 +349,50 @@ class InstallerCore
|
|
|
331
349
|
end
|
|
332
350
|
end
|
|
333
351
|
|
|
352
|
+
# Intent 331a (D6/R7): a screen kind file (scripts/lib/screens/<kind>.rb)
|
|
353
|
+
# must reach an installed ~/.plastic the same way a new template or hook
|
|
354
|
+
# does - glob-derived, so "add a file, not a diff" is actually true for an
|
|
355
|
+
# installed Plastic, not just an in-repo one. This repo ships none yet;
|
|
356
|
+
# the glob answers {} until one exists.
|
|
357
|
+
def screen_files
|
|
358
|
+
Dir.glob(File.join(package_root, "scripts", "lib", "screens", "*.rb")).each_with_object({}) do |path, acc|
|
|
359
|
+
next unless File.file?(path)
|
|
360
|
+
|
|
361
|
+
rel = File.join("scripts", "lib", "screens", File.basename(path))
|
|
362
|
+
acc[rel] = rel
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# The command line (intent 363). Glob-derived for the reason screen_files is:
|
|
367
|
+
# a command is one row in scripts/lib/cli/table.rb plus one file, and a
|
|
368
|
+
# hand-written manifest would make it two files and a diff. The launcher
|
|
369
|
+
# bin/plastic requires scripts/lib/cli.rb by a relative path, so the whole
|
|
370
|
+
# subtree travels together or an installed copy LoadErrors on the first call.
|
|
371
|
+
def cli_files
|
|
372
|
+
Dir.glob(File.join(package_root, "scripts", "lib", "{cli,rlm}", "**", "*.rb")).each_with_object({}) do |path, acc|
|
|
373
|
+
rel = path.sub("#{package_root}/", "")
|
|
374
|
+
acc[rel] = rel
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# `plastic help TOPIC` (intent 372, family 4) reads docs/help/TOPIC.md from
|
|
379
|
+
# package_root the same way it reads a command's file, so the chapter has to
|
|
380
|
+
# ship the same way a template does: glob-derived, one file added and it
|
|
381
|
+
# installs with no diff here.
|
|
382
|
+
def help_files
|
|
383
|
+
Dir.glob(File.join(package_root, "docs", "help", "*.md")).each_with_object({}) do |path, acc|
|
|
384
|
+
rel = File.join("docs", "help", File.basename(path))
|
|
385
|
+
acc[rel] = rel
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
334
389
|
# Files copied into ~/.plastic on install/update. Every verb script + the shared lib
|
|
335
390
|
# must be here so the installed ~/.plastic/scripts copy is self-complete (sync-guarded
|
|
336
|
-
# by install_sync_test). The templates
|
|
337
|
-
# rest stays a hand-written literal.
|
|
391
|
+
# by install_sync_test). The templates and screen-kind halves are glob-derived
|
|
392
|
+
# (template_files, screen_files above); the rest stays a hand-written literal.
|
|
338
393
|
def core_files
|
|
339
|
-
hand_registered_files.merge(template_files).merge(hook_files)
|
|
394
|
+
hand_registered_files.merge(template_files).merge(hook_files).merge(screen_files).merge(cli_files)
|
|
395
|
+
.merge(help_files)
|
|
340
396
|
end
|
|
341
397
|
|
|
342
398
|
def hand_registered_files
|
|
@@ -357,13 +413,23 @@ class InstallerCore
|
|
|
357
413
|
"scripts/lib/agent_models.rb" => "scripts/lib/agent_models.rb",
|
|
358
414
|
"scripts/lib/config_asks.rb" => "scripts/lib/config_asks.rb",
|
|
359
415
|
"scripts/lib/release_guard.rb" => "scripts/lib/release_guard.rb",
|
|
360
|
-
"scripts/lib/
|
|
416
|
+
"scripts/lib/index_entry.rb" => "scripts/lib/index_entry.rb",
|
|
417
|
+
"scripts/lib/project_config.rb" => "scripts/lib/project_config.rb",
|
|
361
418
|
"scripts/lib/savepoint.rb" => "scripts/lib/savepoint.rb",
|
|
419
|
+
"scripts/lib/untouched_scaffold.rb" => "scripts/lib/untouched_scaffold.rb",
|
|
420
|
+
"scripts/lib/guarded_append.rb" => "scripts/lib/guarded_append.rb",
|
|
421
|
+
"scripts/lib/node_ledger.rb" => "scripts/lib/node_ledger.rb",
|
|
422
|
+
"scripts/lib/node_input_compatibility.rb" => "scripts/lib/node_input_compatibility.rb",
|
|
423
|
+
"scripts/lib/node_progress.rb" => "scripts/lib/node_progress.rb",
|
|
424
|
+
"scripts/node-transition" => "scripts/node-transition",
|
|
362
425
|
"scripts/lib/arm.rb" => "scripts/lib/arm.rb",
|
|
363
426
|
"scripts/lib/lock.rb" => "scripts/lib/lock.rb",
|
|
364
427
|
"scripts/plastic-lock" => "scripts/plastic-lock",
|
|
365
428
|
"scripts/lib/hook_registry.rb" => "scripts/lib/hook_registry.rb",
|
|
366
429
|
"scripts/lib/compact_instructions.rb" => "scripts/lib/compact_instructions.rb",
|
|
430
|
+
"scripts/lib/version_number.rb" => "scripts/lib/version_number.rb",
|
|
431
|
+
"scripts/lib/cli.rb" => "scripts/lib/cli.rb",
|
|
432
|
+
"bin/plastic" => "bin/plastic",
|
|
367
433
|
"scripts/agent-report" => "scripts/agent-report",
|
|
368
434
|
"scripts/lib/insights.rb" => "scripts/lib/insights.rb",
|
|
369
435
|
"scripts/insight-append" => "scripts/insight-append",
|
|
@@ -379,6 +445,14 @@ class InstallerCore
|
|
|
379
445
|
"scripts/lib/intent_validator.rb" => "scripts/lib/intent_validator.rb",
|
|
380
446
|
"scripts/lib/graph_rebuild.rb" => "scripts/lib/graph_rebuild.rb",
|
|
381
447
|
"scripts/lib/store_discovery.rb" => "scripts/lib/store_discovery.rb",
|
|
448
|
+
"scripts/lib/store_layout.rb" => "scripts/lib/store_layout.rb",
|
|
449
|
+
"scripts/lib/stores_move.rb" => "scripts/lib/stores_move.rb",
|
|
450
|
+
"scripts/lib/sqlite.rb" => "scripts/lib/sqlite.rb",
|
|
451
|
+
"scripts/lib/search_index.rb" => "scripts/lib/search_index.rb",
|
|
452
|
+
"scripts/lib/store_sync.rb" => "scripts/lib/store_sync.rb",
|
|
453
|
+
"scripts/lib/work_graph.rb" => "scripts/lib/work_graph.rb",
|
|
454
|
+
"scripts/lib/reference_archive.rb" => "scripts/lib/reference_archive.rb",
|
|
455
|
+
"scripts/lib/backup.rb" => "scripts/lib/backup.rb",
|
|
382
456
|
"scripts/lib/frontmatter_writer.rb" => "scripts/lib/frontmatter_writer.rb",
|
|
383
457
|
"scripts/lib/links_projection.rb" => "scripts/lib/links_projection.rb",
|
|
384
458
|
"scripts/lib/links_section.rb" => "scripts/lib/links_section.rb",
|
|
@@ -421,8 +495,11 @@ class InstallerCore
|
|
|
421
495
|
"scripts/verify-intent" => "scripts/verify-intent",
|
|
422
496
|
"scripts/lib/exec_worktree.rb" => "scripts/lib/exec_worktree.rb",
|
|
423
497
|
"scripts/exec-worktree" => "scripts/exec-worktree",
|
|
498
|
+
"scripts/lib/session_usage.rb" => "scripts/lib/session_usage.rb",
|
|
499
|
+
"scripts/session-usage" => "scripts/session-usage",
|
|
424
500
|
"scripts/doctor.rb" => "scripts/doctor.rb",
|
|
425
501
|
"scripts/lib/doctor_core.rb" => "scripts/lib/doctor_core.rb",
|
|
502
|
+
"scripts/lib/hook_replay.rb" => "scripts/lib/hook_replay.rb",
|
|
426
503
|
"scripts/lib/rule_catalog.rb" => "scripts/lib/rule_catalog.rb",
|
|
427
504
|
"scripts/lib/doctor_exclusions.rb" => "scripts/lib/doctor_exclusions.rb",
|
|
428
505
|
"scripts/lib/doctor_session_ledger.rb" => "scripts/lib/doctor_session_ledger.rb",
|
|
@@ -454,22 +531,157 @@ class InstallerCore
|
|
|
454
531
|
"scripts/lib/intent_screen_ansi.rb" => "scripts/lib/intent_screen_ansi.rb",
|
|
455
532
|
"scripts/lib/screen_paint.rb" => "scripts/lib/screen_paint.rb",
|
|
456
533
|
"scripts/lib/message_display.rb" => "scripts/lib/message_display.rb",
|
|
534
|
+
# Intent 331d (A1): scripts/dashboard.rb require_relatives this lib
|
|
535
|
+
# directly; templates/dashboard-screen.md and scripts/lib/screens/
|
|
536
|
+
# dashboard.rb are glob-derived (template_files, screen_files above)
|
|
537
|
+
# and need no entry here.
|
|
538
|
+
"scripts/lib/dashboard_screen.rb" => "scripts/lib/dashboard_screen.rb",
|
|
457
539
|
"scripts/hook-message-display" => "scripts/hook-message-display",
|
|
540
|
+
# Intent 334 (G1): the node file and graph.md library, plus its
|
|
541
|
+
# validator and CLI (327 D40/D41).
|
|
542
|
+
"scripts/lib/graph_edges.rb" => "scripts/lib/graph_edges.rb",
|
|
543
|
+
"scripts/lib/node_file.rb" => "scripts/lib/node_file.rb",
|
|
544
|
+
"scripts/lib/atomic_write.rb" => "scripts/lib/atomic_write.rb",
|
|
545
|
+
"scripts/lib/graph_file.rb" => "scripts/lib/graph_file.rb",
|
|
546
|
+
"scripts/lib/work_graph_validator.rb" => "scripts/lib/work_graph_validator.rb",
|
|
547
|
+
"scripts/validate-work-graph" => "scripts/validate-work-graph",
|
|
548
|
+
# Intent 335a: every id an intent has ever seen, so a deleted node's id
|
|
549
|
+
# is never reissued to a new node with the dead one's ledger history.
|
|
550
|
+
"scripts/lib/node_ids.rb" => "scripts/lib/node_ids.rb",
|
|
551
|
+
# Intent 339 (G6): the generated outcome.md library and its CLI.
|
|
552
|
+
"scripts/lib/outcome_report.rb" => "scripts/lib/outcome_report.rb",
|
|
553
|
+
"scripts/outcome-report" => "scripts/outcome-report",
|
|
554
|
+
# Intent 336 (G3): ReadySet, the one function that says what may run
|
|
555
|
+
# next, and its CLI. node-transition requires the lib as of n5;
|
|
556
|
+
# registered here (rather than waiting for n8's own CLI/doc unit) so
|
|
557
|
+
# test/install_sync_test.rb stays green across every intermediate unit.
|
|
558
|
+
"scripts/lib/ready_set.rb" => "scripts/lib/ready_set.rb",
|
|
559
|
+
"scripts/ready-set" => "scripts/ready-set",
|
|
560
|
+
# Intent 337 (G4): the roadmap graph model and its tree renderer,
|
|
561
|
+
# registered as they land (test/installer_core_test.rb requires every
|
|
562
|
+
# scripts/lib file to be manifest-covered as soon as it exists, not
|
|
563
|
+
# only once the CLI that ships it arrives at n4).
|
|
564
|
+
"scripts/lib/roadmap_graph.rb" => "scripts/lib/roadmap_graph.rb",
|
|
565
|
+
"scripts/lib/graph_tree.rb" => "scripts/lib/graph_tree.rb",
|
|
566
|
+
"scripts/lib/index_projection.rb" => "scripts/lib/index_projection.rb",
|
|
567
|
+
"scripts/lib/roadmap_render.rb" => "scripts/lib/roadmap_render.rb",
|
|
568
|
+
"scripts/lib/roadmap_migration.rb" => "scripts/lib/roadmap_migration.rb",
|
|
569
|
+
"scripts/roadmap-graph" => "scripts/roadmap-graph",
|
|
570
|
+
"scripts/index-projection" => "scripts/index-projection",
|
|
571
|
+
# Intent 338 (G5): the node input command - the trust-boundary wrapper,
|
|
572
|
+
# the five-block gatherer/assembler, and the CLI 340's runner calls.
|
|
573
|
+
"scripts/lib/data_boundary.rb" => "scripts/lib/data_boundary.rb",
|
|
574
|
+
"scripts/lib/node_input.rb" => "scripts/lib/node_input.rb",
|
|
575
|
+
"scripts/node-input" => "scripts/node-input",
|
|
576
|
+
# Intent 342 (G9): the backward shim that presents actions/*.md as a
|
|
577
|
+
# node graph for any legacy intent, so WorkGraphValidator can require
|
|
578
|
+
# it without going red on contact with install_sync_test.
|
|
579
|
+
"scripts/lib/action_graph_shim.rb" => "scripts/lib/action_graph_shim.rb",
|
|
580
|
+
# Intent 340 (G7, n1): the runner command - the subcommand table over
|
|
581
|
+
# the declared node graph, its shared context (RunnerCore), and the
|
|
582
|
+
# installed-core integrity check (CoreIntegrity) the runner's trust
|
|
583
|
+
# boundary and doctor will both call.
|
|
584
|
+
"scripts/runner" => "scripts/runner",
|
|
585
|
+
"scripts/lib/runner_core.rb" => "scripts/lib/runner_core.rb",
|
|
586
|
+
"scripts/lib/core_integrity.rb" => "scripts/lib/core_integrity.rb",
|
|
587
|
+
# Intent 340 (G7, n2): the merge abort, the reclaim, and the extension -
|
|
588
|
+
# the first thing every `step` does, routed from scripts/runner's `sweep`
|
|
589
|
+
# verb.
|
|
590
|
+
"scripts/lib/runner_sweep.rb" => "scripts/lib/runner_sweep.rb",
|
|
591
|
+
# Intent 340 (G7, n3): a work node's own git worktree, cut from the
|
|
592
|
+
# intent branch tip, merged back into the intent branch, and swept once
|
|
593
|
+
# its node is terminal.
|
|
594
|
+
"scripts/lib/node_worktree.rb" => "scripts/lib/node_worktree.rb",
|
|
595
|
+
# Intent 340 (G7, n4): the return schema (NodeReturn) and the six-check
|
|
596
|
+
# gate (RunnerAbsorb) that turns one executor return into exactly one
|
|
597
|
+
# node ledger transition, required lazily by scripts/runner's `step`.
|
|
598
|
+
"scripts/lib/node_return.rb" => "scripts/lib/node_return.rb",
|
|
599
|
+
"scripts/lib/runner_absorb.rb" => "scripts/lib/runner_absorb.rb",
|
|
600
|
+
# Intent 340 (G7, n5): validation, policy, leases and the dispatch plan
|
|
601
|
+
# - RunnerPolicy (the kind table) and RunnerDispatch, required lazily
|
|
602
|
+
# by scripts/runner's `step`.
|
|
603
|
+
"scripts/lib/runner_policy.rb" => "scripts/lib/runner_policy.rb",
|
|
604
|
+
"scripts/lib/runner_dispatch.rb" => "scripts/lib/runner_dispatch.rb",
|
|
605
|
+
# Intent 340b (G7c, n1): the harness seam - HarnessAdapter, required
|
|
606
|
+
# by scripts/lib/runner_dispatch.rb and by scripts/runner's `step`
|
|
607
|
+
# directly.
|
|
608
|
+
"scripts/lib/harness_adapter.rb" => "scripts/lib/harness_adapter.rb",
|
|
609
|
+
# Intent 340b (G7c, n6): the Codex leg - CodexAdapter (the `codex exec`
|
|
610
|
+
# argv, the sandbox per kind, and the bounded subprocess) and
|
|
611
|
+
# scripts/node-run, the CLI that runs one node's whole attempt over
|
|
612
|
+
# it and writes only a return file, never a ledger transition.
|
|
613
|
+
"scripts/lib/codex_adapter.rb" => "scripts/lib/codex_adapter.rb",
|
|
614
|
+
"scripts/node-run" => "scripts/node-run",
|
|
615
|
+
# Intent 340b (G7c, n3): the engine deny rule - the frozen permissions.deny
|
|
616
|
+
# entry list, merged into settings.json at install and removed surgically
|
|
617
|
+
# at uninstall.
|
|
618
|
+
"scripts/lib/engine_permissions.rb" => "scripts/lib/engine_permissions.rb",
|
|
619
|
+
# Intent 340b (G7c, n4): the Stop gate, the shared ActiveDelivery walk
|
|
620
|
+
# it and the PreCompact hand-off both call, and hook-stop, the Stop
|
|
621
|
+
# hook body scripts/hook-stop's launcher (hooks/stop) relays into.
|
|
622
|
+
"scripts/lib/stop_gate.rb" => "scripts/lib/stop_gate.rb",
|
|
623
|
+
"scripts/lib/active_delivery.rb" => "scripts/lib/active_delivery.rb",
|
|
624
|
+
"scripts/hook-stop" => "scripts/hook-stop",
|
|
625
|
+
# Intent 355 (n2): the call budget PreToolUse hook (RunnerPolicy.call_cap
|
|
626
|
+
# is its cap table, above); its launcher (hooks/call-budget) ships via
|
|
627
|
+
# hook_files' own glob, so only the hook script itself needs an entry.
|
|
628
|
+
"scripts/hook-call-budget" => "scripts/hook-call-budget",
|
|
629
|
+
"scripts/meter-watch" => "scripts/meter-watch",
|
|
630
|
+
"scripts/lib/meter_watch.rb" => "scripts/lib/meter_watch.rb",
|
|
631
|
+
# Intent 340 (G7, n6): answer (closes a decision node or unparks a
|
|
632
|
+
# work node parked at needs_decision), proposals (mints ids for what
|
|
633
|
+
# an executor proposed), and rewind (resets the intent branch to a
|
|
634
|
+
# node's own commit and respins it) - routed from scripts/runner's
|
|
635
|
+
# `answer` and `rewind` verbs.
|
|
636
|
+
"scripts/lib/runner_answer.rb" => "scripts/lib/runner_answer.rb",
|
|
637
|
+
"scripts/lib/runner_proposals.rb" => "scripts/lib/runner_proposals.rb",
|
|
638
|
+
"scripts/lib/runner_rewind.rb" => "scripts/lib/runner_rewind.rb",
|
|
639
|
+
# Intent 343 (G10, n1): the ledger reader - GraphMeasure.read turns one
|
|
640
|
+
# intent directory into one frozen measurement record. No CLI yet
|
|
641
|
+
# (scripts/graph-measure ships at n2); registered here so it is never
|
|
642
|
+
# a lib that exists on disk but never reaches an install.
|
|
643
|
+
"scripts/lib/graph_measure.rb" => "scripts/lib/graph_measure.rb",
|
|
644
|
+
# Intent 343 (G10, n2): the graph-measure command - the subcommand
|
|
645
|
+
# table over the ledger reader, and the two renderers (text, JSON)
|
|
646
|
+
# over the one record it returns.
|
|
647
|
+
"scripts/graph-measure" => "scripts/graph-measure",
|
|
648
|
+
"scripts/lib/graph_measure_report.rb" => "scripts/lib/graph_measure_report.rb",
|
|
649
|
+
# Intent 343 (G10, n4): the `budget` verb - whether the `budget:` a
|
|
650
|
+
# node's envelope declares (327 C19) is a ceiling that ever actually
|
|
651
|
+
# held, read from the ledger and the real input files.
|
|
652
|
+
"scripts/lib/graph_measure_budget.rb" => "scripts/lib/graph_measure_budget.rb",
|
|
653
|
+
# Intent 343 (G10, n5): the `cohorts` verb's model section - the store
|
|
654
|
+
# walk and whether a recorded model= still matches what config
|
|
655
|
+
# resolves today through RunnerPolicy.
|
|
656
|
+
"scripts/lib/graph_measure_models.rb" => "scripts/lib/graph_measure_models.rb",
|
|
657
|
+
# Intent 343 (G10, n6): the rest of the `cohorts` verb - approve-then-
|
|
658
|
+
# fix per verify model, hop on versus off, delivery latency, the
|
|
659
|
+
# evidence bar, and the two concurrency ceilings.
|
|
660
|
+
"scripts/lib/graph_measure_cohorts.rb" => "scripts/lib/graph_measure_cohorts.rb",
|
|
661
|
+
# Intent 340b (G7c, n7): the Codex loop - composes `step` and
|
|
662
|
+
# `node-run` itself (concurrency two, serial absorb, iteration-capped),
|
|
663
|
+
# routed from scripts/runner's internal `until-empty` verb.
|
|
664
|
+
"scripts/lib/runner_until_empty.rb" => "scripts/lib/runner_until_empty.rb",
|
|
665
|
+
# Intent 340a (G7b, n1): the delivery watch - one tick over disk truth,
|
|
666
|
+
# stalled and done-unreported classification, no CLI and no dispatch.
|
|
667
|
+
"scripts/lib/runner_watch.rb" => "scripts/lib/runner_watch.rb",
|
|
458
668
|
}
|
|
459
669
|
end
|
|
460
670
|
|
|
461
671
|
def bootstrap
|
|
462
672
|
puts " \u{1f331} First install \u{2014} bootstrapping store..."
|
|
463
673
|
|
|
464
|
-
|
|
465
|
-
FileUtils.mkdir_p(File.join(plastic_home, "
|
|
674
|
+
legacy = %w[store projects INDEX.md roadmaps].any? { |entry| File.exist?(File.join(plastic_home, entry)) }
|
|
675
|
+
FileUtils.mkdir_p(File.join(plastic_home, "stores", "global")) unless legacy
|
|
676
|
+
FileUtils.mkdir_p(Plastic::StoreLayout.global_store(plastic_home))
|
|
677
|
+
FileUtils.mkdir_p(Plastic::StoreLayout.projects_root(plastic_home))
|
|
466
678
|
|
|
467
679
|
write_if_missing(File.join(plastic_home, "config.yml"), <<~YAML)
|
|
468
680
|
version: 3
|
|
469
681
|
execution_mode: subagent-driven
|
|
470
682
|
stale_threshold_days: 3
|
|
471
|
-
context_offer_tokens:
|
|
472
|
-
context_insist_tokens:
|
|
683
|
+
context_offer_tokens: 150000
|
|
684
|
+
context_insist_tokens: 250000
|
|
473
685
|
hash_length: 6
|
|
474
686
|
hash_algorithm: sha256-base36
|
|
475
687
|
max_slug_words: 5
|
|
@@ -480,7 +692,7 @@ class InstallerCore
|
|
|
480
692
|
|
|
481
693
|
write_if_missing(File.join(plastic_home, "projects.yml"), "---\nprojects: {}\n")
|
|
482
694
|
|
|
483
|
-
write_if_missing(File.join(plastic_home, "INDEX.md"), <<~MD)
|
|
695
|
+
write_if_missing(File.join(Plastic::StoreLayout.global_root(plastic_home), "INDEX.md"), <<~MD)
|
|
484
696
|
# Index
|
|
485
697
|
|
|
486
698
|
## Active
|
|
@@ -498,7 +710,7 @@ class InstallerCore
|
|
|
498
710
|
# Plastic: Agent Instructions
|
|
499
711
|
|
|
500
712
|
Read `PLASTIC.md` in this directory for the core conventions; deeper doctrine lives
|
|
501
|
-
in the `plastic
|
|
713
|
+
in the chapters `plastic help TOPIC` prints. Follow it exactly. Never modify it:
|
|
502
714
|
it is overwritten on plugin updates.
|
|
503
715
|
|
|
504
716
|
This file (`AGENTS.md`) is where project-specific rules live.
|
|
@@ -509,6 +721,25 @@ class InstallerCore
|
|
|
509
721
|
puts " \u{2705} Store bootstrapped"
|
|
510
722
|
end
|
|
511
723
|
|
|
724
|
+
# Intent 372 (former install skill, lines 119-127): `~/.plastic` becomes its own
|
|
725
|
+
# git repository the first time it exists, so `update` can later commit the core
|
|
726
|
+
# files it re-syncs. Leaves an existing repository alone.
|
|
727
|
+
def git_init_if_absent(runner: ->(cmd) { system(*cmd) })
|
|
728
|
+
FileUtils.mkdir_p(plastic_home)
|
|
729
|
+
return if File.directory?(File.join(plastic_home, ".git"))
|
|
730
|
+
runner.call(["git", "-C", plastic_home, "init", "-q"])
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# Intent 372 (former install skill, lines 167-179): register every Plastic store as
|
|
734
|
+
# a QMD collection. QmdSync.register already no-ops per store when QMD is absent;
|
|
735
|
+
# the detector is injected too so a run stays hermetic on a machine that happens to
|
|
736
|
+
# have the real `qmd` binary on PATH.
|
|
737
|
+
def register_with_qmd(runner: QmdSync.default_runner, detector: QmdSync.method(:detect))
|
|
738
|
+
QmdSync.enumerate_stores(plastic_home: plastic_home).each do |store|
|
|
739
|
+
QmdSync.register(collection: store[:collection], dir: store[:dir], runner: runner, detector: detector)
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
|
|
512
743
|
# --- Agent adapters ---
|
|
513
744
|
|
|
514
745
|
# Uniform per-agent install record dir: <agent-dir>/plastic/ holds VERSION and
|
|
@@ -730,6 +961,111 @@ class InstallerCore
|
|
|
730
961
|
removed
|
|
731
962
|
end
|
|
732
963
|
|
|
964
|
+
# Intent 344 (G11, D10): leftovers from the retired per-session day pointer
|
|
965
|
+
# and the retired inter-hook coordination key. A `current` candidate is
|
|
966
|
+
# real only when reached with no symlink between plastic_home and the
|
|
967
|
+
# session directory: a store root's own real path must equal the plain
|
|
968
|
+
# join of plastic_home's real path with the store's lexical relative path
|
|
969
|
+
# (a symlinked store, or a symlinked projects/<slug> above it, breaks
|
|
970
|
+
# that equality), and neither the store's `.tmp` directory nor the
|
|
971
|
+
# session directory beneath it may itself be a symlink. A tmp-file
|
|
972
|
+
# candidate is judged by its containing directory's real path instead,
|
|
973
|
+
# so a symlinked tmp root such as macOS's real /tmp still works; its
|
|
974
|
+
# name is matched on raw bytes so an undecodable name never raises.
|
|
975
|
+
# Never opens or parses a candidate: a torn or malformed leftover is
|
|
976
|
+
# deleted by name alone, exactly like a well-formed one. Only a regular
|
|
977
|
+
# file that is not a symlink is ever a candidate for deletion. Returns
|
|
978
|
+
# the count removed; a delete failure warns once per path and the sync
|
|
979
|
+
# continues, since a stuck leftover must never fail an install.
|
|
980
|
+
def remove_retired_session_state(plastic_home:, tmp_dirs:)
|
|
981
|
+
candidates = []
|
|
982
|
+
|
|
983
|
+
real_store_roots(plastic_home).each do |store_root|
|
|
984
|
+
candidates.concat(current_candidates(store_root))
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
Array(tmp_dirs).compact.each do |dir|
|
|
988
|
+
candidates.concat(tmp_json_candidates(dir))
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
removed = 0
|
|
992
|
+
candidates.uniq.each do |candidate|
|
|
993
|
+
begin
|
|
994
|
+
next unless File.lstat(candidate).file?
|
|
995
|
+
rescue SystemCallError
|
|
996
|
+
next
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
begin
|
|
1000
|
+
File.delete(candidate)
|
|
1001
|
+
removed += 1
|
|
1002
|
+
rescue SystemCallError => e
|
|
1003
|
+
warn " \u{26a0}\u{fe0f} Could not remove retired session state #{candidate}: #{e.message}"
|
|
1004
|
+
end
|
|
1005
|
+
end
|
|
1006
|
+
removed
|
|
1007
|
+
rescue StandardError
|
|
1008
|
+
removed || 0
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
# Every store root (the global store, plus each project store) whose real
|
|
1012
|
+
# path is reached with no symlink between it and plastic_home.
|
|
1013
|
+
def real_store_roots(plastic_home)
|
|
1014
|
+
roots = [Plastic::StoreLayout.global_store(plastic_home)] +
|
|
1015
|
+
Dir.glob(File.join(Plastic::StoreLayout.projects_root(plastic_home), "*", "store"))
|
|
1016
|
+
roots.uniq.select { |root| real_store_root?(plastic_home, root) }
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
def real_store_root?(plastic_home, store_root)
|
|
1020
|
+
return false unless File.directory?(store_root)
|
|
1021
|
+
|
|
1022
|
+
real_home = File.realpath(plastic_home)
|
|
1023
|
+
relative = store_root.sub(/\A#{Regexp.escape(plastic_home)}\/?/, "")
|
|
1024
|
+
File.realpath(store_root) == File.join(real_home, relative)
|
|
1025
|
+
rescue SystemCallError
|
|
1026
|
+
false
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
# `current` candidates under one validated store root: `.tmp` and every
|
|
1030
|
+
# session directory beneath it must be real directories, never symlinks.
|
|
1031
|
+
def current_candidates(store_root)
|
|
1032
|
+
tmp_root = File.join(store_root, ".tmp")
|
|
1033
|
+
return [] unless File.lstat(tmp_root).directory?
|
|
1034
|
+
|
|
1035
|
+
Dir.children(tmp_root).filter_map do |child|
|
|
1036
|
+
begin
|
|
1037
|
+
session_dir = File.join(tmp_root, child)
|
|
1038
|
+
next unless File.lstat(session_dir).directory?
|
|
1039
|
+
|
|
1040
|
+
File.join(session_dir, "current")
|
|
1041
|
+
rescue SystemCallError
|
|
1042
|
+
nil
|
|
1043
|
+
end
|
|
1044
|
+
end
|
|
1045
|
+
rescue SystemCallError
|
|
1046
|
+
[]
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
# Retired session-state leftover files sitting directly in `dir`, resolved through
|
|
1050
|
+
# `dir`'s own real path so a symlinked tmp root (macOS's real /tmp) still
|
|
1051
|
+
# works; the name match runs on raw bytes so an undecodable name is
|
|
1052
|
+
# simply not a match, never a raised error.
|
|
1053
|
+
def tmp_json_candidates(dir)
|
|
1054
|
+
real = File.realpath(dir)
|
|
1055
|
+
Dir.children(real).select { |name| retired_tmp_name?(name) }.map { |name| File.join(real, name) }
|
|
1056
|
+
rescue SystemCallError
|
|
1057
|
+
[]
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
# True when `name`'s raw bytes match the retired session-state leftover shape
|
|
1061
|
+
# (`plastic-<session>--<id>.json`). Runs on `name.b` so a name that is
|
|
1062
|
+
# not valid UTF-8 is judged on bytes instead of raising.
|
|
1063
|
+
def retired_tmp_name?(name)
|
|
1064
|
+
name.b.match?(/\Aplastic-[^\/]+--[^\/]+\.json\z/n)
|
|
1065
|
+
rescue StandardError
|
|
1066
|
+
false
|
|
1067
|
+
end
|
|
1068
|
+
|
|
733
1069
|
# True when `path`, once expanded, is one of `roots` itself or lives underneath
|
|
734
1070
|
# one of them. String-prefix containment guarded by a trailing separator so a
|
|
735
1071
|
# sibling directory that merely shares a prefix (`/home/x-evil` vs root `/home/x`)
|
|
@@ -752,7 +1088,7 @@ class InstallerCore
|
|
|
752
1088
|
# fails milder than the bug it prevents, independent of what any caller's `root:` is or
|
|
753
1089
|
# what a hand-edited manifest claims.
|
|
754
1090
|
def store_roots
|
|
755
|
-
[
|
|
1091
|
+
%w[store projects stores].map { |name| File.expand_path(File.join(plastic_home, name)) }
|
|
756
1092
|
end
|
|
757
1093
|
|
|
758
1094
|
# `install_skills_flat` relocates any top-level underscore-prefixed markdown
|
|
@@ -813,7 +1149,7 @@ class InstallerCore
|
|
|
813
1149
|
installed += install_skills_flat(skills_source, skills_root, exclude: skill_exclude) if File.directory?(skills_source)
|
|
814
1150
|
|
|
815
1151
|
# Copy agent role files into <dir>/agents (manifest-tracked, pruned on update)
|
|
816
|
-
installed += install_agents(File.join(config[:dir], "agents"), models: agent_model_overrides, advisor_enabled: advisor_enabled?)
|
|
1152
|
+
installed += install_agents(File.join(config[:dir], "agents"), models: agent_model_overrides, efforts: agent_effort_overrides, advisor_enabled: advisor_enabled?)
|
|
817
1153
|
|
|
818
1154
|
# Write VERSION
|
|
819
1155
|
version_file = File.join(plastic_dir, "VERSION")
|
|
@@ -828,6 +1164,11 @@ class InstallerCore
|
|
|
828
1164
|
choice = statusline_choice(settings_path, argv: argv, input: input, reinstall: reinstall)
|
|
829
1165
|
merge_claude_hooks(settings_path, choice: choice)
|
|
830
1166
|
|
|
1167
|
+
# The engine deny rule (intent 340b, G7c, n3, D4): a second ownership
|
|
1168
|
+
# mechanism from the hook merge above, so it is its own call rather than a
|
|
1169
|
+
# branch inside merge_claude_hooks.
|
|
1170
|
+
merge_engine_permissions(settings_path)
|
|
1171
|
+
|
|
831
1172
|
# Instruction injection (intent 312): the compact-instructions block into
|
|
832
1173
|
# ~/.claude/CLAUDE.md. A partial-ownership user file, so it is NOT manifest-tracked
|
|
833
1174
|
# (stripped surgically on uninstall), the same treatment ~/.codex/AGENTS.md gets.
|
|
@@ -859,7 +1200,12 @@ class InstallerCore
|
|
|
859
1200
|
# Codex-scoped overrides only (agents.models.codex.*): a literal Claude
|
|
860
1201
|
# model id set under agents.models.claude.* (or the legacy flat form,
|
|
861
1202
|
# which resolves as claude) must never reach a Codex TOML.
|
|
862
|
-
installed += generate_codex_agents(
|
|
1203
|
+
installed += generate_codex_agents(
|
|
1204
|
+
File.join(config[:home_dir], "agents"),
|
|
1205
|
+
models: agent_model_overrides(harness: "codex"),
|
|
1206
|
+
efforts: agent_effort_overrides(harness: "codex"),
|
|
1207
|
+
advisor_enabled: advisor_enabled?
|
|
1208
|
+
)
|
|
863
1209
|
|
|
864
1210
|
# Instruction injection (L1): Plastic standing conventions into ~/.codex/AGENTS.md.
|
|
865
1211
|
# Partial-ownership file, so it is NOT manifest-tracked (stripped surgically on uninstall).
|
|
@@ -889,24 +1235,17 @@ class InstallerCore
|
|
|
889
1235
|
# a whole-file, Plastic-owned .toml per repo agents/*.md instead of copying markdown.
|
|
890
1236
|
# The returned paths append to `installed`, so they are manifest-tracked and pruned on
|
|
891
1237
|
# uninstall by the manifest whole-file-delete path, exactly like ~/.claude/agents/*.md.
|
|
892
|
-
def generate_codex_agents(agents_root, models: {})
|
|
1238
|
+
def generate_codex_agents(agents_root, models: {}, efforts: {}, advisor_enabled: true)
|
|
893
1239
|
sources = Dir.glob(File.join(package_root, "agents", "*.md"))
|
|
894
1240
|
return [] if sources.empty?
|
|
895
1241
|
|
|
896
1242
|
FileUtils.mkdir_p(agents_root)
|
|
897
1243
|
sources.filter_map do |src|
|
|
898
1244
|
basename = File.basename(src, ".md")
|
|
899
|
-
|
|
900
|
-
# owner has not evaluated the Codex reasoning-model ecosystem long enough to
|
|
901
|
-
# judge it. Skip every AgentModels::CONSULTATION_AGENTS file (both
|
|
902
|
-
# plastic-advisor and plastic-faux-advisor) by name, a deliberate and
|
|
903
|
-
# mechanical scope cut tracked at intent 186 (Codex advisor evaluation), not
|
|
904
|
-
# a permanent exclusion and not conditioned on any frontmatter or override
|
|
905
|
-
# value.
|
|
906
|
-
next if AgentModels::CONSULTATION_AGENTS.include?(basename)
|
|
1245
|
+
next if !advisor_enabled && AgentModels::CONSULTATION_AGENTS.include?(basename)
|
|
907
1246
|
|
|
908
1247
|
dest = File.join(agents_root, "#{basename}.toml")
|
|
909
|
-
write_text_atomic(dest, render_codex_agent_toml(src, models[basename]))
|
|
1248
|
+
write_text_atomic(dest, render_codex_agent_toml(src, models[basename], efforts[basename]))
|
|
910
1249
|
dest
|
|
911
1250
|
end
|
|
912
1251
|
end
|
|
@@ -914,16 +1253,25 @@ class InstallerCore
|
|
|
914
1253
|
# Render one repo agents/*.md into a deterministic Codex agent TOML document. Fixed field
|
|
915
1254
|
# order (name, description, the model field(s) from codex_model_fields, developer_instructions)
|
|
916
1255
|
# so regenerate is byte-identical (idempotency).
|
|
917
|
-
def render_codex_agent_toml(source_path, override)
|
|
1256
|
+
def render_codex_agent_toml(source_path, override, effort_override = nil)
|
|
918
1257
|
front, body = split_frontmatter(File.read(source_path))
|
|
919
1258
|
name = (front["name"] || File.basename(source_path, ".md")).to_s
|
|
920
1259
|
description = (front["description"] || "").to_s
|
|
921
|
-
effective =
|
|
1260
|
+
effective = if override && !override.to_s.empty?
|
|
1261
|
+
override
|
|
1262
|
+
else
|
|
1263
|
+
AgentModels.shipped_model_for(name, harness: "codex") || front["model"]
|
|
1264
|
+
end
|
|
1265
|
+
effort = if effort_override && !effort_override.to_s.empty?
|
|
1266
|
+
effort_override
|
|
1267
|
+
else
|
|
1268
|
+
front["effort"] || AgentModels.shipped_effort_for(name)
|
|
1269
|
+
end
|
|
922
1270
|
|
|
923
1271
|
parts = []
|
|
924
1272
|
parts << %(name = "#{toml_inline_escape(name)}")
|
|
925
1273
|
parts << %(description = "#{toml_inline_escape(description)}")
|
|
926
|
-
parts << codex_model_fields(effective)
|
|
1274
|
+
parts << codex_model_fields(effective, effort: effort)
|
|
927
1275
|
parts << "developer_instructions = \"\"\"\n#{toml_ml_escape(body.strip)}\n\"\"\""
|
|
928
1276
|
parts.reject(&:empty?).join("\n") + "\n"
|
|
929
1277
|
end
|
|
@@ -943,20 +1291,20 @@ class InstallerCore
|
|
|
943
1291
|
# The model-selection line(s). A known tier alias (opus/sonnet/haiku) emits BOTH a `model` line
|
|
944
1292
|
# (from AgentModels.codex_model_for, the intent-186 per-role Codex identity) and a
|
|
945
1293
|
# model_reasoning_effort line, model first for deterministic byte-identical regenerate. Any other
|
|
946
|
-
# non-empty value is a literal Codex model id
|
|
947
|
-
#
|
|
948
|
-
|
|
949
|
-
def codex_model_fields(effective)
|
|
1294
|
+
# non-empty value is a literal Codex model id. Every non-empty model also emits the resolved effort,
|
|
1295
|
+
# medium by default or the harness-scoped override. Empty emits no model fields.
|
|
1296
|
+
def codex_model_fields(effective, effort: AgentModels::DEFAULT_EFFORT)
|
|
950
1297
|
return "" if effective.nil? || effective.to_s.empty?
|
|
951
|
-
|
|
952
|
-
if effort
|
|
1298
|
+
if AgentModels.effort_for(effective)
|
|
953
1299
|
lines = []
|
|
954
1300
|
model = AgentModels.codex_model_for(effective)
|
|
955
1301
|
lines << %(model = "#{toml_inline_escape(model)}") if model && !model.to_s.empty?
|
|
956
|
-
lines << %(model_reasoning_effort = "#{effort}")
|
|
1302
|
+
lines << %(model_reasoning_effort = "#{toml_inline_escape(effort)}") unless effort.to_s.empty?
|
|
957
1303
|
lines.join("\n")
|
|
958
1304
|
else
|
|
959
|
-
%(model = "#{toml_inline_escape(effective.to_s)}")
|
|
1305
|
+
lines = [%(model = "#{toml_inline_escape(effective.to_s)}")]
|
|
1306
|
+
lines << %(model_reasoning_effort = "#{toml_inline_escape(effort)}") unless effort.to_s.empty?
|
|
1307
|
+
lines.join("\n")
|
|
960
1308
|
end
|
|
961
1309
|
end
|
|
962
1310
|
|
|
@@ -1041,7 +1389,7 @@ class InstallerCore
|
|
|
1041
1389
|
skills_source = File.join(package_root, "skills")
|
|
1042
1390
|
skill_exclude = advisor_enabled? ? [] : ["agent-advisor"]
|
|
1043
1391
|
installed += install_skills_flat(skills_source, File.join(config[:dir], "skills"), exclude: skill_exclude) if File.directory?(skills_source)
|
|
1044
|
-
installed += install_agents(File.join(config[:dir], "agents"), models: agent_model_overrides, advisor_enabled: advisor_enabled?)
|
|
1392
|
+
installed += install_agents(File.join(config[:dir], "agents"), models: agent_model_overrides, efforts: agent_effort_overrides, advisor_enabled: advisor_enabled?)
|
|
1045
1393
|
|
|
1046
1394
|
# Uniform per-agent record (intent 210, D2): write VERSION alongside the manifest,
|
|
1047
1395
|
# the same shape install_claude already writes.
|
|
@@ -1089,10 +1437,9 @@ class InstallerCore
|
|
|
1089
1437
|
# them to `installed` before write_manifest (manifest + prune are then automatic).
|
|
1090
1438
|
# No-op safe: returns [] when the package has no agents dir or it is empty.
|
|
1091
1439
|
# advisor_enabled: false (advisor.enabled config key) skips every
|
|
1092
|
-
# AgentModels::CONSULTATION_AGENTS file entirely
|
|
1093
|
-
# plastic-faux-advisor), so a user who declined the advisor never gets either
|
|
1440
|
+
# AgentModels::CONSULTATION_AGENTS file entirely, so a user who declined the advisor never gets either
|
|
1094
1441
|
# agent installed.
|
|
1095
|
-
def install_agents(agents_root, models: {}, advisor_enabled: true)
|
|
1442
|
+
def install_agents(agents_root, models: {}, efforts: {}, advisor_enabled: true)
|
|
1096
1443
|
sources = Dir.glob(File.join(package_root, "agents", "*.md"))
|
|
1097
1444
|
return [] if sources.empty?
|
|
1098
1445
|
|
|
@@ -1104,9 +1451,13 @@ class InstallerCore
|
|
|
1104
1451
|
sources.map do |src|
|
|
1105
1452
|
dest = File.join(agents_root, File.basename(src))
|
|
1106
1453
|
basename = File.basename(src, ".md")
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1454
|
+
model = models[basename]
|
|
1455
|
+
effort = efforts[basename]
|
|
1456
|
+
if model || effort
|
|
1457
|
+
content = File.read(src)
|
|
1458
|
+
content = rewrite_model_line(content, model) if model
|
|
1459
|
+
content = rewrite_effort_line(content, effort) if effort
|
|
1460
|
+
File.write(dest, content)
|
|
1110
1461
|
else
|
|
1111
1462
|
FileUtils.cp(src, dest)
|
|
1112
1463
|
end
|
|
@@ -1120,29 +1471,48 @@ class InstallerCore
|
|
|
1120
1471
|
content.sub(/^model:[^\n]*$/, "model: #{model}")
|
|
1121
1472
|
end
|
|
1122
1473
|
|
|
1474
|
+
def rewrite_effort_line(content, effort)
|
|
1475
|
+
return content.sub(/^effort:[^\n]*$/, "effort: #{effort}") if content.match?(/^effort:/)
|
|
1476
|
+
|
|
1477
|
+
content.sub(/^(model:[^\n]*)$/) { "#{Regexp.last_match(1)}\neffort: #{effort}" }
|
|
1478
|
+
end
|
|
1479
|
+
|
|
1123
1480
|
# Resolve per-agent model overrides for this install: project config (when a
|
|
1124
1481
|
# project dir is known) overlaid on global config, scoped to `harness`
|
|
1125
1482
|
# ("claude" or "codex"). Defaults are NOT included, so unconfigured agents
|
|
1126
1483
|
# keep their shipped frontmatter.
|
|
1127
1484
|
#
|
|
1128
|
-
# Both advisor agents
|
|
1485
|
+
# Both advisor agents resolve through
|
|
1129
1486
|
# this SAME generic map, like any other agent: a config author sets
|
|
1130
|
-
# agents.models.claude
|
|
1131
|
-
# agents.models.plastic-advisor, read as claude) to point either agent at a
|
|
1487
|
+
# agents.models.claude.<agent> (or the legacy flat form, read as Claude) to point either agent at a
|
|
1132
1488
|
# different literal model. There is no separate advisor-specific model key;
|
|
1133
1489
|
# which agent the advisor SKILL routes to by default is a routing decision
|
|
1134
1490
|
# (advisor.claude.default), never a model-selection one.
|
|
1135
1491
|
def agent_model_overrides(project_dir = nil, harness: "claude")
|
|
1136
|
-
|
|
1492
|
+
global_path = File.join(plastic_home, "config.yml")
|
|
1493
|
+
migrate_advisor_config_file(global_path)
|
|
1494
|
+
global_config = load_config_yaml(global_path)
|
|
1137
1495
|
project_config =
|
|
1138
1496
|
if project_dir
|
|
1139
|
-
|
|
1497
|
+
project_path = File.join(project_dir, ".plastic_store", "config.yml")
|
|
1498
|
+
migrate_advisor_config_file(project_path)
|
|
1499
|
+
load_config_yaml(project_path)
|
|
1140
1500
|
else
|
|
1141
1501
|
{}
|
|
1142
1502
|
end
|
|
1143
1503
|
AgentModels.override_map(project_config: project_config, global_config: global_config, harness: harness)
|
|
1144
1504
|
end
|
|
1145
1505
|
|
|
1506
|
+
def agent_effort_overrides(project_dir = nil, harness: "claude")
|
|
1507
|
+
global_path = File.join(plastic_home, "config.yml")
|
|
1508
|
+
migrate_advisor_config_file(global_path)
|
|
1509
|
+
global_config = load_config_yaml(global_path)
|
|
1510
|
+
project_path = project_dir && File.join(project_dir, ".plastic_store", "config.yml")
|
|
1511
|
+
migrate_advisor_config_file(project_path) if project_path
|
|
1512
|
+
project_config = project_path ? load_config_yaml(project_path) : {}
|
|
1513
|
+
AgentModels.effort_override_map(project_config: project_config, global_config: global_config, harness: harness)
|
|
1514
|
+
end
|
|
1515
|
+
|
|
1146
1516
|
# advisor.enabled: project overlays global, missing or malformed counts as
|
|
1147
1517
|
# enabled (fail-open). Harness-blind: false skips both advisor agents and the
|
|
1148
1518
|
# agent-advisor skill on every installed harness.
|
|
@@ -1154,10 +1524,14 @@ class InstallerCore
|
|
|
1154
1524
|
value != false
|
|
1155
1525
|
end
|
|
1156
1526
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1527
|
+
ADVISOR_NAME_MIGRATIONS = {
|
|
1528
|
+
"plastic-advisor" => "plastic-primary-advisor",
|
|
1529
|
+
"plastic-faux-advisor" => "plastic-secondary-advisor"
|
|
1530
|
+
}.freeze
|
|
1531
|
+
ADVISOR_SHORTHANDS = {
|
|
1532
|
+
"primary" => "plastic-primary-advisor", "secondary" => "plastic-secondary-advisor",
|
|
1533
|
+
"real" => "plastic-primary-advisor", "faux" => "plastic-secondary-advisor"
|
|
1534
|
+
}.freeze
|
|
1161
1535
|
|
|
1162
1536
|
# Write advisor.enabled / advisor.claude.default into the global config.yml
|
|
1163
1537
|
# from install-time flags. Absent flags change nothing: advisor.enabled
|
|
@@ -1165,7 +1539,7 @@ class InstallerCore
|
|
|
1165
1539
|
# (the skill's own fallback chain applies) when missing.
|
|
1166
1540
|
# --no-advisor -> advisor.enabled: false
|
|
1167
1541
|
# --advisor VALUE -> advisor.claude.default: VALUE (an agent name, or the
|
|
1168
|
-
# shorthand "
|
|
1542
|
+
# shorthand "primary"/"secondary")
|
|
1169
1543
|
def apply_config_flags(argv)
|
|
1170
1544
|
no_advisor = argv.include?("--no-advisor")
|
|
1171
1545
|
advisor_idx = argv.index("--advisor")
|
|
@@ -1173,7 +1547,13 @@ class InstallerCore
|
|
|
1173
1547
|
return unless no_advisor || advisor_value
|
|
1174
1548
|
|
|
1175
1549
|
config_path = File.join(plastic_home, "config.yml")
|
|
1176
|
-
config =
|
|
1550
|
+
config = if File.exist?(config_path)
|
|
1551
|
+
parsed = YAML.safe_load(File.read(config_path))
|
|
1552
|
+
return false unless parsed.nil? || parsed.is_a?(Hash)
|
|
1553
|
+
migrate_advisor_config(parsed || {})
|
|
1554
|
+
else
|
|
1555
|
+
{}
|
|
1556
|
+
end
|
|
1177
1557
|
|
|
1178
1558
|
if no_advisor
|
|
1179
1559
|
config["advisor"] ||= {}
|
|
@@ -1188,6 +1568,9 @@ class InstallerCore
|
|
|
1188
1568
|
|
|
1189
1569
|
FileUtils.mkdir_p(plastic_home)
|
|
1190
1570
|
File.write(config_path, YAML.dump(config))
|
|
1571
|
+
true
|
|
1572
|
+
rescue StandardError
|
|
1573
|
+
false
|
|
1191
1574
|
end
|
|
1192
1575
|
|
|
1193
1576
|
def load_config_yaml(path)
|
|
@@ -1197,6 +1580,45 @@ class InstallerCore
|
|
|
1197
1580
|
{}
|
|
1198
1581
|
end
|
|
1199
1582
|
|
|
1583
|
+
# Renames retired advisor keys without discarding a current key. It accepts
|
|
1584
|
+
# malformed config sections and leaves unrelated values untouched.
|
|
1585
|
+
def migrate_advisor_config(config)
|
|
1586
|
+
return {} unless config.is_a?(Hash)
|
|
1587
|
+
config = Marshal.load(Marshal.dump(config))
|
|
1588
|
+
advisor = config["advisor"]
|
|
1589
|
+
if advisor.is_a?(Hash) && advisor["claude"].is_a?(Hash)
|
|
1590
|
+
default = advisor["claude"]["default"]
|
|
1591
|
+
advisor["claude"]["default"] = ADVISOR_NAME_MIGRATIONS.fetch(default, default)
|
|
1592
|
+
end
|
|
1593
|
+
agents = config["agents"]
|
|
1594
|
+
%w[models efforts].each do |section_name|
|
|
1595
|
+
section = agents.is_a?(Hash) ? agents[section_name] : nil
|
|
1596
|
+
next unless section.is_a?(Hash)
|
|
1597
|
+
migrate_advisor_keys!(section)
|
|
1598
|
+
%w[claude codex].each { |harness| migrate_advisor_keys!(section[harness]) if section[harness].is_a?(Hash) }
|
|
1599
|
+
end
|
|
1600
|
+
config
|
|
1601
|
+
end
|
|
1602
|
+
|
|
1603
|
+
def migrate_advisor_config_file(path)
|
|
1604
|
+
return false unless path && File.file?(path)
|
|
1605
|
+
parsed = YAML.safe_load(File.read(path))
|
|
1606
|
+
return false unless parsed.is_a?(Hash)
|
|
1607
|
+
migrated = migrate_advisor_config(parsed)
|
|
1608
|
+
return false if migrated == parsed
|
|
1609
|
+
File.write(path, YAML.dump(migrated))
|
|
1610
|
+
true
|
|
1611
|
+
rescue StandardError
|
|
1612
|
+
false
|
|
1613
|
+
end
|
|
1614
|
+
|
|
1615
|
+
def migrate_advisor_keys!(section)
|
|
1616
|
+
ADVISOR_NAME_MIGRATIONS.each do |legacy, current|
|
|
1617
|
+
section[current] = section[legacy] if !section.key?(current) && section.key?(legacy)
|
|
1618
|
+
section.delete(legacy)
|
|
1619
|
+
end
|
|
1620
|
+
end
|
|
1621
|
+
|
|
1200
1622
|
# --- Legacy plugin migration ---
|
|
1201
1623
|
|
|
1202
1624
|
# Earlier versions registered Plastic as a local marketplace plugin
|
|
@@ -1293,7 +1715,7 @@ class InstallerCore
|
|
|
1293
1715
|
puts " \u{2139}\u{fe0f} Kept #{kept.size} hook(s) Plastic does not own, named with the reserved plastic- prefix:"
|
|
1294
1716
|
kept.each { |cmd| puts " - #{tilde(cmd.to_s)}" }
|
|
1295
1717
|
puts " The plastic- prefix is reserved for Plastic's own hooks. Rename yours (for"
|
|
1296
|
-
puts " example ~/.claude/hooks/writing
|
|
1718
|
+
puts " example ~/.claude/hooks/plain-writing) so a future update never mistakes it."
|
|
1297
1719
|
end
|
|
1298
1720
|
|
|
1299
1721
|
# --- settings.json merge (read-modify-write, never clobber) ---
|
|
@@ -1381,6 +1803,44 @@ class InstallerCore
|
|
|
1381
1803
|
removed
|
|
1382
1804
|
end
|
|
1383
1805
|
|
|
1806
|
+
# --- The engine deny rule (intent 340b, G7c, n3) ---
|
|
1807
|
+
#
|
|
1808
|
+
# A second ownership mechanism from merge_claude_hooks above (D4, node n3): a
|
|
1809
|
+
# permissions.deny entry is a bare string with no marker in it, so EnginePermissions
|
|
1810
|
+
# owns its four entries by exact-string membership, not by a plastic- launcher
|
|
1811
|
+
# basename. This pair only does the read-modify-write; EnginePermissions.merge_into
|
|
1812
|
+
# and .remove_from are the pure transforms.
|
|
1813
|
+
|
|
1814
|
+
# Merges EnginePermissions::ENTRIES into settings.json. Unlike merge_claude_hooks,
|
|
1815
|
+
# this refuses rather than starting from {} when the existing file cannot be
|
|
1816
|
+
# parsed (row 3.11): a hand-edited settings file is never silently replaced.
|
|
1817
|
+
# Returns true when it wrote, false when it refused.
|
|
1818
|
+
def merge_engine_permissions(settings_path)
|
|
1819
|
+
if File.exist?(settings_path)
|
|
1820
|
+
settings = read_json_safe(settings_path)
|
|
1821
|
+
return false if settings.nil?
|
|
1822
|
+
else
|
|
1823
|
+
settings = {}
|
|
1824
|
+
end
|
|
1825
|
+
|
|
1826
|
+
write_json_atomic(settings_path, EnginePermissions.merge_into(settings))
|
|
1827
|
+
true
|
|
1828
|
+
end
|
|
1829
|
+
|
|
1830
|
+
# Removes exactly EnginePermissions::ENTRIES from settings.json, leaving every
|
|
1831
|
+
# other deny entry (the owner's own, and any Plastic entry the owner has since
|
|
1832
|
+
# edited) in place. Returns true when it wrote, false on a missing or
|
|
1833
|
+
# unparseable file, or a permissions/deny shape it does not recognize.
|
|
1834
|
+
def remove_engine_permissions(settings_path)
|
|
1835
|
+
return false unless File.exist?(settings_path)
|
|
1836
|
+
|
|
1837
|
+
settings = read_json_safe(settings_path)
|
|
1838
|
+
return false unless settings.is_a?(Hash)
|
|
1839
|
+
|
|
1840
|
+
write_json_atomic(settings_path, EnginePermissions.remove_from(settings))
|
|
1841
|
+
true
|
|
1842
|
+
end
|
|
1843
|
+
|
|
1384
1844
|
# --- Codex AGENTS.md marked-section injection (22a/Beads pattern) ---
|
|
1385
1845
|
# New primitive: markdown marked-section merge, the analog of merge_claude_hooks'
|
|
1386
1846
|
# JSON read-modify-write for a partial-ownership text file. Three states
|
|
@@ -1570,6 +2030,7 @@ class InstallerCore
|
|
|
1570
2030
|
if key == "claude"
|
|
1571
2031
|
settings_path = File.join(config[:dir], "settings.json")
|
|
1572
2032
|
remove_claude_hooks(settings_path) if File.exist?(settings_path)
|
|
2033
|
+
remove_engine_permissions(settings_path) if File.exist?(settings_path)
|
|
1573
2034
|
removed.concat(migrate_legacy_plugin(config[:dir]))
|
|
1574
2035
|
|
|
1575
2036
|
# The compact-instructions block in the user-owned CLAUDE.md (intent 312): a
|
|
@@ -1718,11 +2179,22 @@ class InstallerCore
|
|
|
1718
2179
|
|
|
1719
2180
|
def read_json_safe(path)
|
|
1720
2181
|
return nil unless File.exist?(path)
|
|
2182
|
+
|
|
1721
2183
|
JSON.parse(File.read(path))
|
|
1722
2184
|
rescue JSON::ParserError
|
|
1723
|
-
#
|
|
1724
|
-
content
|
|
1725
|
-
|
|
2185
|
+
# The comment/trailing-comma-stripped retry below can itself raise
|
|
2186
|
+
# JSON::ParserError on genuinely malformed content (a truncated file, or
|
|
2187
|
+
# plain garbage). A nested begin/rescue is required here because a
|
|
2188
|
+
# method-level `rescue` clause never catches an exception raised from
|
|
2189
|
+
# INSIDE a sibling rescue clause's own body (only from the main body);
|
|
2190
|
+
# doctor_core.rb#read_json_safe carries the same fix for the same reason
|
|
2191
|
+
# (intent 331e, F5).
|
|
2192
|
+
begin
|
|
2193
|
+
content = File.read(path).gsub(%r{//[^\n]*}, "").gsub(/,(\s*[}\]])/, '\1')
|
|
2194
|
+
JSON.parse(content)
|
|
2195
|
+
rescue
|
|
2196
|
+
nil
|
|
2197
|
+
end
|
|
1726
2198
|
rescue
|
|
1727
2199
|
nil
|
|
1728
2200
|
end
|