@zalom/plastic 2.0.0-alpha.9 → 2.0.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.md +13 -136
- package/README.md +357 -133
- package/agents/plastic-enforcer.md +20 -15
- 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 +6 -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/{skills/conventions/references → docs/help}/completion-and-done.md +15 -16
- 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 +2 -2
- 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 +153 -30
- 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 +48 -83
- 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 +99 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +31 -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 +53 -0
- package/scripts/lib/cli/commands/intent_end.rb +62 -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 +71 -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 +42 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +72 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +33 -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 +70 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +45 -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 +46 -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 +229 -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 +531 -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 +913 -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 +158 -8
- 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 +525 -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 +25 -18
- 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/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 +42 -33
- 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 +119 -0
- package/scripts/roadmap-savepoint +7 -0
- 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 +25 -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/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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../store_layout"
|
|
4
|
+
require_relative "../index_entry"
|
|
5
|
+
require_relative "../store_discovery"
|
|
6
|
+
|
|
7
|
+
# Plastic::CLI::Scope (intent 363) - which store a command works on. Six skills
|
|
8
|
+
# used to restate this rule in prose; it is stated here once.
|
|
9
|
+
#
|
|
10
|
+
# A named project wins. With no name, the project whose repository holds the
|
|
11
|
+
# working directory wins, longest path first, so a repository nested inside
|
|
12
|
+
# another resolves to the inner one. With neither, the global store answers.
|
|
13
|
+
#
|
|
14
|
+
# Every path comes from the injected environment and home directory, so a test
|
|
15
|
+
# points at a temporary store and never at the real ~/.plastic.
|
|
16
|
+
module Plastic
|
|
17
|
+
class CLI
|
|
18
|
+
class Scope
|
|
19
|
+
UnknownProject = Class.new(StandardError)
|
|
20
|
+
|
|
21
|
+
GLOBAL = "global"
|
|
22
|
+
|
|
23
|
+
def initialize(env:, home:, slug: nil, directory: Dir.pwd)
|
|
24
|
+
@env = env
|
|
25
|
+
@home = home
|
|
26
|
+
@requested = slug
|
|
27
|
+
@directory = canonical_path(File.expand_path(directory))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def plastic_home
|
|
31
|
+
@plastic_home ||= @env["PLASTIC_HOME"] || File.join(@home, ".plastic")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def stores
|
|
35
|
+
@stores ||= StoreDiscovery.discover(plastic_home)[:stores]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def slug
|
|
39
|
+
@slug ||= resolve_slug
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def root
|
|
43
|
+
@root ||= found&.fetch(:root) || plastic_home
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def store
|
|
47
|
+
File.join(root, "store")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def index_path
|
|
51
|
+
File.join(root, "INDEX.md")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def roadmaps_dir
|
|
55
|
+
File.join(root, "roadmaps")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def active_entries
|
|
59
|
+
return [] unless File.exist?(index_path)
|
|
60
|
+
|
|
61
|
+
section(File.read(index_path)).filter_map do |line|
|
|
62
|
+
match = IndexEntry.match(line)
|
|
63
|
+
match && [match[1], match[2]]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def active_ids
|
|
68
|
+
active_entries.map(&:first)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def intent_dir(id)
|
|
72
|
+
Dir.glob(File.join(store, "#{id}--*")).min
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def directory_project
|
|
76
|
+
@directory_project = project_for_directory unless defined?(@directory_project)
|
|
77
|
+
@directory_project
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def known_slugs
|
|
81
|
+
stores.map { |store| store[:slug] }.sort
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def found
|
|
87
|
+
stores.find { |store| store[:slug] == slug }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def resolve_slug
|
|
91
|
+
return requested_slug if @requested
|
|
92
|
+
|
|
93
|
+
directory_project || GLOBAL
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def requested_slug
|
|
97
|
+
return @requested if known_slugs.include?(@requested)
|
|
98
|
+
|
|
99
|
+
raise UnknownProject,
|
|
100
|
+
"no project named #{@requested.inspect}; this machine has #{known_slugs.join(", ")}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def project_for_directory
|
|
104
|
+
(repositories + stores.map { |store| [store[:slug], store[:root]] })
|
|
105
|
+
.select { |_slug, path| inside?(path) }
|
|
106
|
+
.max_by { |_slug, path| path.length }
|
|
107
|
+
&.first
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def inside?(path)
|
|
111
|
+
path = canonical_path(File.expand_path(path))
|
|
112
|
+
@directory == path || @directory.start_with?("#{path}#{File::SEPARATOR}")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def canonical_path(path)
|
|
116
|
+
return File.realpath(path) if File.exist?(path)
|
|
117
|
+
|
|
118
|
+
File.join(canonical_path(File.dirname(path)), File.basename(path))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def repositories
|
|
122
|
+
StoreProvisioning.load_projects(plastic_home).filter_map do |slug, info|
|
|
123
|
+
path = info.is_a?(Hash) ? info["path"].to_s : ""
|
|
124
|
+
[slug, path] unless path.empty?
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def section(text)
|
|
129
|
+
body = text.match(/^##\s+Active\s*$(.*?)(?=^##\s|\z)/m)
|
|
130
|
+
body ? body[1].lines.map(&:strip) : []
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Plastic::CLI::TABLE (intent 363) - the whole command line in one frozen hash:
|
|
4
|
+
# a command name mapped to the file that holds it, the class inside
|
|
5
|
+
# Plastic::CLI::Commands, and the one line `plastic help` prints beside it.
|
|
6
|
+
#
|
|
7
|
+
# Adding a command is one row here plus one file. `plastic help` reads this
|
|
8
|
+
# hash and loads nothing, so listing the commands costs no command file.
|
|
9
|
+
# test/cli/table_test.rb walks every row, so a half-wired command cannot ship.
|
|
10
|
+
module Plastic
|
|
11
|
+
class CLI
|
|
12
|
+
TABLE = {
|
|
13
|
+
"auto brief" => ["commands/auto_brief", "AutoBrief", "Print an intent's spawn preamble, and the advisor shapes for --role advisor"],
|
|
14
|
+
"auto lock" => ["commands/auto_lock", "AutoLock", "Inspect, repair, or release an intent's delivery lock"],
|
|
15
|
+
"auto report" => ["commands/auto_report", "AutoReport", "Print an intent's completion report, then the review-by-risk rules"],
|
|
16
|
+
"auto take" => ["commands/auto_take", "AutoTake", "Arm an intent's delivery lock for this session"],
|
|
17
|
+
"auto" => ["commands/auto", "Auto", "List the auto subcommands"],
|
|
18
|
+
"backup" => ["commands/backup", "Backup", "Write one archive of the three databases and the config"],
|
|
19
|
+
"checkout" => ["commands/checkout", "Checkout", "Restore missing store files from the databases"],
|
|
20
|
+
"continue" => ["commands/continue", "Continue", "Show where one project stands and what runs next"],
|
|
21
|
+
"doctor" => ["commands/doctor", "Doctor", "Diagnose Plastic installation health"],
|
|
22
|
+
"feedback" => ["commands/feedback", "Feedback", "File a feedback report, body on standard input"],
|
|
23
|
+
"help" => ["commands/help", "Help", "List the commands and help topics, or show one usage"],
|
|
24
|
+
"hook" => ["commands/hook", "Hook", "Run one harness hook event through its launcher"],
|
|
25
|
+
"index" => ["commands/index", "Index", "Rebuild the search index from every markdown file in the stores"],
|
|
26
|
+
"install" => ["commands/install", "Install", "Install Plastic into this machine's agents"],
|
|
27
|
+
"intent" => ["commands/intent", "Intent", "List the intent subcommands"],
|
|
28
|
+
"intent answer" => ["commands/intent_answer", "IntentAnswer", "Answer an intent's needs_decision node"],
|
|
29
|
+
"intent end" => ["commands/intent_end", "IntentEnd", "Close an intent as delivered or abandoned"],
|
|
30
|
+
"intent new" => ["commands/intent_new", "IntentNew", "Create a new intent"],
|
|
31
|
+
"intent note" => ["commands/intent_note", "IntentNote", "Append a savepoint note to an intent"],
|
|
32
|
+
"intent rule" => ["commands/intent_rule", "IntentRule", "Record a ruling in an intent's Insights"],
|
|
33
|
+
"intent show" => ["commands/intent_show", "IntentShow", "Print an intent's state screen"],
|
|
34
|
+
"intent spec" => ["commands/intent_spec", "IntentSpec", "Print an intent's state screen, then the speccing rules"],
|
|
35
|
+
"intent step" => ["commands/intent_step", "IntentStep", "Run the next ready step of an intent's graph"],
|
|
36
|
+
"intent verify" => ["commands/intent_verify", "IntentVerify", "Run an intent's merge-gate checks"],
|
|
37
|
+
"migrate" => ["commands/migrate", "Migrate", "List the migrate subcommands"],
|
|
38
|
+
"migrate stores" => ["commands/migrate_stores", "MigrateStores", "Move the stores under stores/, behind a full copy of the home"],
|
|
39
|
+
"next" => ["commands/next", "Next", "Print the next action in one line"],
|
|
40
|
+
"project" => ["commands/project", "Project", "List the project subcommands"],
|
|
41
|
+
"project links" => ["commands/project_links", "ProjectLinks", "Project every store's Links sections from frontmatter"],
|
|
42
|
+
"project list" => ["commands/project_list", "ProjectList", "List every store this machine holds"],
|
|
43
|
+
"project new" => ["commands/project_new", "ProjectNew", "Register, provision and validate a new project"],
|
|
44
|
+
"query" => ["commands/query", "Query", "Run one read-only SQL statement on the index"],
|
|
45
|
+
"render" => ["commands/render", "Render", "Print one markdown file as an HTML page"],
|
|
46
|
+
"roadmap" => ["commands/roadmap", "Roadmap", "List the roadmap subcommands"],
|
|
47
|
+
"roadmap check" => ["commands/roadmap_check", "RoadmapCheck", "Check a roadmap's graph for cycles and dangling ids"],
|
|
48
|
+
"roadmap log" => ["commands/roadmap_log", "RoadmapLog", "Append a savepoint line to a roadmap's ledger"],
|
|
49
|
+
"roadmap migrate" => ["commands/roadmap_migrate", "RoadmapMigrate", "Write a roadmap's Graph section from its batches"],
|
|
50
|
+
"roadmap next" => ["commands/roadmap_next", "RoadmapNext", "Name the roadmap most worth continuing"],
|
|
51
|
+
"roadmap show" => ["commands/roadmap_show", "RoadmapShow", "Print a roadmap's state screen"],
|
|
52
|
+
"rollback" => ["commands/rollback", "Rollback", "Move to a Plastic version this machine has run before"],
|
|
53
|
+
"search" => ["commands/search", "Search", "Find text in the stores, ranked, with one excerpt each"],
|
|
54
|
+
"session commit" => ["commands/session_commit", "SessionCommit", "Commit one verified checklist item"],
|
|
55
|
+
"session handoff" => ["commands/session_handoff", "SessionHandoff", "Write this session's hand-off into the day ledger"],
|
|
56
|
+
"session summary" => ["commands/session_summary", "SessionSummary", "Print the day ledger's open items and recent activity"],
|
|
57
|
+
"session" => ["commands/session", "Session", "List the session subcommands"],
|
|
58
|
+
"status" => ["commands/status", "Status", "Show active work in every store"],
|
|
59
|
+
"sync" => ["commands/sync", "Sync", "Bring the store files and the three databases level"],
|
|
60
|
+
"uninstall" => ["commands/uninstall", "Uninstall", "Remove Plastic from this machine's agents"],
|
|
61
|
+
"update" => ["commands/update", "Update", "Move Plastic to the next version on its channel"],
|
|
62
|
+
"version" => ["commands/version", "Version", "Print the installed Plastic version"]
|
|
63
|
+
}.freeze
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "did_you_mean"
|
|
5
|
+
require_relative "cli/table"
|
|
6
|
+
require_relative "cli/command"
|
|
7
|
+
require_relative "cli/output"
|
|
8
|
+
|
|
9
|
+
# Plastic::CLI (intent 363) - the dispatcher behind bin/plastic. It reads the
|
|
10
|
+
# command table, matches the longest command name the arguments begin with,
|
|
11
|
+
# requires that one file, and calls it. Nothing else is loaded, which is what
|
|
12
|
+
# keeps a call at about 20 milliseconds with RubyGems off.
|
|
13
|
+
#
|
|
14
|
+
# No arguments, or --help, print the command list. --version prints the
|
|
15
|
+
# version. A command's own --help prints that command's usage line, read from
|
|
16
|
+
# the class without building it. An unknown name exits 2 and names the closest
|
|
17
|
+
# command, so a typo never becomes a stack trace.
|
|
18
|
+
module Plastic
|
|
19
|
+
class CLI
|
|
20
|
+
FLAGS = {"--version" => "version", "-v" => "version",
|
|
21
|
+
"--help" => "help", "-h" => "help"}.freeze
|
|
22
|
+
|
|
23
|
+
def self.call(...)
|
|
24
|
+
new(...).call
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(argv, out: $stdout, err: $stderr, env: ENV, home: Dir.home,
|
|
28
|
+
directory: Dir.pwd, table: TABLE)
|
|
29
|
+
@argv = argv.dup
|
|
30
|
+
@out = out
|
|
31
|
+
@err = err
|
|
32
|
+
@env = env
|
|
33
|
+
@home = home
|
|
34
|
+
@directory = directory
|
|
35
|
+
@table = table
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call
|
|
39
|
+
matched = match(expanded)
|
|
40
|
+
return unknown(expanded.first) unless matched
|
|
41
|
+
|
|
42
|
+
name, rest = matched
|
|
43
|
+
return usage(name, rest) if help?(rest)
|
|
44
|
+
|
|
45
|
+
command(name).call(rest, out: @out, err: @err, env: @env, home: @home,
|
|
46
|
+
directory: @directory)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def match(argv)
|
|
50
|
+
[2, 1].each do |width|
|
|
51
|
+
name = argv.first(width).join(" ")
|
|
52
|
+
return [name, argv.drop(width)] if @table.key?(name)
|
|
53
|
+
end
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def expanded
|
|
60
|
+
return ["help"] if @argv.empty?
|
|
61
|
+
|
|
62
|
+
[FLAGS.fetch(@argv.first, @argv.first)] + @argv.drop(1)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def help?(rest)
|
|
66
|
+
rest.any? { |argument| FLAGS[argument] == "help" }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def usage(name, rest)
|
|
70
|
+
Output.new(out: @out, err: @err)
|
|
71
|
+
.row("usage", command(name)::USAGE_LINE)
|
|
72
|
+
.flush(json: rest.include?("--json"))
|
|
73
|
+
Command::OK
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def command(name)
|
|
77
|
+
file, const, = @table.fetch(name)
|
|
78
|
+
require_relative "cli/#{file}"
|
|
79
|
+
Commands.const_get(const)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def unknown(name)
|
|
83
|
+
suggestions = closest(name)
|
|
84
|
+
@err.puts "plastic: no command named #{name.inspect}"
|
|
85
|
+
@err.puts "Closest: #{suggestions.join(", ")}" unless suggestions.empty?
|
|
86
|
+
@err.puts "Run `plastic help` for the command list."
|
|
87
|
+
Command::USAGE
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def closest(name)
|
|
91
|
+
DidYouMean::SpellChecker.new(dictionary: @table.keys).correct(name)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require_relative "runner_policy"
|
|
8
|
+
|
|
9
|
+
# CodexAdapter (intent 340b, G7c, n6): the argv `codex exec` needs for one
|
|
10
|
+
# node's kind, and the mechanics of running it once - a bounded subprocess
|
|
11
|
+
# whose stdin carries the node input, whose stdout and `--output-last-message`
|
|
12
|
+
# file are the only two places a return can come from, and whose timeout
|
|
13
|
+
# kills the whole process group rather than one pid.
|
|
14
|
+
#
|
|
15
|
+
# Pure where it can be: sandbox_mode, git_dir_for_worktree, add_dir_args and
|
|
16
|
+
# build_argv touch nothing but the filesystem read they are explicitly given
|
|
17
|
+
# (the worktree's own `.git` file), never a git call and never a guess at a
|
|
18
|
+
# repository root. #execute is the one place this module actually spawns a
|
|
19
|
+
# child process; scripts/node-run is the only caller.
|
|
20
|
+
module CodexAdapter
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Verify and research get NO write access at all (spec D9): read-only, no
|
|
24
|
+
# `--add-dir`. Everything else - work, and any kind this table has never
|
|
25
|
+
# heard of - gets the widest treatment, matching RunnerPolicy's own
|
|
26
|
+
# unknown-kind-falls-back-to-work rule (matrix row 6.10).
|
|
27
|
+
READ_ONLY_KINDS = %w[verify research].freeze
|
|
28
|
+
|
|
29
|
+
def sandbox_mode(kind)
|
|
30
|
+
READ_ONLY_KINDS.include?(kind.to_s) ? "read-only" : "workspace-write"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The repository's OWN `.git` directory, derived from `worktree`'s `.git`
|
|
34
|
+
# FILE (a git worktree never carries a real `.git` directory of its own -
|
|
35
|
+
# spec) rather than any guess at how many path segments separate a
|
|
36
|
+
# worktree from its repo (matrix row 6.6). `.git` reads
|
|
37
|
+
# `gitdir: <repo>/.git/worktrees/<name>`; two directories up from that
|
|
38
|
+
# target is the repository's real `.git`. Returns nil when `worktree` is
|
|
39
|
+
# not a git worktree at all (no `.git` file, or `.git` is a directory,
|
|
40
|
+
# meaning `worktree` is a repository's own primary checkout) - callers
|
|
41
|
+
# treat that as "no `--add-dir` to add", never as a reason to guess.
|
|
42
|
+
def git_dir_for_worktree(worktree)
|
|
43
|
+
gitfile = File.join(worktree.to_s, ".git")
|
|
44
|
+
return nil unless File.file?(gitfile)
|
|
45
|
+
|
|
46
|
+
content = File.read(gitfile)
|
|
47
|
+
m = content.match(/\Agitdir:\s*(.+?)\s*\z/m)
|
|
48
|
+
return nil unless m
|
|
49
|
+
|
|
50
|
+
worktrees_entry = File.expand_path(m[1], worktree.to_s)
|
|
51
|
+
File.dirname(File.dirname(worktrees_entry))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# [] for a read-only kind (matrix row 6.4): `-C` already makes the
|
|
55
|
+
# worktree writable, and `--add-dir` on top of `--sandbox read-only` would
|
|
56
|
+
# contradict the sandbox mode itself. Exactly one `--add-dir`, the
|
|
57
|
+
# repository's own `.git`, for every other kind (matrix row 6.5) - nil
|
|
58
|
+
# when the worktree's own gitdir pointer cannot be resolved, so a caller
|
|
59
|
+
# handed a plain (non-worktree) directory never gets a bogus argument.
|
|
60
|
+
def add_dir_args(kind:, worktree:)
|
|
61
|
+
return [] if READ_ONLY_KINDS.include?(kind.to_s)
|
|
62
|
+
|
|
63
|
+
git_dir = git_dir_for_worktree(worktree)
|
|
64
|
+
git_dir ? ["--add-dir", git_dir] : []
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The whole `codex exec` argv for one node (matrix rows 6.1-6.9): `-C` at
|
|
68
|
+
# the worktree given (row 6.8), the sandbox mode and `--add-dir` this
|
|
69
|
+
# kind's row calls for and nothing else (row 6.7 - never the intent
|
|
70
|
+
# worktree, never `~/.plastic`), `--output-last-message` at the path the
|
|
71
|
+
# caller names, and a bare `-` so the prompt is read from stdin (row 6.9) -
|
|
72
|
+
# the node input itself never rides in this array.
|
|
73
|
+
def build_argv(kind:, worktree:, output_last_message:, model: nil, effort: nil)
|
|
74
|
+
argv = [
|
|
75
|
+
"codex", "exec",
|
|
76
|
+
"-C", worktree.to_s,
|
|
77
|
+
"--sandbox", sandbox_mode(kind),
|
|
78
|
+
*add_dir_args(kind: kind, worktree: worktree),
|
|
79
|
+
]
|
|
80
|
+
argv += ["--model", model.to_s] unless model.to_s.strip.empty?
|
|
81
|
+
argv += ["--config", %(model_reasoning_effort="#{effort}")] unless effort.to_s.strip.empty?
|
|
82
|
+
argv + ["--output-last-message", output_last_message.to_s, "-"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# matrix row 6.23: the wall-clock bound comes from the KIND's own lease
|
|
86
|
+
# length (RunnerPolicy.lease_minutes), never one shared constant - a long
|
|
87
|
+
# `work` build must not be capped as tightly as a quick `verify` pass.
|
|
88
|
+
def timeout_seconds(kind)
|
|
89
|
+
RunnerPolicy.lease_minutes(kind) * 60
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The return text (matrix rows 6.18/6.19): `output_last_message_path`'s
|
|
93
|
+
# own content when it exists and is non-blank (row 6.18 - stdout is never
|
|
94
|
+
# even inspected on this path), else the last YAML document found in
|
|
95
|
+
# `stdout` (row 6.19), else nil (no return anywhere - row 6.20 is the
|
|
96
|
+
# caller's problem, not this method's).
|
|
97
|
+
def read_return(output_last_message_path:, stdout:)
|
|
98
|
+
from_file = file_present_content(output_last_message_path)
|
|
99
|
+
return from_file unless from_file.nil?
|
|
100
|
+
|
|
101
|
+
stdout_fallback(stdout)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The LAST YAML document in `text` that parses to a mapping, or nil when
|
|
105
|
+
# none does (row 6.20's other half). Documents are separated the ordinary
|
|
106
|
+
# YAML way, a line holding only `---`; a stream with no such separator at
|
|
107
|
+
# all is treated as one candidate document. Every candidate is tried from
|
|
108
|
+
# the end, so three documents (one row names explicitly) still resolve to
|
|
109
|
+
# the last one, and prose or a fragment that never parses to a mapping is
|
|
110
|
+
# skipped rather than returned as the "document".
|
|
111
|
+
def stdout_fallback(text)
|
|
112
|
+
return nil if text.to_s.strip.empty?
|
|
113
|
+
|
|
114
|
+
segments = text.to_s.split(/^---[ \t]*$/m).map(&:strip).reject(&:empty?)
|
|
115
|
+
segments = [text.to_s.strip] if segments.empty?
|
|
116
|
+
|
|
117
|
+
segments.reverse_each do |segment|
|
|
118
|
+
return segment if safe_yaml(segment).is_a?(Hash)
|
|
119
|
+
end
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Runs `argv` once, feeding `stdin_data` on stdin through a scratch file
|
|
124
|
+
# (never a pipe - the same "never deadlock the read side" reasoning
|
|
125
|
+
# HookReplay.run_bounded already carries), bounded to `timeout_seconds`.
|
|
126
|
+
# `argv`'s own child becomes its own process group (`pgroup: true`) so a
|
|
127
|
+
# timeout can kill every child `codex exec` spawned along the way, not
|
|
128
|
+
# just the pid this call started (matrix row 6.24) - `codex exec` itself
|
|
129
|
+
# spawns shells for the commands it runs, and those would otherwise
|
|
130
|
+
# outlive a plain `Process.kill` on the parent alone, holding the
|
|
131
|
+
# worktree.
|
|
132
|
+
#
|
|
133
|
+
# Returns {exit_code:, timed_out:, stdout:, message:}. `message` is
|
|
134
|
+
# #read_return's result, computed only when the call did not time out - a
|
|
135
|
+
# timed-out child's partial output is never trusted (matrix row 6.22).
|
|
136
|
+
def execute(argv, stdin_data:, timeout_seconds:, output_last_message_path:)
|
|
137
|
+
Dir.mktmpdir("plastic-node-run-") do |scratch|
|
|
138
|
+
in_path = File.join(scratch, "stdin")
|
|
139
|
+
out_path = File.join(scratch, "stdout")
|
|
140
|
+
err_path = File.join(scratch, "stderr")
|
|
141
|
+
File.binwrite(in_path, stdin_data.to_s)
|
|
142
|
+
|
|
143
|
+
pid = Process.spawn(*argv.map(&:to_s), in: in_path, out: out_path, err: err_path, pgroup: true)
|
|
144
|
+
timed_out = false
|
|
145
|
+
exit_code =
|
|
146
|
+
begin
|
|
147
|
+
Timeout.timeout(timeout_seconds) do
|
|
148
|
+
Process.wait(pid)
|
|
149
|
+
$?.exitstatus
|
|
150
|
+
end
|
|
151
|
+
rescue Timeout::Error
|
|
152
|
+
timed_out = true
|
|
153
|
+
kill_group(pid)
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
stdout = File.exist?(out_path) ? File.read(out_path) : ""
|
|
158
|
+
message = timed_out ? nil : read_return(output_last_message_path: output_last_message_path, stdout: stdout)
|
|
159
|
+
|
|
160
|
+
{ exit_code: exit_code, timed_out: timed_out, stdout: stdout, message: message }
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# --- internals ---------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
def file_present_content(path)
|
|
167
|
+
return nil if path.nil? || !File.exist?(path)
|
|
168
|
+
|
|
169
|
+
content = File.read(path)
|
|
170
|
+
content.strip.empty? ? nil : content
|
|
171
|
+
end
|
|
172
|
+
private_class_method :file_present_content
|
|
173
|
+
|
|
174
|
+
def safe_yaml(text)
|
|
175
|
+
YAML.safe_load(text, aliases: false, permitted_classes: [])
|
|
176
|
+
rescue Psych::Exception, ArgumentError
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
private_class_method :safe_yaml
|
|
180
|
+
|
|
181
|
+
# Kills the whole process GROUP `pid` leads, not just `pid` itself (matrix
|
|
182
|
+
# row 6.24): a negative pid signals the group. Rescued and reaped exactly
|
|
183
|
+
# like HookReplay.kill_and_reap - a process that exited in the race
|
|
184
|
+
# between the timeout firing and this call raises Errno::ESRCH on the kill
|
|
185
|
+
# or Errno::ECHILD on the wait, neither of which should ever propagate.
|
|
186
|
+
def kill_group(pid)
|
|
187
|
+
Process.kill("KILL", -pid)
|
|
188
|
+
rescue StandardError
|
|
189
|
+
nil
|
|
190
|
+
ensure
|
|
191
|
+
begin
|
|
192
|
+
Process.wait(pid)
|
|
193
|
+
rescue StandardError
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
private_class_method :kill_group
|
|
198
|
+
end
|
|
@@ -19,23 +19,31 @@ require "digest"
|
|
|
19
19
|
#
|
|
20
20
|
# Library only: no CLI, no ENV, no I/O.
|
|
21
21
|
module CompactInstructions
|
|
22
|
-
#
|
|
23
|
-
OFFER_TOKENS =
|
|
24
|
-
INSIST_TOKENS =
|
|
22
|
+
# 15 and 25 percent of a 1M window (intent 355, n5, D7).
|
|
23
|
+
OFFER_TOKENS = 150_000
|
|
24
|
+
INSIST_TOKENS = 250_000
|
|
25
25
|
|
|
26
26
|
# Static on purpose. A body rendered from the user's config would change its hash
|
|
27
27
|
# every time they edited config.yml, and doctor would then report a correct install
|
|
28
28
|
# as stale, so the block states the shipped numbers and names the two keys as the
|
|
29
29
|
# override instead. It names the hand-off in words and never by path, so it reads
|
|
30
30
|
# correctly whether or not the hand-off writer is installed.
|
|
31
|
+
# The `@` line is a Claude Code import: the harness reads the named file into the
|
|
32
|
+
# session. It is the whole of ruling D43, that PLASTIC.md is the only instruction text
|
|
33
|
+
# Plastic carries. It names the installed copy under the Plastic home, because a bare
|
|
34
|
+
# `@PLASTIC.md` would resolve against ~/.claude, where no such file exists.
|
|
31
35
|
BODY = <<~MD.freeze
|
|
36
|
+
Plastic runs from one command, `plastic`. Its instructions are one page, imported here:
|
|
37
|
+
|
|
38
|
+
@~/.plastic/PLASTIC.md
|
|
39
|
+
|
|
32
40
|
Plastic watches this session's context. When the harness reports how much of the
|
|
33
41
|
window is used:
|
|
34
42
|
|
|
35
|
-
- At
|
|
43
|
+
- At 150,000 tokens, offer to compact. Say that the hand-off in today's day ledger
|
|
36
44
|
is written and current, and take no for an answer: a task that is nearly done
|
|
37
45
|
does not need the interruption.
|
|
38
|
-
- At
|
|
46
|
+
- At 250,000 tokens, insist. Take no new work, write the hand-off in today's day
|
|
39
47
|
ledger, and compact before continuing.
|
|
40
48
|
- After a compaction, say continue. The day summary at boot and the hand-off carry
|
|
41
49
|
the state; do not rebuild it by re-reading files.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "digest"
|
|
6
|
+
|
|
7
|
+
# CoreIntegrity (intent 340, G7, n1): re-hashes the files an installed
|
|
8
|
+
# ~/.plastic/manifest.json lists, so the runner's own "am I running trusted
|
|
9
|
+
# code" check has one implementation. scripts/doctor.rb's
|
|
10
|
+
# Doctor#check_install_integrity does this same arithmetic inline today,
|
|
11
|
+
# per-agent (Codex/Hermes/Claude), against each agent's own manifest; this
|
|
12
|
+
# module is not a refactor of that (out of scope for this node), it exists
|
|
13
|
+
# so the runner - and a later node that points doctor at it - never have to
|
|
14
|
+
# reimplement the same hashing loop a third time.
|
|
15
|
+
#
|
|
16
|
+
# Pure and side-effect-free: `check` takes plastic_home: and never raises
|
|
17
|
+
# across its boundary. A missing, unreadable, or non-JSON manifest.json is a
|
|
18
|
+
# named refusal (`reason:`), never an exception. A manifest-listed file that
|
|
19
|
+
# no longer exists is reported under `missing`, distinct from `drifted` (a
|
|
20
|
+
# file that exists but no longer hashes to the manifest's recorded value).
|
|
21
|
+
module CoreIntegrity
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def check(plastic_home:)
|
|
25
|
+
manifest_path = File.join(plastic_home.to_s, "manifest.json")
|
|
26
|
+
return refusal("manifest.json not found at #{manifest_path}") unless File.exist?(manifest_path)
|
|
27
|
+
|
|
28
|
+
raw = begin
|
|
29
|
+
File.read(manifest_path)
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
return refusal("manifest.json is unreadable: #{e.message}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
data = begin
|
|
35
|
+
JSON.parse(raw)
|
|
36
|
+
rescue JSON::ParserError => e
|
|
37
|
+
return refusal("manifest.json is not valid JSON: #{e.message}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
files = data.is_a?(Hash) ? data["files"] : nil
|
|
41
|
+
return refusal("manifest.json's files: is not a mapping") unless files.is_a?(Hash)
|
|
42
|
+
|
|
43
|
+
drifted = []
|
|
44
|
+
missing = []
|
|
45
|
+
files.each do |path, expected_hash|
|
|
46
|
+
unless File.exist?(path)
|
|
47
|
+
missing << path
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# v1 minor 6/row 11.16: a tracked file that exists but cannot be read
|
|
52
|
+
# (permissions changed under this process) must never raise out of a
|
|
53
|
+
# module whose whole contract is "never raises across its boundary" -
|
|
54
|
+
# unreadable is reported the same way a hash mismatch is: this
|
|
55
|
+
# process cannot confirm the file matches what the manifest expects,
|
|
56
|
+
# which is exactly what `drifted` already means.
|
|
57
|
+
actual_hash = begin
|
|
58
|
+
Digest::SHA256.file(path).hexdigest
|
|
59
|
+
rescue SystemCallError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
drifted << path if actual_hash.nil? || actual_hash != expected_hash
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
{ ok: drifted.empty? && missing.empty?, drifted: drifted, missing: missing, reason: nil }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def refusal(reason)
|
|
69
|
+
{ ok: false, drifted: [], missing: [], reason: reason }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "report_screen"
|
|
5
|
+
|
|
6
|
+
# DashboardScreen (intent 331d) - the presentation half of the dashboard
|
|
7
|
+
# screen. scripts/dashboard.rb's screen_fields sources every fact (Active, In
|
|
8
|
+
# delivery, Delivered, Roadmap, Sessions, Changed, the two capped row lists)
|
|
9
|
+
# through the same helpers report-screen and DaySummary already use, turning
|
|
10
|
+
# a missing source into "not recorded" or "none" before it ever reaches
|
|
11
|
+
# here; this module carries no data-sourcing logic of its own, only layout,
|
|
12
|
+
# exactly like ReportScreen.render_state and IntentScreen.render do for
|
|
13
|
+
# their own screens.
|
|
14
|
+
module DashboardScreen
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
TEMPLATE_PATH = File.expand_path("../../templates/dashboard-screen.md", __dir__)
|
|
18
|
+
|
|
19
|
+
def render(fields, template: nil)
|
|
20
|
+
out = (template || File.read(TEMPLATE_PATH)).dup
|
|
21
|
+
out = out.gsub("{{scope}}", fields.fetch(:scope).to_s)
|
|
22
|
+
out = out.gsub("{{active}}", fields.fetch(:active).to_s)
|
|
23
|
+
out = out.gsub("{{in_delivery}}", fields.fetch(:in_delivery).to_s)
|
|
24
|
+
out = out.gsub("{{delivered}}", fields.fetch(:delivered).to_s)
|
|
25
|
+
out = out.gsub("{{roadmap}}", fields.fetch(:roadmap).to_s)
|
|
26
|
+
out = out.gsub("{{sessions}}", fields.fetch(:sessions).to_s)
|
|
27
|
+
out = out.gsub("{{changed}}", fields.fetch(:changed).to_s)
|
|
28
|
+
out = out.gsub("{{where_we_are.rows}}", where_we_are_rows(fields.fetch(:where_we_are, [])))
|
|
29
|
+
out = out.gsub("{{where_we_go_next.rows}}", where_we_go_next_rows(fields.fetch(:where_we_go_next, [])))
|
|
30
|
+
ReportScreen.fit_screen(out.gsub(/\n{3,}/, "\n\n"))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def where_we_are_rows(rows)
|
|
34
|
+
rows.map { |r| "| #{r[:graph_id]} | #{r[:intent]} | #{r[:stage]} | #{r[:progress]} | #{r[:lead]} |" }.join("\n")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def where_we_go_next_rows(rows)
|
|
38
|
+
rows.map { |r| "| #{r[:rank]} | #{r[:graph_id]} | #{r[:intent]} | #{r[:reason]} |" }.join("\n")
|
|
39
|
+
end
|
|
40
|
+
end
|