@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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../command"
|
|
4
|
+
require_relative "../legacy"
|
|
5
|
+
|
|
6
|
+
# `plastic session handoff` - writes this session's hand-off into the day
|
|
7
|
+
# ledger (`write-handoff --trigger tick`, run through Legacy).
|
|
8
|
+
module Plastic
|
|
9
|
+
class CLI
|
|
10
|
+
module Commands
|
|
11
|
+
class SessionHandoff < Command
|
|
12
|
+
USAGE_LINE = "plastic session handoff [--json]"
|
|
13
|
+
|
|
14
|
+
def initialize(argv, runner: nil, **streams)
|
|
15
|
+
super(argv, **streams)
|
|
16
|
+
@runner = runner
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
status = legacy.run("write-handoff", "--trigger", "tick")
|
|
21
|
+
raise Failure, "write-handoff exited #{status}" unless status.zero?
|
|
22
|
+
|
|
23
|
+
@output.next_step("plastic session summary", because: "the summary is what a fresh handoff feeds")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def legacy
|
|
29
|
+
@legacy ||= Legacy.new(env: @env, runner: @runner)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../command"
|
|
4
|
+
require_relative "../legacy"
|
|
5
|
+
|
|
6
|
+
# `plastic session summary` - the block SessionStart injects at boot: open
|
|
7
|
+
# items, the last five done, live auto intents, other active sessions
|
|
8
|
+
# (`day-summary`, run through Legacy).
|
|
9
|
+
module Plastic
|
|
10
|
+
class CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class SessionSummary < Command
|
|
13
|
+
USAGE_LINE = "plastic session summary [--json]"
|
|
14
|
+
|
|
15
|
+
def initialize(argv, runner: nil, **streams)
|
|
16
|
+
super(argv, **streams)
|
|
17
|
+
@runner = runner
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
status = legacy.run("day-summary")
|
|
22
|
+
raise Failure, "day-summary exited #{status}" unless status.zero?
|
|
23
|
+
|
|
24
|
+
@output.next_step("plastic status", because: "the board is where the summary's open items are worked from")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def legacy
|
|
30
|
+
@legacy ||= Legacy.new(env: @env, runner: @runner)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../command"
|
|
4
|
+
|
|
5
|
+
# `plastic status` - every store this machine holds, the intents each one has
|
|
6
|
+
# open, and the one store worth continuing.
|
|
7
|
+
#
|
|
8
|
+
# The store to continue is chosen by a fixed rule, so two runs never disagree:
|
|
9
|
+
# the store whose repository holds the working directory, else the store with
|
|
10
|
+
# the most active intents, and on a tie the first in discovery order, which puts
|
|
11
|
+
# the global store ahead of the projects and the projects in name order.
|
|
12
|
+
module Plastic
|
|
13
|
+
class CLI
|
|
14
|
+
module Commands
|
|
15
|
+
class Status < Command
|
|
16
|
+
USAGE_LINE = "plastic status [--json]"
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
counted.each { |slug, ids| @output.row(slug, summary(ids)) }
|
|
20
|
+
command, because = decision
|
|
21
|
+
@output.next_step(command, because: because)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def counted
|
|
27
|
+
@counted ||= scope.stores.to_h do |store|
|
|
28
|
+
slug = store[:slug]
|
|
29
|
+
[slug, Scope.new(env: @env, home: @home, slug: slug, directory: @directory).active_ids]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def summary(ids)
|
|
34
|
+
return "0 active" if ids.empty?
|
|
35
|
+
|
|
36
|
+
"#{ids.length} active #{ids.join(", ")}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def decision
|
|
40
|
+
here = scope.directory_project
|
|
41
|
+
return [continue_command(here), "the working directory is inside #{here}"] if busy?(here)
|
|
42
|
+
|
|
43
|
+
chosen = busiest
|
|
44
|
+
return ["plastic help", "no store has active work"] unless chosen
|
|
45
|
+
|
|
46
|
+
[continue_command(chosen), "#{chosen} holds the most active work, #{intents(chosen)}"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def busy?(slug)
|
|
50
|
+
slug && !counted.fetch(slug, []).empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def busiest
|
|
54
|
+
counted.reject { |_slug, ids| ids.empty? }.max_by { |_slug, ids| ids.length }&.first
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def intents(slug)
|
|
58
|
+
count = counted.fetch(slug).length
|
|
59
|
+
(count == 1) ? "1 intent" : "#{count} intents"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def continue_command(slug)
|
|
63
|
+
(slug == Scope::GLOBAL) ? "plastic continue" : "plastic continue --project #{slug}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../command"
|
|
4
|
+
require_relative "../table"
|
|
5
|
+
|
|
6
|
+
# Plastic::CLI::Commands::SubcommandList (intent 372, family 3) - the shared
|
|
7
|
+
# base every bare group command (`plastic intent`, `plastic project`,
|
|
8
|
+
# `plastic roadmap`) runs on: list the table rows under its prefix, or a
|
|
9
|
+
# usage error naming that same list for a word that matches none of them.
|
|
10
|
+
module Plastic
|
|
11
|
+
class CLI
|
|
12
|
+
module Commands
|
|
13
|
+
class SubcommandList < Command
|
|
14
|
+
def call
|
|
15
|
+
unless arguments.empty?
|
|
16
|
+
raise Usage, "no subcommand named #{arguments.first.inspect}. Try: #{subcommand_names.join(", ")}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@output.row("usage", self.class::USAGE_LINE)
|
|
20
|
+
subcommands.each { |name, (_file, _const, summary)| @output.row(name, summary) }
|
|
21
|
+
@output.next_step(self.class::NEXT, because: self.class::BECAUSE)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def subcommands
|
|
27
|
+
TABLE.select { |name, _| name.start_with?("#{self.class::PREFIX} ") }.sort.to_h
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def subcommand_names
|
|
31
|
+
subcommands.keys
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../command"
|
|
4
|
+
require_relative "../../store_sync"
|
|
5
|
+
require_relative "../../work_graph"
|
|
6
|
+
|
|
7
|
+
module Plastic
|
|
8
|
+
class CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class Sync < Command
|
|
11
|
+
USAGE_LINE = "plastic sync [--dry-run] [--json]"
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
home = scope.plastic_home
|
|
15
|
+
plan = File.exist?(SearchIndex.path(home)) ? StoreSync.plan(home) : [["build", SearchIndex::NAME]]
|
|
16
|
+
refuse(home, plan.filter_map { |action, path| path if action == "conflict" })
|
|
17
|
+
plan.group_by(&:first).each { |action, pairs| @output.row(action, pairs.map(&:last)) }
|
|
18
|
+
@output.row("result", "nothing to do") if plan.empty?
|
|
19
|
+
apply(home, plan) unless options[:dry_run]
|
|
20
|
+
@output.next_step("plastic search TERMS", because: "a changed file is read into its row, a changed row is written out, and both changed is yours to settle")
|
|
21
|
+
rescue Sqlite::Error => e
|
|
22
|
+
raise Failure, e.message
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def switches(parser)
|
|
28
|
+
parser.on("--dry-run") { options[:dry_run] = true }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def refuse(home, conflicts)
|
|
32
|
+
return if conflicts.empty?
|
|
33
|
+
|
|
34
|
+
diffs = conflicts.map { |path| "#{path}\n#{StoreSync.diff(home, path)}" }
|
|
35
|
+
raise Refusal, "the file and the row both changed; nothing was written\n#{diffs.join("\n")}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def apply(home, plan)
|
|
39
|
+
(plan == [["build", SearchIndex::NAME]]) ? SearchIndex.build(home) : StoreSync.apply(home, plan)
|
|
40
|
+
WorkGraph.build(home)
|
|
41
|
+
ReferenceArchive.build(home)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "installer_verb"
|
|
4
|
+
|
|
5
|
+
# `plastic uninstall` - removes Plastic from this machine's agents and leaves
|
|
6
|
+
# the store alone. The work is still scripts/uninstall.rb, run through Legacy.
|
|
7
|
+
module Plastic
|
|
8
|
+
class CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class Uninstall < InstallerVerb
|
|
11
|
+
USAGE_LINE = "plastic uninstall [--claude] [--codex] [--hermes] [--all]"
|
|
12
|
+
FLAGS = %w[--claude --codex --hermes --all].freeze
|
|
13
|
+
|
|
14
|
+
SCRIPT = "uninstall.rb"
|
|
15
|
+
AFTER = "none"
|
|
16
|
+
BECAUSE = "Plastic is removed from this machine's agents"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "installer_verb"
|
|
4
|
+
|
|
5
|
+
# `plastic update` - moves this machine to the next version on its channel. The
|
|
6
|
+
# work is still scripts/update.rb, run through Legacy.
|
|
7
|
+
module Plastic
|
|
8
|
+
class CLI
|
|
9
|
+
module Commands
|
|
10
|
+
class Update < InstallerVerb
|
|
11
|
+
USAGE_LINE = "plastic update [--claude] [--codex] [--hermes] [--all] [--latest] [--beta] [--alpha] [--yes] [--reinstall] [--full-doctor]"
|
|
12
|
+
FLAGS = %w[--claude --codex --hermes --all --latest --beta --alpha --yes --reinstall --full-doctor].freeze
|
|
13
|
+
|
|
14
|
+
SCRIPT = "update.rb"
|
|
15
|
+
AFTER = "plastic version"
|
|
16
|
+
BECAUSE = "the update is done, and restarting the session picks up the new conventions"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../command"
|
|
5
|
+
|
|
6
|
+
# `plastic version` - the version of the package this command came from, and the
|
|
7
|
+
# file it was read from. The VERSION file wins, because an installed copy
|
|
8
|
+
# carries one; the package manifest answers in a checkout, which has none.
|
|
9
|
+
module Plastic
|
|
10
|
+
class CLI
|
|
11
|
+
module Commands
|
|
12
|
+
class Version < Command
|
|
13
|
+
USAGE_LINE = "plastic version [--json]"
|
|
14
|
+
|
|
15
|
+
SOURCES = ["VERSION", "package.json"].freeze
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
path, number = found
|
|
19
|
+
raise Failure, "no VERSION file and no package.json under #{package_root}" unless number
|
|
20
|
+
|
|
21
|
+
@output.row("version", number)
|
|
22
|
+
@output.row("source", path)
|
|
23
|
+
@output.next_step("plastic status", because: "the command line works, so read the work next")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def found
|
|
27
|
+
SOURCES.each do |name|
|
|
28
|
+
path = File.join(package_root, name)
|
|
29
|
+
number = read(path)
|
|
30
|
+
return [path, number] if number
|
|
31
|
+
end
|
|
32
|
+
[nil, nil]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def read(path)
|
|
38
|
+
return nil unless File.file?(path)
|
|
39
|
+
return File.read(path).strip unless path.end_with?(".json")
|
|
40
|
+
|
|
41
|
+
number = JSON.parse(File.read(path))["version"]
|
|
42
|
+
number&.to_s&.strip
|
|
43
|
+
rescue JSON::ParserError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def package_root
|
|
48
|
+
@package_root ||= @env["PLASTIC_PACKAGE_ROOT"] || File.expand_path("../../../..", __dir__)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../roadmap_queue"
|
|
4
|
+
|
|
5
|
+
# Plastic::CLI::Frontier (intent 363) - the one reader of "what comes next" in a
|
|
6
|
+
# scope. `plastic next` prints it as a line and `plastic continue` prints it
|
|
7
|
+
# inside a project block, so the rule that picks the work is stated once rather
|
|
8
|
+
# than twice.
|
|
9
|
+
#
|
|
10
|
+
# It reads RoadmapQueue's queue mode: the liveliest roadmap, its frontier batch,
|
|
11
|
+
# the entries that can be dispatched, the entries in flight and the blocked
|
|
12
|
+
# ones. With no roadmap at all it answers the "none" state rather than raising,
|
|
13
|
+
# and a roadmap with no grouping heading raises through to the command, which
|
|
14
|
+
# fails loudly instead of parsing it as zero entries.
|
|
15
|
+
module Plastic
|
|
16
|
+
class CLI
|
|
17
|
+
class Frontier
|
|
18
|
+
NONE = "none"
|
|
19
|
+
|
|
20
|
+
def initialize(scope)
|
|
21
|
+
@scope = scope
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def state
|
|
25
|
+
payload["state"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def roadmap
|
|
29
|
+
payload["roadmap"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def heading
|
|
33
|
+
payload["frontier_wave"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def dispatchable_ids
|
|
37
|
+
ids(payload["dispatchable_queue"])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def in_flight_ids
|
|
41
|
+
ids(payload["in_flight"])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def blocked_ids
|
|
45
|
+
ids(payload["blocked"])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def id
|
|
49
|
+
(dispatchable_ids + in_flight_ids).first
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def summary
|
|
53
|
+
return NONE unless id
|
|
54
|
+
|
|
55
|
+
"#{id} in #{heading}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def because
|
|
59
|
+
return "#{@scope.slug} has no roadmap" if state == NONE
|
|
60
|
+
return "every entry on #{roadmap} is delivered" if id.nil?
|
|
61
|
+
|
|
62
|
+
"#{id} is first on the frontier of #{roadmap}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def next_step
|
|
66
|
+
return "plastic status" unless id
|
|
67
|
+
|
|
68
|
+
plan = @scope.intent_dir(id)
|
|
69
|
+
plan ? "read #{File.join(plan, "plan.md")}" : "plastic status"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def payload
|
|
75
|
+
@payload ||= RoadmapQueue.new(roadmaps_dir: @scope.roadmaps_dir,
|
|
76
|
+
index_path: @scope.index_path).queue
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def ids(entries)
|
|
80
|
+
Array(entries).map { |entry| entry["id"] }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require_relative "command"
|
|
6
|
+
|
|
7
|
+
# Plastic::CLI::Legacy (intent 363) - runs a script that has not moved into a
|
|
8
|
+
# module yet as a child process, and passes its output straight through. It is
|
|
9
|
+
# scaffolding: when the last family is migrated it is deleted, and the commands
|
|
10
|
+
# call their modules in the same process instead.
|
|
11
|
+
#
|
|
12
|
+
# A child that exits 3 refused, and the refusal travels as a Refusal so the
|
|
13
|
+
# agent is told to stop and ask rather than to retry with a flag. Every other
|
|
14
|
+
# status is handed back to the caller unchanged.
|
|
15
|
+
module Plastic
|
|
16
|
+
class CLI
|
|
17
|
+
class Legacy
|
|
18
|
+
DEFAULT_RUNNER = lambda do |path, arguments|
|
|
19
|
+
system(RbConfig.ruby, path, *arguments)
|
|
20
|
+
exit_code($CHILD_STATUS)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.exit_code(status)
|
|
24
|
+
return Command::OK if status.success?
|
|
25
|
+
|
|
26
|
+
status.exitstatus || Command::FAILED
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(env:, runner: nil)
|
|
30
|
+
@env = env
|
|
31
|
+
@runner = runner || DEFAULT_RUNNER
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def package_root
|
|
35
|
+
@package_root ||= @env["PLASTIC_PACKAGE_ROOT"] || File.expand_path("../../..", __dir__)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def script_path(script)
|
|
39
|
+
File.join(package_root, "scripts", script)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def run(script, *arguments)
|
|
43
|
+
status = @runner.call(script_path(script), arguments)
|
|
44
|
+
raise Command::Refusal, "#{script} needs the owner" if status == Command::REFUSED
|
|
45
|
+
|
|
46
|
+
status
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
# Plastic::CLI::Output (intent 363) - one place where a command's answer is
|
|
6
|
+
# printed. A result is rows of label and value; every command ends with a
|
|
7
|
+
# `next:` line and the `because:` line that gives the rule behind it, which is
|
|
8
|
+
# where the step order the skills used to carry now lives. `--json` prints the
|
|
9
|
+
# same three things with stable keys.
|
|
10
|
+
#
|
|
11
|
+
# Results go to the output stream and diagnostics to the error stream, so a
|
|
12
|
+
# caller can pipe one without the other. Nothing here colours its output.
|
|
13
|
+
module Plastic
|
|
14
|
+
class CLI
|
|
15
|
+
class Output
|
|
16
|
+
LABEL_GAP = 2
|
|
17
|
+
|
|
18
|
+
def initialize(out:, err:)
|
|
19
|
+
@out = out
|
|
20
|
+
@err = err
|
|
21
|
+
@rows = []
|
|
22
|
+
@next_step = nil
|
|
23
|
+
@because = nil
|
|
24
|
+
@flushed = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def row(label, value)
|
|
28
|
+
@rows << [label, value]
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def next_step(command, because:)
|
|
33
|
+
@next_step = command
|
|
34
|
+
@because = because
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def flush(json: false)
|
|
39
|
+
return self if @flushed
|
|
40
|
+
|
|
41
|
+
@flushed = true
|
|
42
|
+
json ? print_json : print_text
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def raw(text)
|
|
47
|
+
@out.puts text
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def usage(message, banner)
|
|
52
|
+
@err.puts "plastic: #{message}"
|
|
53
|
+
@err.puts banner
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def refused(message)
|
|
58
|
+
@err.puts "plastic: refused, #{message}"
|
|
59
|
+
@err.puts "This step belongs to the owner. Stop and ask; do not retry with a flag."
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def failed(message)
|
|
64
|
+
@err.puts "plastic: #{message}"
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def print_json
|
|
71
|
+
@out.puts JSON.pretty_generate("result" => @rows.to_h, "next" => @next_step,
|
|
72
|
+
"because" => @because)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def print_text
|
|
76
|
+
printed = print_rows
|
|
77
|
+
return unless @next_step
|
|
78
|
+
|
|
79
|
+
@out.puts if printed
|
|
80
|
+
@out.puts "next: #{@next_step}"
|
|
81
|
+
@out.puts "because: #{@because}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def print_rows
|
|
85
|
+
width = label_width
|
|
86
|
+
printed = false
|
|
87
|
+
@rows.each do |label, value|
|
|
88
|
+
Array(value).each_with_index do |item, index|
|
|
89
|
+
@out.puts "#{(index.zero? ? label : "").ljust(width)}#{item}"
|
|
90
|
+
printed = true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
printed
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def label_width
|
|
97
|
+
widths = @rows.reject { |_label, value| Array(value).empty? }.map { |label, _value| label.length }
|
|
98
|
+
widths.empty? ? 0 : widths.max + LABEL_GAP
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
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 = 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
|
|
105
|
+
.select { |_slug, path| inside?(path) }
|
|
106
|
+
.max_by { |_slug, path| path.length }
|
|
107
|
+
&.first
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def inside?(path)
|
|
111
|
+
@directory == path || @directory.start_with?("#{path}#{File::SEPARATOR}")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def repositories
|
|
115
|
+
StoreProvisioning.load_projects(plastic_home).filter_map do |slug, info|
|
|
116
|
+
path = info.is_a?(Hash) ? info["path"].to_s : ""
|
|
117
|
+
[slug, path] unless path.empty?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def section(text)
|
|
122
|
+
body = text.match(/^##\s+Active\s*$(.*?)(?=^##\s|\z)/m)
|
|
123
|
+
body ? body[1].lines.map(&:strip) : []
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|