@zalom/plastic 2.0.0-alpha.27 → 2.0.0-alpha.29
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 -139
- package/README.md +345 -133
- package/agents/plastic-enforcer.md +9 -10
- package/agents/plastic-executor.md +1 -1
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +35 -1
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +6 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/verify-change +345 -0
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +4 -7
- package/{skills/auto/references → docs/help}/agent-architecture.md +7 -7
- package/{skills/conventions/references → docs/help}/completion-and-done.md +1 -1
- package/{skills/auto/references → docs/help}/human-report-contract.md +17 -19
- package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +5 -5
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/package.json +3 -2
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +10 -9
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +48 -42
- package/scripts/end-intent +2 -8
- package/scripts/file-session-intent +2 -1
- package/scripts/hook-capture +4 -3
- package/scripts/hook-close +2 -1
- package/scripts/hook-record +3 -2
- package/scripts/hook-savepoint +3 -2
- package/scripts/hook-session-start +5 -4
- package/scripts/hook-stop +2 -1
- package/scripts/insight-append +1 -2
- package/scripts/install.rb +3 -1
- package/scripts/lib/active_delivery.rb +1 -1
- package/scripts/lib/arm.rb +2 -1
- package/scripts/lib/backup.rb +65 -0
- package/scripts/lib/cli/command.rb +85 -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 +60 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +25 -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 +20 -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 +61 -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 +25 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +43 -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 +49 -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 +25 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +44 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +34 -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 +84 -0
- package/scripts/lib/cli/legacy.rb +50 -0
- package/scripts/lib/cli/output.rb +102 -0
- package/scripts/lib/cli/scope.rb +127 -0
- package/scripts/lib/cli/table.rb +64 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/compact_instructions.rb +8 -0
- package/scripts/lib/day_summary.rb +4 -3
- package/scripts/lib/doctor_core.rb +7 -32
- package/scripts/lib/doctor_session_ledger.rb +2 -1
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_measure_models.rb +3 -1
- package/scripts/lib/index_entry.rb +9 -0
- package/scripts/lib/installer_core.rb +73 -19
- package/scripts/lib/intent_screen.rb +3 -3
- package/scripts/lib/lock.rb +2 -2
- package/scripts/lib/node_input.rb +3 -2
- package/scripts/lib/preflight.rb +4 -6
- package/scripts/lib/project_config.rb +2 -1
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +2 -0
- package/scripts/lib/report_screen.rb +4 -3
- 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_queue.rb +2 -2
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/runner_absorb.rb +3 -2
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_git.rb +4 -3
- package/scripts/lib/sqlite.rb +22 -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 +2 -7
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/worktree.rb +3 -8
- package/scripts/lib/worktree_sweep.rb +3 -2
- package/scripts/link-suggest +2 -1
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +3 -12
- package/scripts/plastic-lock +3 -2
- package/scripts/promote-session-item +3 -2
- package/scripts/release-check +10 -5
- package/scripts/report-screen +1 -1
- package/scripts/session-commit +2 -1
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +25 -4
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +6 -6
- package/templates/render.css +10 -0
- package/bin/plastic.js +0 -70
- package/skills/agent-advisor/SKILL.md +0 -84
- package/skills/auto/SKILL.md +0 -297
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -64
- package/skills/conventions/SKILL.md +0 -29
- package/skills/dashboard/SKILL.md +0 -180
- 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 -305
- 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 -215
- package/skills/intent-continuing/SKILL.md +0 -156
- package/skills/intent-continuing/references/board-fill.md +0 -52
- package/skills/intent-continuing/references/boarding-matrix.md +0 -35
- 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 -89
- 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 -182
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -87
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -47
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -136
- 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 -376
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -70
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -90
- package/skills/roadmap/references/file-format.md +0 -134
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -66
- 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}/knowledge-graph.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +0 -0
- /package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
package/scripts/hook-record
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
# lands, and calls the session-commit seam when that script exists. Always
|
|
11
11
|
# exits 0 (spec D3).
|
|
12
12
|
|
|
13
|
+
require_relative "lib/store_layout"
|
|
13
14
|
require "json"
|
|
14
15
|
require "timeout"
|
|
15
16
|
require "fileutils"
|
|
@@ -77,7 +78,7 @@ skip_day_ledger = inside_plastic || !intent_dir_abs.nil?
|
|
|
77
78
|
# --- (c) promote the newest pending line, Item savepoint, seam -------------
|
|
78
79
|
unless skip_day_ledger
|
|
79
80
|
begin
|
|
80
|
-
store =
|
|
81
|
+
store = Plastic::StoreLayout.global_store(plastic_home)
|
|
81
82
|
sid = SessionLedger.short_session_id(nil, session_id)
|
|
82
83
|
today = SessionLedger.day_id
|
|
83
84
|
|
|
@@ -152,7 +153,7 @@ end
|
|
|
152
153
|
# the heartbeat is not written and no directory is created here either --
|
|
153
154
|
# the session tmp directory, created by session start, is the guard.
|
|
154
155
|
begin
|
|
155
|
-
store ||=
|
|
156
|
+
store ||= Plastic::StoreLayout.global_store(plastic_home)
|
|
156
157
|
sid ||= SessionLedger.short_session_id(nil, session_id)
|
|
157
158
|
if Dir.exist?(SessionLedger.session_tmp_dir(store, sid))
|
|
158
159
|
File.write(SessionLedger.heartbeat_path(store, sid), "#{Time.now.utc.iso8601}\n")
|
package/scripts/hook-savepoint
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
#
|
|
14
14
|
# Usage: hook-savepoint <plastic_home> (stdin: the hook JSON)
|
|
15
15
|
|
|
16
|
+
require_relative "lib/store_layout"
|
|
16
17
|
require "json"
|
|
17
18
|
require_relative "lib/session_ledger"
|
|
18
19
|
require_relative "lib/handoff"
|
|
@@ -31,7 +32,7 @@ MESSAGE = "PLASTIC SAVEPOINT - context is being compacted. The hand-off for this
|
|
|
31
32
|
def resolve_intent(plastic_home, session_id, read_config_path)
|
|
32
33
|
return nil if session_id.strip.empty?
|
|
33
34
|
|
|
34
|
-
global_store =
|
|
35
|
+
global_store = Plastic::StoreLayout.global_store(plastic_home)
|
|
35
36
|
roots_raw = IO.popen({ "PLASTIC_HOME" => plastic_home }, [read_config_path, "project_roots"],
|
|
36
37
|
err: File::NULL, &:read).to_s.strip
|
|
37
38
|
project_roots = roots_raw.empty? ? [] : Array(JSON.parse(roots_raw))
|
|
@@ -51,7 +52,7 @@ begin
|
|
|
51
52
|
|
|
52
53
|
unless plastic_home.empty? || session_id.strip.empty?
|
|
53
54
|
home = File.expand_path(plastic_home)
|
|
54
|
-
store =
|
|
55
|
+
store = Plastic::StoreLayout.global_store(home)
|
|
55
56
|
session = SessionLedger.short_session_id(session_id, nil)
|
|
56
57
|
day = Handoff.day_for(store, session, today: SessionLedger.day_id)
|
|
57
58
|
read_config_path = File.expand_path("read-config", __dir__)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Usage: hook-session-start <index_path> <plastic_home> <mode> [plugin_root]
|
|
4
4
|
# Outputs hook JSON for the session-start hook. Exits silently if nothing to show.
|
|
5
5
|
|
|
6
|
+
require_relative "lib/store_layout"
|
|
6
7
|
require "json"
|
|
7
8
|
require "date"
|
|
8
9
|
require "yaml"
|
|
@@ -58,7 +59,7 @@ end
|
|
|
58
59
|
# home (~/.plastic) as argument 2; the store lives one level below it. Compose the
|
|
59
60
|
# store exactly once here, so no later line re-derives it and no path can gain a
|
|
60
61
|
# doubled store/ segment.
|
|
61
|
-
store_dir =
|
|
62
|
+
store_dir = Plastic::StoreLayout.global_store(plastic_home)
|
|
62
63
|
|
|
63
64
|
# --- Current version ---
|
|
64
65
|
# Read early, before any of the richer INDEX/project parsing below, so the
|
|
@@ -154,7 +155,7 @@ begin
|
|
|
154
155
|
project_path = File.expand_path(info["path"])
|
|
155
156
|
if cwd.start_with?(project_path)
|
|
156
157
|
current_project = { "slug" => slug, "parent" => info["parent"], "path" => project_path }
|
|
157
|
-
project_index =
|
|
158
|
+
project_index = File.join(Plastic::StoreLayout.project_root(plastic_home, slug), "INDEX.md")
|
|
158
159
|
if File.exist?(project_index)
|
|
159
160
|
p_section = nil
|
|
160
161
|
File.readlines(project_index).each do |pline|
|
|
@@ -220,7 +221,7 @@ begin
|
|
|
220
221
|
if File.exist?(cache_file)
|
|
221
222
|
cache = JSON.parse(File.read(cache_file)) rescue {}
|
|
222
223
|
if cache["updateAvailable"]
|
|
223
|
-
update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run
|
|
224
|
+
update_notice = "Plastic update available: #{cache["current"]} -> #{cache["latest"]} — run `plastic update`"
|
|
224
225
|
end
|
|
225
226
|
end
|
|
226
227
|
|
|
@@ -291,7 +292,7 @@ begin
|
|
|
291
292
|
if plastic_md && !subagent_session
|
|
292
293
|
if current_project
|
|
293
294
|
slug = current_project["slug"]
|
|
294
|
-
banner = "Project: #{slug} | Store:
|
|
295
|
+
banner = "Project: #{slug} | Store: #{File.join(Plastic::StoreLayout.project_root(plastic_home, slug), "store").sub(Dir.home, "~")}/"
|
|
295
296
|
if project_active.any? && project_active.first =~ /\[([^\]]+)\].*store\/([\w-]+)\//
|
|
296
297
|
intent_name, dir_name = $1, $2
|
|
297
298
|
intent_id = dir_name.split("--").first
|
package/scripts/hook-stop
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
#
|
|
15
15
|
# Usage: hook-stop <plastic_home> (stdin: the hook JSON)
|
|
16
16
|
|
|
17
|
+
require_relative "lib/store_layout"
|
|
17
18
|
require "json"
|
|
18
19
|
require_relative "lib/stop_gate"
|
|
19
20
|
|
|
@@ -32,7 +33,7 @@ begin
|
|
|
32
33
|
payload = {} unless payload.is_a?(Hash)
|
|
33
34
|
|
|
34
35
|
home = File.expand_path(plastic_home)
|
|
35
|
-
global_store =
|
|
36
|
+
global_store = Plastic::StoreLayout.global_store(home)
|
|
36
37
|
read_config = File.expand_path("read-config", __dir__)
|
|
37
38
|
|
|
38
39
|
config_stop_hook = read_config_value("runner.stop_hook", home, read_config)
|
package/scripts/insight-append
CHANGED
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
# ever reaches Insights.append_insight; the library itself stays untouched,
|
|
17
17
|
# since a tag is a text-level convention, not a new field on the ledger.
|
|
18
18
|
# Doctor's unpromoted_rules check (scripts/doctor.rb) later lists any tagged
|
|
19
|
-
# entry whose exact text no
|
|
20
|
-
# carries yet.
|
|
19
|
+
# entry whose exact text no docs/help/*.md chapter carries yet.
|
|
21
20
|
#
|
|
22
21
|
# Usage:
|
|
23
22
|
# insight-append <intent_dir> <text> --stage S --author A [--rule]
|
package/scripts/install.rb
CHANGED
|
@@ -75,12 +75,14 @@ class Install < InstallerCore
|
|
|
75
75
|
# they are reported, never re-installed, so the summary line and the Codex
|
|
76
76
|
# trust reminder only ever count agents that actually changed this run.
|
|
77
77
|
def run(selected:, force: false, reinstall: false, ledger_action: nil, argv: ARGV, input: $stdin,
|
|
78
|
-
already_registered: [])
|
|
78
|
+
already_registered: [], qmd_runner: QmdSync.default_runner, qmd_detector: QmdSync.method(:detect))
|
|
79
79
|
fresh = !installed?
|
|
80
80
|
mode = fresh ? :install : :update # :update here means "re-sync, skip bootstrap"
|
|
81
81
|
|
|
82
82
|
distribute(mode)
|
|
83
83
|
bootstrap if fresh
|
|
84
|
+
git_init_if_absent
|
|
85
|
+
register_with_qmd(runner: qmd_runner, detector: qmd_detector)
|
|
84
86
|
migrate_advisor_config_file(File.join(plastic_home, "config.yml"))
|
|
85
87
|
apply_config_flags(argv)
|
|
86
88
|
|
|
@@ -60,7 +60,7 @@ module ActiveDelivery
|
|
|
60
60
|
value.nil? || value.to_s.strip.empty?
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
DEFAULT_PROJECT_ROOTS = ["~/.plastic/projects"].freeze
|
|
63
|
+
DEFAULT_PROJECT_ROOTS = ["~/.plastic/projects", "~/.plastic/stores"].freeze
|
|
64
64
|
|
|
65
65
|
# The configured project roots (344 n2, D6): the `project_roots` list in
|
|
66
66
|
# `<plastic_home>/config.yml`, each expanded, or the default
|
package/scripts/lib/arm.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "digest"
|
|
5
6
|
require "fileutils"
|
|
6
7
|
require_relative "lock"
|
|
@@ -63,7 +64,7 @@ module Arm
|
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
def global_store(home)
|
|
66
|
-
File.join(File.expand_path(home), ".plastic"
|
|
67
|
+
Plastic::StoreLayout.global_store(File.join(File.expand_path(home), ".plastic"))
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
# The minimal hash Worktree.provision, release, and finish consume: the
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "json"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "zlib"
|
|
8
|
+
require_relative "store_sync"
|
|
9
|
+
require_relative "worktree"
|
|
10
|
+
|
|
11
|
+
module Plastic
|
|
12
|
+
module Backup
|
|
13
|
+
DATABASES = [SearchIndex::NAME, WorkGraph::NAME, ReferenceArchive::NAME].freeze
|
|
14
|
+
ROOT_FILES = %w[config.yml projects.yml INDEX.md].freeze
|
|
15
|
+
Missing = Class.new(StandardError)
|
|
16
|
+
|
|
17
|
+
def self.dir(home)
|
|
18
|
+
File.join(home, "backups")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.list(home)
|
|
22
|
+
Dir.glob(File.join(dir(home), "plastic-backup--*.tar.gz")).sort
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.call(home, version:, now: Time.now.utc)
|
|
26
|
+
missing = DATABASES.reject { |name| File.exist?(File.join(home, name)) }
|
|
27
|
+
raise Missing, missing.join(", ") unless missing.empty?
|
|
28
|
+
|
|
29
|
+
Worktree.ensure_gitignored(home, "backups/")
|
|
30
|
+
Dir.mktmpdir("plastic-backup") do |work|
|
|
31
|
+
DATABASES.each { |name| Sqlite.call(File.join(home, name), "VACUUM INTO #{Sqlite.quote(File.join(work, name))};", readonly: true) }
|
|
32
|
+
FileUtils.cp(ROOT_FILES.map { |name| File.join(home, name) }.select { |path| File.exist?(path) }, work)
|
|
33
|
+
File.write(File.join(work, "manifest.json"), JSON.pretty_generate(manifest(work, version, now)))
|
|
34
|
+
pack(work, File.join(dir(home), "plastic-backup--#{now.strftime("%Y%m%dT%H%M%SZ")}.tar.gz"))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.manifest(work, version, now)
|
|
39
|
+
files = Dir.children(work).sort.map do |name|
|
|
40
|
+
path = File.join(work, name)
|
|
41
|
+
{"name" => name, "bytes" => File.size(path), "sha256" => Digest::SHA256.file(path).hexdigest}
|
|
42
|
+
end
|
|
43
|
+
{"version" => version, "at" => now.strftime("%Y-%m-%dT%H:%M:%SZ"), "files" => files}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.pack(work, archive)
|
|
47
|
+
require "rubygems"
|
|
48
|
+
require "rubygems/package"
|
|
49
|
+
FileUtils.mkdir_p(File.dirname(archive))
|
|
50
|
+
draft = "#{archive}.tmp"
|
|
51
|
+
Zlib::GzipWriter.open(draft) do |gzip|
|
|
52
|
+
Gem::Package::TarWriter.new(gzip) do |tar|
|
|
53
|
+
Dir.children(work).sort.each do |name|
|
|
54
|
+
path = File.join(work, name)
|
|
55
|
+
tar.add_file_simple(name, 0o644, File.size(path)) { |entry| IO.copy_stream(path, entry) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
File.rename(draft, archive)
|
|
60
|
+
archive
|
|
61
|
+
ensure
|
|
62
|
+
FileUtils.rm_f(draft.to_s)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "output"
|
|
5
|
+
require_relative "scope"
|
|
6
|
+
|
|
7
|
+
module Plastic
|
|
8
|
+
class CLI
|
|
9
|
+
class Command
|
|
10
|
+
OK = 0
|
|
11
|
+
FAILED = 1
|
|
12
|
+
USAGE = 2
|
|
13
|
+
REFUSED = 3
|
|
14
|
+
|
|
15
|
+
Usage = Class.new(StandardError)
|
|
16
|
+
Refusal = Class.new(StandardError)
|
|
17
|
+
Failure = Class.new(StandardError)
|
|
18
|
+
|
|
19
|
+
def self.call(...)
|
|
20
|
+
command = new(...)
|
|
21
|
+
command.call
|
|
22
|
+
command.flush
|
|
23
|
+
OK
|
|
24
|
+
rescue OptionParser::ParseError, Scope::UnknownProject, Usage => e
|
|
25
|
+
command.output.usage(e.message, self::USAGE_LINE)
|
|
26
|
+
USAGE
|
|
27
|
+
rescue Refusal => e
|
|
28
|
+
command.output.refused(e.message)
|
|
29
|
+
REFUSED
|
|
30
|
+
rescue Failure => e
|
|
31
|
+
command.output.failed(e.message)
|
|
32
|
+
FAILED
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_reader :output
|
|
36
|
+
|
|
37
|
+
def initialize(argv, out:, err:, env: ENV, home: Dir.home, directory: Dir.pwd)
|
|
38
|
+
@argv = argv.dup
|
|
39
|
+
@env = env
|
|
40
|
+
@home = home
|
|
41
|
+
@directory = directory
|
|
42
|
+
@output = Output.new(out: out, err: err)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def call
|
|
46
|
+
raise NoMethodError, "#{self.class} must define call"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def flush
|
|
50
|
+
@output.flush(json: options[:json])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def options
|
|
56
|
+
return @options if @options
|
|
57
|
+
|
|
58
|
+
@options = {}
|
|
59
|
+
parser.parse!(@argv)
|
|
60
|
+
@options
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def arguments
|
|
64
|
+
options
|
|
65
|
+
@argv
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parser
|
|
69
|
+
@parser ||= OptionParser.new do |o|
|
|
70
|
+
o.banner = self.class::USAGE_LINE
|
|
71
|
+
o.on("--json") { @options[:json] = true }
|
|
72
|
+
switches(o)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def switches(_parser)
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def scope
|
|
81
|
+
@scope ||= Scope.new(env: @env, home: @home, slug: options[:project], directory: @directory)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "subcommand_list"
|
|
4
|
+
|
|
5
|
+
# `plastic auto` - the list of `plastic auto ...` subcommands, straight out of
|
|
6
|
+
# the table.
|
|
7
|
+
module Plastic
|
|
8
|
+
class CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class Auto < SubcommandList
|
|
11
|
+
USAGE_LINE = "plastic auto SUBCOMMAND [options]"
|
|
12
|
+
PREFIX = "auto"
|
|
13
|
+
NEXT = "plastic auto take ID"
|
|
14
|
+
BECAUSE = "taking the intent is what every other auto subcommand needs first"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "intent_command"
|
|
4
|
+
|
|
5
|
+
# `plastic auto brief` - an intent's spawn preamble (`spawn-preamble`, run
|
|
6
|
+
# through Legacy). `--role advisor` also prints the three shapes an advisor
|
|
7
|
+
# request takes, so a dispatching lead never has to look them up.
|
|
8
|
+
module Plastic
|
|
9
|
+
class CLI
|
|
10
|
+
module Commands
|
|
11
|
+
class AutoBrief < IntentCommand
|
|
12
|
+
USAGE_LINE = "plastic auto brief ID [--role ROLE] [--json]"
|
|
13
|
+
|
|
14
|
+
SCRIPT = "spawn-preamble"
|
|
15
|
+
AFTER = "plastic auto report ID"
|
|
16
|
+
BECAUSE = "the report closes the turn the preamble opened"
|
|
17
|
+
|
|
18
|
+
ADVISOR_SHAPES =
|
|
19
|
+
"State the shape you need: a verdict plus the biggest risk for one bounded " \
|
|
20
|
+
"decision; a stepped plan plus a risk map for a plan or plan review; rival " \
|
|
21
|
+
"approaches and kill criteria for architecture, one-way doors, or deadlocks."
|
|
22
|
+
|
|
23
|
+
def call
|
|
24
|
+
super
|
|
25
|
+
@output.row("advisor request shapes", ADVISOR_SHAPES) if role == "advisor"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def role
|
|
31
|
+
options[:role]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def switches(parser)
|
|
35
|
+
parser.on("--role ROLE") { |v| @options[:role] = v }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def script_arguments
|
|
39
|
+
role ? [intent_dir, "--role", role] : [intent_dir]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../command"
|
|
4
|
+
require_relative "../legacy"
|
|
5
|
+
|
|
6
|
+
# `plastic auto lock` - inspects, repairs, or releases an intent's delivery
|
|
7
|
+
# lock (`plastic-lock status|fix|release --intent-dir DIR`, run through
|
|
8
|
+
# Legacy). The verb comes first, then the id, since the verb is what the
|
|
9
|
+
# table's two-token match consumes as part of the command name.
|
|
10
|
+
module Plastic
|
|
11
|
+
class CLI
|
|
12
|
+
module Commands
|
|
13
|
+
class AutoLock < Command
|
|
14
|
+
USAGE_LINE = "plastic auto lock status|fix|release ID [--json]"
|
|
15
|
+
|
|
16
|
+
VERBS = %w[status fix release].freeze
|
|
17
|
+
AFTER = {"status" => "plastic auto lock fix ID", "fix" => "plastic auto brief ID",
|
|
18
|
+
"release" => "plastic session summary"}.freeze
|
|
19
|
+
BECAUSE = {"status" => "a stale or held lock is repaired by fix, not by re-arming",
|
|
20
|
+
"fix" => "a repaired lock is ready for the preamble again",
|
|
21
|
+
"release" => "the summary is what a freed lock leads to"}.freeze
|
|
22
|
+
|
|
23
|
+
def initialize(argv, runner: nil, **streams)
|
|
24
|
+
super(argv, **streams)
|
|
25
|
+
@runner = runner
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call
|
|
29
|
+
raise Usage, "VERB must be one of #{VERBS.join(", ")}" unless VERBS.include?(verb)
|
|
30
|
+
|
|
31
|
+
status = legacy.run("plastic-lock", verb, "--intent-dir", intent_dir)
|
|
32
|
+
raise Failure, "plastic-lock exited #{status}" unless status.zero?
|
|
33
|
+
|
|
34
|
+
@output.next_step(AFTER.fetch(verb).sub("ID", id.to_s), because: BECAUSE.fetch(verb))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def verb
|
|
40
|
+
arguments[0]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def id
|
|
44
|
+
arguments[1]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def intent_dir
|
|
48
|
+
dir = scope.intent_dir(id)
|
|
49
|
+
raise Failure, "no intent #{id.inspect} in #{scope.slug}; plastic status lists the ones that exist" unless dir
|
|
50
|
+
|
|
51
|
+
dir
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def legacy
|
|
55
|
+
@legacy ||= Legacy.new(env: @env, runner: @runner)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "intent_command"
|
|
4
|
+
|
|
5
|
+
# `plastic auto report` - an intent's completion report (`agent-report`, run
|
|
6
|
+
# through Legacy), then the review-by-risk rules a lead checks it against.
|
|
7
|
+
module Plastic
|
|
8
|
+
class CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class AutoReport < IntentCommand
|
|
11
|
+
USAGE_LINE = "plastic auto report ID [--role ROLE] [--json]"
|
|
12
|
+
|
|
13
|
+
SCRIPT = "agent-report"
|
|
14
|
+
AFTER = "plastic auto lock release ID"
|
|
15
|
+
BECAUSE = "closing the lock is what a delivered or abandoned report leads to"
|
|
16
|
+
|
|
17
|
+
REVIEW_RULES = <<~TEXT.chomp
|
|
18
|
+
Review by risk, boot 3, only when a rule fires; otherwise the green suite is
|
|
19
|
+
the review. Dispatch the post-execution reviewer when any of these holds:
|
|
20
|
+
1. the diff touches a path on the risk list (hooks, the lock, the arming
|
|
21
|
+
module, the installer, a release file).
|
|
22
|
+
2. a failure-mode matrix row names a test not in the diff, or a test the
|
|
23
|
+
green run did not execute.
|
|
24
|
+
3. the completion report carries a status other than delivered, or a
|
|
25
|
+
non-empty deviations or blockers field.
|
|
26
|
+
The reviewer returns a pass or fixes; at most one review-fix round, ever.
|
|
27
|
+
TEXT
|
|
28
|
+
|
|
29
|
+
def call
|
|
30
|
+
super
|
|
31
|
+
@output.row("review rules", REVIEW_RULES)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def role
|
|
37
|
+
options[:role]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def switches(parser)
|
|
41
|
+
parser.on("--role ROLE") { |v| @options[:role] = v }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def script_arguments
|
|
45
|
+
role ? [intent_dir, "--role", role] : [intent_dir]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "intent_command"
|
|
4
|
+
|
|
5
|
+
# `plastic auto take` - arms the intent's delivery lock for this session
|
|
6
|
+
# (`plastic-lock arm --intent-dir DIR --mode auto`, run through Legacy).
|
|
7
|
+
module Plastic
|
|
8
|
+
class CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class AutoTake < IntentCommand
|
|
11
|
+
USAGE_LINE = "plastic auto take ID [--json]"
|
|
12
|
+
|
|
13
|
+
SCRIPT = "plastic-lock"
|
|
14
|
+
AFTER = "plastic auto brief ID"
|
|
15
|
+
BECAUSE = "the preamble is the live state a taken intent is read from next"
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def script_arguments
|
|
20
|
+
["arm", "--intent-dir", intent_dir, "--mode", "auto"]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../command"
|
|
4
|
+
require_relative "version"
|
|
5
|
+
require_relative "../../backup"
|
|
6
|
+
|
|
7
|
+
module Plastic
|
|
8
|
+
class CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class Backup < Command
|
|
11
|
+
USAGE_LINE = "plastic backup [--list] [--json]"
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
options[:list] ? list : write
|
|
15
|
+
rescue Plastic::Backup::Missing => e
|
|
16
|
+
@output.next_step("plastic sync", because: "a backup copies the three databases, and sync builds them")
|
|
17
|
+
raise Failure, "no #{e.message} at #{scope.plastic_home}; run plastic sync"
|
|
18
|
+
rescue Sqlite::Error => e
|
|
19
|
+
raise Failure, e.message
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def switches(parser)
|
|
25
|
+
parser.on("--list") { options[:list] = true }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def write
|
|
29
|
+
version = Version.new([], out: nil, err: nil, env: @env).found.last
|
|
30
|
+
@output.row("archive", Plastic::Backup.call(scope.plastic_home, version: version))
|
|
31
|
+
@output.next_step("plastic backup --list", because: "to restore, unpack the archive into an empty ~/.plastic and run plastic checkout")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def list
|
|
35
|
+
archives = Plastic::Backup.list(scope.plastic_home)
|
|
36
|
+
archives.each { |path| @output.row(File.basename(path), "#{File.size(path)} bytes #{File.mtime(path).strftime("%Y-%m-%d %H:%M")}") }
|
|
37
|
+
@output.row("result", "no backup yet") if archives.empty?
|
|
38
|
+
@output.next_step("plastic backup", because: "copying backups/ off this machine is your choice; Plastic sends nothing")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../command"
|
|
4
|
+
require_relative "../../store_sync"
|
|
5
|
+
|
|
6
|
+
module Plastic
|
|
7
|
+
class CLI
|
|
8
|
+
module Commands
|
|
9
|
+
class Checkout < Command
|
|
10
|
+
USAGE_LINE = "plastic checkout [--json]"
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
home = scope.plastic_home
|
|
14
|
+
raise Failure, "no databases at #{home}; run plastic sync" unless File.exist?(ReferenceArchive.path(home))
|
|
15
|
+
|
|
16
|
+
restored = StoreSync.checkout(home)
|
|
17
|
+
@output.row("restored", restored.empty? ? "nothing was missing" : restored)
|
|
18
|
+
@output.next_step("plastic sync --dry-run", because: "checkout restores missing files only; a changed file is never overwritten")
|
|
19
|
+
rescue Sqlite::Error => e
|
|
20
|
+
raise Failure, e.message
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../command"
|
|
4
|
+
require_relative "../frontier"
|
|
5
|
+
require_relative "../scope"
|
|
6
|
+
|
|
7
|
+
# `plastic continue` - where one project stands and what runs next: the store it
|
|
8
|
+
# uses, the intents it has open, its liveliest roadmap and that roadmap's
|
|
9
|
+
# frontier, then the step to take and the rule behind it.
|
|
10
|
+
#
|
|
11
|
+
# With no roadmap the first active intent answers instead, so a project that
|
|
12
|
+
# never grew a roadmap still gets a next step.
|
|
13
|
+
module Plastic
|
|
14
|
+
class CLI
|
|
15
|
+
module Commands
|
|
16
|
+
class Continue < Command
|
|
17
|
+
USAGE_LINE = "plastic continue [--project SLUG] [--json]"
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
@output.row("project", scope.slug)
|
|
21
|
+
@output.row("root", scope.root)
|
|
22
|
+
@output.row("active", active)
|
|
23
|
+
@output.row("roadmap", roadmap)
|
|
24
|
+
command, because = decision
|
|
25
|
+
@output.next_step(command, because: because)
|
|
26
|
+
rescue RoadmapSavepoint::MissingGroupingHeading => e
|
|
27
|
+
raise Failure, e.message
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def switches(parser)
|
|
33
|
+
parser.on("--project SLUG") { |slug| @options[:project] = slug }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def active
|
|
37
|
+
scope.active_entries.map { |id, title| "#{id} #{title}" }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def roadmap
|
|
41
|
+
return Frontier::NONE unless frontier.roadmap
|
|
42
|
+
|
|
43
|
+
"#{frontier.roadmap} frontier #{frontier.heading}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def decision
|
|
47
|
+
return [frontier.next_step, frontier.because] if frontier.id
|
|
48
|
+
|
|
49
|
+
id = scope.active_ids.first
|
|
50
|
+
return [plan_for(id), "#{id} is the first active intent in #{scope.slug}"] if id
|
|
51
|
+
|
|
52
|
+
["plastic help", "#{scope.slug} has no active work and no roadmap"]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def plan_for(id)
|
|
56
|
+
dir = scope.intent_dir(id)
|
|
57
|
+
dir ? "read #{File.join(dir, "plan.md")}" : "plastic status"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def frontier
|
|
61
|
+
@frontier ||= Frontier.new(scope)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "installer_verb"
|
|
4
|
+
|
|
5
|
+
# `plastic doctor` - diagnoses Plastic installation health. The work is still
|
|
6
|
+
# scripts/doctor.rb, run through Legacy; each finding prints its own repair.
|
|
7
|
+
module Plastic
|
|
8
|
+
class CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class Doctor < InstallerVerb
|
|
11
|
+
USAGE_LINE = "plastic doctor [--core] [--store WHICH]"
|
|
12
|
+
FLAGS = %w[--core --store].freeze
|
|
13
|
+
|
|
14
|
+
SCRIPT = "doctor.rb"
|
|
15
|
+
AFTER = "none"
|
|
16
|
+
BECAUSE = "the findings are the whole answer, and each one names its own repair"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|