@zalom/plastic 2.0.0-alpha.9 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLASTIC.md +13 -136
- package/README.md +357 -133
- package/agents/plastic-enforcer.md +21 -16
- package/agents/plastic-executor.md +15 -4
- package/agents/plastic-node-research.md +30 -0
- package/agents/plastic-node-verify.md +28 -0
- package/agents/plastic-node-work.md +33 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/assets/plastic-logo.svg +1 -0
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +43 -6
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +8 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/test +24 -4
- package/bin/verify-change +345 -0
- package/config_asks.yml +4 -4
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
- package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
- package/docs/help/completion-and-done.md +53 -0
- package/docs/help/human-report-contract.md +152 -0
- package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
- package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +11 -13
- package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
- package/{skills/conventions/references → docs/help}/roadmaps.md +5 -4
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/hooks/call-budget +4 -0
- package/hooks/hooks.json +24 -0
- package/hooks/message-display +55 -2
- package/hooks/session-start +5 -1
- package/hooks/statusline +32 -27
- package/hooks/stop +5 -0
- package/package.json +5 -3
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +267 -16
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +680 -39
- package/scripts/end-intent +378 -56
- package/scripts/exec-worktree +5 -5
- package/scripts/file-session-intent +2 -1
- package/scripts/graph-measure +249 -0
- package/scripts/hook-call-budget +222 -0
- package/scripts/hook-capture +24 -123
- package/scripts/hook-close +2 -1
- package/scripts/hook-message-display +7 -0
- package/scripts/hook-record +24 -16
- package/scripts/hook-savepoint +27 -3
- package/scripts/hook-session-start +359 -321
- package/scripts/hook-stop +58 -0
- package/scripts/index-projection +74 -0
- package/scripts/insight-append +17 -4
- package/scripts/install.rb +9 -7
- package/scripts/lib/action_graph_shim.rb +279 -0
- package/scripts/lib/active_delivery.rb +105 -0
- package/scripts/lib/agent_models.rb +63 -25
- package/scripts/lib/arm.rb +74 -91
- package/scripts/lib/atomic_write.rb +31 -0
- package/scripts/lib/backup.rb +65 -0
- package/scripts/lib/cli/command.rb +92 -0
- package/scripts/lib/cli/commands/auto.rb +18 -0
- package/scripts/lib/cli/commands/auto_brief.rb +44 -0
- package/scripts/lib/cli/commands/auto_lock.rb +111 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +68 -0
- package/scripts/lib/cli/commands/backup.rb +43 -0
- package/scripts/lib/cli/commands/checkout.rb +25 -0
- package/scripts/lib/cli/commands/continue.rb +66 -0
- package/scripts/lib/cli/commands/doctor.rb +81 -0
- package/scripts/lib/cli/commands/feedback.rb +40 -0
- package/scripts/lib/cli/commands/help.rb +69 -0
- package/scripts/lib/cli/commands/hook.rb +32 -0
- package/scripts/lib/cli/commands/index.rb +23 -0
- package/scripts/lib/cli/commands/install.rb +21 -0
- package/scripts/lib/cli/commands/installer_verb.rb +37 -0
- package/scripts/lib/cli/commands/intent.rb +19 -0
- package/scripts/lib/cli/commands/intent_answer.rb +37 -0
- package/scripts/lib/cli/commands/intent_command.rb +58 -0
- package/scripts/lib/cli/commands/intent_end.rb +75 -0
- package/scripts/lib/cli/commands/intent_new.rb +62 -0
- package/scripts/lib/cli/commands/intent_note.rb +43 -0
- package/scripts/lib/cli/commands/intent_rule.rb +36 -0
- package/scripts/lib/cli/commands/intent_show.rb +32 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +79 -0
- package/scripts/lib/cli/commands/intent_verify.rb +26 -0
- package/scripts/lib/cli/commands/migrate.rb +16 -0
- package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
- package/scripts/lib/cli/commands/next.rb +54 -0
- package/scripts/lib/cli/commands/project.rb +19 -0
- package/scripts/lib/cli/commands/project_links.rb +46 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +95 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +35 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +77 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +65 -0
- package/scripts/lib/cli/commands/rollback.rb +20 -0
- package/scripts/lib/cli/commands/search.rb +60 -0
- package/scripts/lib/cli/commands/session.rb +18 -0
- package/scripts/lib/cli/commands/session_commit.rb +42 -0
- package/scripts/lib/cli/commands/session_handoff.rb +34 -0
- package/scripts/lib/cli/commands/session_summary.rb +35 -0
- package/scripts/lib/cli/commands/status.rb +68 -0
- package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
- package/scripts/lib/cli/commands/sync.rb +55 -0
- package/scripts/lib/cli/commands/uninstall.rb +20 -0
- package/scripts/lib/cli/commands/update.rb +20 -0
- package/scripts/lib/cli/commands/version.rb +53 -0
- package/scripts/lib/cli/frontier.rb +86 -0
- package/scripts/lib/cli/intent_progress.rb +36 -0
- package/scripts/lib/cli/legacy.rb +80 -0
- package/scripts/lib/cli/output.rb +137 -0
- package/scripts/lib/cli/scope.rb +134 -0
- package/scripts/lib/cli/table.rb +65 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +198 -0
- package/scripts/lib/compact_instructions.rb +13 -5
- package/scripts/lib/core_integrity.rb +71 -0
- package/scripts/lib/dashboard_screen.rb +40 -0
- package/scripts/lib/data_boundary.rb +132 -0
- package/scripts/lib/day_summary.rb +23 -14
- package/scripts/lib/doctor_core.rb +114 -38
- package/scripts/lib/doctor_session_ledger.rb +5 -53
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_edges.rb +137 -0
- package/scripts/lib/graph_file.rb +246 -0
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +409 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +413 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +98 -0
- package/scripts/lib/guarded_append.rb +155 -0
- package/scripts/lib/handoff.rb +40 -13
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +27 -3
- package/scripts/lib/hook_replay.rb +235 -0
- package/scripts/lib/index_entry.rb +62 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +543 -71
- package/scripts/lib/intent_screen.rb +4 -4
- package/scripts/lib/intent_screen_ansi.rb +73 -12
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/lock.rb +10 -11
- package/scripts/lib/message_display.rb +350 -54
- package/scripts/lib/meter_watch.rb +185 -0
- package/scripts/lib/node_file.rb +234 -0
- package/scripts/lib/node_ids.rb +99 -0
- package/scripts/lib/node_input.rb +921 -0
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +386 -0
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/node_return.rb +204 -0
- package/scripts/lib/node_worktree.rb +337 -0
- package/scripts/lib/outcome_report.rb +440 -0
- package/scripts/lib/preflight.rb +4 -6
- package/scripts/lib/project_config.rb +46 -0
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/ready_set.rb +462 -0
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +18 -0
- package/scripts/lib/report_screen.rb +1371 -47
- package/scripts/lib/rlm/corpus.rb +13 -0
- package/scripts/lib/rlm/probe.rb +29 -0
- package/scripts/lib/rlm/query.rb +22 -0
- package/scripts/lib/roadmap_graph.rb +210 -0
- package/scripts/lib/roadmap_migration.rb +95 -0
- package/scripts/lib/roadmap_queue.rb +170 -12
- package/scripts/lib/roadmap_render.rb +150 -0
- package/scripts/lib/roadmap_savepoint.rb +64 -14
- package/scripts/lib/runner_absorb.rb +703 -0
- package/scripts/lib/runner_answer.rb +206 -0
- package/scripts/lib/runner_core.rb +194 -0
- package/scripts/lib/runner_dispatch.rb +526 -0
- package/scripts/lib/runner_policy.rb +191 -0
- package/scripts/lib/runner_proposals.rb +275 -0
- package/scripts/lib/runner_rewind.rb +201 -0
- package/scripts/lib/runner_sweep.rb +231 -0
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +141 -19
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/screen_paint.rb +365 -28
- package/scripts/lib/screens/dashboard.rb +20 -0
- package/scripts/lib/screens/plan.rb +18 -0
- package/scripts/lib/screens/roadmap.rb +15 -0
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +67 -19
- package/scripts/lib/session_ledger.rb +48 -4
- package/scripts/lib/session_usage.rb +190 -0
- package/scripts/lib/sqlite.rb +22 -0
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/store_discovery.rb +7 -6
- package/scripts/lib/store_layout.rb +54 -0
- package/scripts/lib/store_provisioning.rb +2 -1
- package/scripts/lib/store_sync.rb +85 -0
- package/scripts/lib/stores_move.rb +93 -0
- package/scripts/lib/untouched_scaffold.rb +51 -0
- package/scripts/lib/verify_intent.rb +36 -8
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/lib/worktree.rb +27 -32
- package/scripts/lib/worktree_sweep.rb +6 -5
- package/scripts/link-suggest +2 -1
- package/scripts/meter-watch +57 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +4 -13
- package/scripts/node-input +92 -0
- package/scripts/node-run +225 -0
- package/scripts/node-transition +291 -0
- package/scripts/outcome-report +74 -0
- package/scripts/plastic-lock +65 -60
- package/scripts/project-links +6 -21
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +53 -9
- package/scripts/ready-set +126 -0
- package/scripts/release-check +123 -0
- package/scripts/report-screen +177 -16
- package/scripts/roadmap-graph +125 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/rollback.rb +5 -1
- package/scripts/runner +581 -0
- package/scripts/savepoint-note +11 -9
- package/scripts/session-commit +2 -1
- package/scripts/session-usage +56 -0
- package/scripts/skill-lint +115 -6
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +31 -4
- package/scripts/validate-work-graph +39 -0
- package/scripts/verify-intent +3 -2
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +7 -7
- package/templates/config.yml +16 -9
- package/templates/dashboard-screen.md +22 -0
- package/templates/display-fixture.md +21 -0
- package/templates/graph.md +16 -0
- package/templates/index.md +1 -1
- package/templates/intent-screen.md +1 -1
- package/templates/node-decision.md +11 -0
- package/templates/node-research.md +13 -0
- package/templates/node-verify.md +13 -0
- package/templates/node-work.md +22 -0
- package/templates/outcome.md +8 -3
- package/templates/project.yml +1 -1
- package/templates/render.css +10 -0
- package/templates/report-plan.md +15 -0
- package/templates/report-roadmap-delivered.md +10 -0
- package/templates/report-roadmap-plan.md +9 -0
- package/templates/report-roadmap-state.md +9 -0
- package/templates/report-state.md +1 -1
- package/templates/roadmap.md +13 -0
- package/bin/plastic.js +0 -70
- package/scripts/lib/bridge.rb +0 -116
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -296
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -66
- package/skills/auto/references/human-report-contract.md +0 -78
- package/skills/conventions/SKILL.md +0 -29
- package/skills/conventions/references/completion-and-done.md +0 -43
- package/skills/dashboard/SKILL.md +0 -169
- package/skills/dashboard/evals/evals.json +0 -38
- package/skills/dashboard/references/classification.md +0 -22
- package/skills/dashboard/templates/dashboard-global.md +0 -20
- package/skills/dashboard/templates/dashboard-project.md +0 -19
- package/skills/direct/SKILL.md +0 -66
- package/skills/direct/references/request-signals.md +0 -59
- package/skills/doctor/SKILL.md +0 -299
- package/skills/doctor/report.md +0 -102
- package/skills/feedback/SKILL.md +0 -98
- package/skills/feedback/references/transport-and-privacy.md +0 -65
- package/skills/feedback/report.md +0 -36
- package/skills/install/SKILL.md +0 -217
- package/skills/intent-continuing/SKILL.md +0 -154
- package/skills/intent-continuing/references/board-fill.md +0 -43
- package/skills/intent-continuing/references/boarding-matrix.md +0 -34
- package/skills/intent-continuing/references/context-management.md +0 -28
- package/skills/intent-continuing/references/liveness-ranking.md +0 -57
- package/skills/intent-creating/SKILL.md +0 -164
- package/skills/intent-creating/evals/evals.json +0 -72
- package/skills/intent-creating/references/lifecycle.md +0 -81
- package/skills/intent-creating/references/wikilinks.md +0 -8
- package/skills/intent-ending/SKILL.md +0 -176
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -170
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -42
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -130
- package/skills/intent-speccing/evals/evals.json +0 -126
- package/skills/intent-speccing/references/design-principles.md +0 -44
- package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
- package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
- package/skills/project-creating/SKILL.md +0 -162
- package/skills/project-creating/references/hubs-projects.md +0 -55
- package/skills/project-creating/references/project-scaffolding.md +0 -97
- package/skills/releasing/SKILL.md +0 -337
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -66
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -64
- package/skills/roadmap/references/file-format.md +0 -124
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -65
- package/skills/tutorial/evals/evals.json +0 -186
- package/skills/uninstall/SKILL.md +0 -75
- package/skills/update/SKILL.md +0 -126
- /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
- /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
|
@@ -10,37 +10,48 @@
|
|
|
10
10
|
module AgentModels
|
|
11
11
|
# Claude Code aliases only (never pinned ids, never Fable). Keys are the agent
|
|
12
12
|
# file basenames without the `.md` extension.
|
|
13
|
+
# The three per-kind node agents (intent 340b, G7c, n2), dispatched by
|
|
14
|
+
# HarnessAdapter::AGENT_TYPE_BY_KIND. work and research resolve the
|
|
15
|
+
# executor tier, verify the advisor tier, mirroring RunnerPolicy's
|
|
16
|
+
# model_role split (327 D12): work and research run on plastic-executor's
|
|
17
|
+
# own tier, and verify on the lifecycle advisor tier, never the cheap tier.
|
|
13
18
|
TIER_DEFAULTS = {
|
|
14
19
|
"plastic-enforcer" => "opus",
|
|
15
|
-
"plastic-executor" => "sonnet"
|
|
20
|
+
"plastic-executor" => "sonnet",
|
|
21
|
+
"plastic-node-work" => "sonnet",
|
|
22
|
+
"plastic-node-verify" => "opus",
|
|
23
|
+
"plastic-node-research" => "sonnet"
|
|
16
24
|
}.freeze
|
|
17
25
|
|
|
18
|
-
# The two
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# ships `model: opus`). Both are shipped DEFAULTS in frontmatter, never a
|
|
26
|
+
# The two consultation agents are Primary Advisor and Secondary Advisor.
|
|
27
|
+
# Both ship Fable; Primary uses medium effort and Secondary uses high effort.
|
|
28
|
+
# Both are shipped defaults in frontmatter, never a
|
|
22
29
|
# hard-wired identity: agents.models.claude.<name> (or the legacy flat form)
|
|
23
30
|
# overrides either one through the same install-time frontmatter rewrite
|
|
24
31
|
# every agent override uses. Neither is a lifecycle-stage role: never
|
|
25
|
-
# dispatched by the auto pipeline, not part of TIER_DEFAULTS.
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# Codex reasoning
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# reasoning tier -> the deepest generally-safe effort (high, not the model-dependent
|
|
38
|
-
# xhigh); sonnet the mid execution tier -> medium; haiku the lightest -> low. minimal is
|
|
39
|
-
# unused.
|
|
32
|
+
# dispatched by the auto pipeline, not part of TIER_DEFAULTS. Codex installs
|
|
33
|
+
# generate both advisors with Astra and preserve each role's effort.
|
|
34
|
+
CONSULTATION_AGENTS = %w[plastic-primary-advisor plastic-secondary-advisor].freeze
|
|
35
|
+
|
|
36
|
+
SHIPPED_MODEL_DEFAULTS = TIER_DEFAULTS.merge(
|
|
37
|
+
"plastic-primary-advisor" => "fable",
|
|
38
|
+
"plastic-secondary-advisor" => "fable"
|
|
39
|
+
).freeze
|
|
40
|
+
|
|
41
|
+
# Codex reasoning effort per tier alias. Model choice and reasoning effort are independent:
|
|
42
|
+
# aliases select the recommended OpenAI model, while every Plastic role starts at medium.
|
|
43
|
+
# A valid harness-scoped user override can still choose another effort for one agent.
|
|
40
44
|
EFFORT_BY_ALIAS = {
|
|
41
|
-
"opus" => "
|
|
45
|
+
"opus" => "medium",
|
|
42
46
|
"sonnet" => "medium",
|
|
43
|
-
"haiku" => "
|
|
47
|
+
"haiku" => "medium"
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
DEFAULT_EFFORT = "medium"
|
|
51
|
+
|
|
52
|
+
SHIPPED_EFFORT_DEFAULTS = {
|
|
53
|
+
"plastic-primary-advisor" => "medium",
|
|
54
|
+
"plastic-secondary-advisor" => "high"
|
|
44
55
|
}.freeze
|
|
45
56
|
|
|
46
57
|
# Codex model id per tier alias (intent 186). Codex has NO vendor alias layer: every model id
|
|
@@ -49,17 +60,26 @@ module AgentModels
|
|
|
49
60
|
# centralizing every id in ONE map: Plastic owns the alias, so per-role identity costs a single
|
|
50
61
|
# line to refresh on a Codex deprecation plus a Plastic release, and no per-role file carries a
|
|
51
62
|
# raw id. opus (deepest reasoning tier) -> the flagship Sol; sonnet (execution tier) -> the
|
|
52
|
-
# balanced Terra; haiku (lightest) -> the fast/cheap Luna.
|
|
53
|
-
#
|
|
54
|
-
#
|
|
63
|
+
# balanced Terra; haiku (lightest) -> the fast/cheap Luna. This is a shipped
|
|
64
|
+
# DEFAULT, fully overridable via agents.models.codex.<name>. Model strength does not change
|
|
65
|
+
# effort: all three aliases use medium unless agents.efforts.codex.<name> overrides it.
|
|
55
66
|
CODEX_MODEL_BY_ALIAS = {
|
|
56
67
|
"opus" => "gpt-5.6-sol",
|
|
57
68
|
"sonnet" => "gpt-5.6-terra",
|
|
58
69
|
"haiku" => "gpt-5.6-luna"
|
|
59
70
|
}.freeze
|
|
60
71
|
|
|
72
|
+
CODEX_MODEL_BY_AGENT = {
|
|
73
|
+
"plastic-primary-advisor" => "gpt-6-astra",
|
|
74
|
+
"plastic-secondary-advisor" => "gpt-6-astra"
|
|
75
|
+
}.freeze
|
|
76
|
+
|
|
61
77
|
module_function
|
|
62
78
|
|
|
79
|
+
def shipped_effort_for(agent)
|
|
80
|
+
SHIPPED_EFFORT_DEFAULTS.fetch(agent.to_s, DEFAULT_EFFORT)
|
|
81
|
+
end
|
|
82
|
+
|
|
63
83
|
# Pull { basename => model } out of a loaded config hash's `agents.models`
|
|
64
84
|
# section, scoped to `harness` ("claude" or "codex"), tolerating a missing or
|
|
65
85
|
# malformed shape. `agents.models` can mix two shapes: legacy FLAT scalar
|
|
@@ -93,6 +113,17 @@ module AgentModels
|
|
|
93
113
|
models_section(global_config, harness: harness).merge(models_section(project_config, harness: harness))
|
|
94
114
|
end
|
|
95
115
|
|
|
116
|
+
def effort_override_map(project_config: {}, global_config: {}, harness: "claude")
|
|
117
|
+
efforts_section(global_config, harness).merge(efforts_section(project_config, harness))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def efforts_section(config, harness)
|
|
121
|
+
agents = config.is_a?(Hash) ? config["agents"] : nil
|
|
122
|
+
efforts = agents.is_a?(Hash) ? agents["efforts"] : nil
|
|
123
|
+
section = efforts.is_a?(Hash) ? efforts[harness] : nil
|
|
124
|
+
section.is_a?(Hash) ? section : {}
|
|
125
|
+
end
|
|
126
|
+
|
|
96
127
|
# The model_reasoning_effort for a Plastic tier alias, or nil for any value that is not
|
|
97
128
|
# one of the three shipped aliases (the caller treats nil as a literal Codex model id).
|
|
98
129
|
def effort_for(value)
|
|
@@ -104,4 +135,11 @@ module AgentModels
|
|
|
104
135
|
def codex_model_for(value)
|
|
105
136
|
CODEX_MODEL_BY_ALIAS[value.to_s]
|
|
106
137
|
end
|
|
138
|
+
|
|
139
|
+
def shipped_model_for(agent, harness: "claude")
|
|
140
|
+
value = SHIPPED_MODEL_DEFAULTS[agent.to_s]
|
|
141
|
+
return value unless harness.to_s == "codex"
|
|
142
|
+
|
|
143
|
+
CODEX_MODEL_BY_AGENT[agent.to_s] || codex_model_for(value) || value
|
|
144
|
+
end
|
|
107
145
|
end
|
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"
|
|
@@ -10,13 +11,14 @@ require_relative "savepoint"
|
|
|
10
11
|
|
|
11
12
|
# Arm - how an auto team takes an intent and gives it back (intent 307).
|
|
12
13
|
#
|
|
13
|
-
#
|
|
14
|
-
# directory
|
|
15
|
-
# the code lands
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
14
|
+
# Two durable facts make a delivery: the delivery lock in the intent
|
|
15
|
+
# directory says who delivers, and the code worktree under the project repo
|
|
16
|
+
# says where the code lands. A conversation session's tmp directory under
|
|
17
|
+
# the global store's `.tmp/` says only that a session started; it names no
|
|
18
|
+
# intent. This module carries the
|
|
19
|
+
# arm, disarm, and repair operations (intent 307); the pure INDEX/project-config
|
|
20
|
+
# helpers that once sat alongside them on a shared-helpers module now live on
|
|
21
|
+
# IndexEntry and ProjectConfig (intent 344).
|
|
20
22
|
#
|
|
21
23
|
# Pure and dependency-injected: every path and clock is an argument, every git
|
|
22
24
|
# call goes through an injected runner, and the only environment read is the
|
|
@@ -32,7 +34,7 @@ module Arm
|
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
# Deterministic, session-less key derived from the store and the intent id,
|
|
35
|
-
# for headless runs that carry no session id at all
|
|
37
|
+
# for headless runs that carry no session id at all.
|
|
36
38
|
def derive_key(store, intent_id)
|
|
37
39
|
"auto-" + Digest::SHA256.hexdigest("#{store}/#{intent_id}")[0, 10]
|
|
38
40
|
end
|
|
@@ -62,12 +64,12 @@ module Arm
|
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
def global_store(home)
|
|
65
|
-
File.join(File.expand_path(home), ".plastic"
|
|
67
|
+
Plastic::StoreLayout.global_store(File.join(File.expand_path(home), ".plastic"))
|
|
66
68
|
end
|
|
67
69
|
|
|
68
70
|
# The minimal hash Worktree.provision, release, and finish consume: the
|
|
69
71
|
# intent block plus, when asked, the derived worktree block.
|
|
70
|
-
def
|
|
72
|
+
def delivery(intent_dir:, home: Dir.home, with_worktree: true)
|
|
71
73
|
dir = File.expand_path(intent_dir)
|
|
72
74
|
data = {
|
|
73
75
|
"intent" => { "id" => intent_id_for(dir), "dir" => File.basename(dir), "store" => store_for(dir) },
|
|
@@ -79,12 +81,7 @@ module Arm
|
|
|
79
81
|
# `{code, code_branch, provisioned}` derived from projects.yml and the
|
|
80
82
|
# intent id: the same path provision creates, `provisioned` iff it exists.
|
|
81
83
|
def worktree_block(intent_dir:, home: Dir.home)
|
|
82
|
-
|
|
83
|
-
store = store_for(dir)
|
|
84
|
-
h = home_for(dir, home: home)
|
|
85
|
-
slug = Worktree.slug_for_store(store, home: h)
|
|
86
|
-
p = Worktree.paths(slug: slug, intent_id: intent_id_for(dir),
|
|
87
|
-
intent_slug: Worktree.slug_from_dir(dir), home: h)
|
|
84
|
+
p = code_paths(intent_dir: intent_dir, home: home)
|
|
88
85
|
code = p["code"]
|
|
89
86
|
provisioned = !blank?(code) && Dir.exist?(code)
|
|
90
87
|
{
|
|
@@ -94,78 +91,78 @@ module Arm
|
|
|
94
91
|
}
|
|
95
92
|
end
|
|
96
93
|
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
def
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
path = pointer_path(session, home: home)
|
|
106
|
-
File.exist?(path) ? File.read(path).strip : nil
|
|
94
|
+
# The code worktree path and branch this intent would have, whether or not
|
|
95
|
+
# the worktree exists on disk (blank code for a store-only project).
|
|
96
|
+
def code_paths(intent_dir:, home: Dir.home)
|
|
97
|
+
dir = File.expand_path(intent_dir)
|
|
98
|
+
h = home_for(dir, home: home)
|
|
99
|
+
slug = Worktree.slug_for_store(store_for(dir), home: h)
|
|
100
|
+
Worktree.paths(slug: slug, intent_id: intent_id_for(dir),
|
|
101
|
+
intent_slug: Worktree.slug_from_dir(dir), home: h)
|
|
107
102
|
end
|
|
108
103
|
|
|
109
|
-
|
|
104
|
+
# Owner rule 2026-08-31: has this session already started a conversation?
|
|
105
|
+
# A conversation session's tmp directory is made at boot under the global
|
|
106
|
+
# store's `.tmp/`; a dispatched or headless session (a derived key) never
|
|
107
|
+
# gets one. Reads only; rescues to false (fail open: a broken store must
|
|
108
|
+
# never block a legitimate delivery).
|
|
109
|
+
def started_session?(session, home:)
|
|
110
110
|
store = global_store(home)
|
|
111
|
-
SessionLedger.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
File.write(path, "#{value}\n")
|
|
115
|
-
path
|
|
111
|
+
Dir.exist?(SessionLedger.session_tmp_dir(store, SessionLedger.short_session_id(nil, session)))
|
|
112
|
+
rescue StandardError
|
|
113
|
+
false
|
|
116
114
|
end
|
|
117
115
|
|
|
118
116
|
# --- arm ---------------------------------------------------------------------
|
|
119
117
|
|
|
120
118
|
# Take an intent for `session`: acquire delivery.lock (stamped with the run
|
|
121
|
-
# mode and the provenance the caller knows)
|
|
122
|
-
# (fail-open for a global-only or non-git intent)
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
119
|
+
# mode and the provenance the caller knows) and provision the code worktree
|
|
120
|
+
# (fail-open for a global-only or non-git intent). Returns
|
|
121
|
+
# `{status:, lock:, worktree:, session:}`. A held, stale, excluded, or
|
|
122
|
+
# corrupt lock returns that status with the lock data read and touches
|
|
123
|
+
# nothing.
|
|
126
124
|
def arm(intent_dir:, session:, mode: "auto", home: Dir.home, harness: nil,
|
|
127
125
|
agent: nil, model: nil, thread: nil, now: Time.now, runner: Worktree::ShellRunner.new,
|
|
128
|
-
host: Socket.gethostname)
|
|
126
|
+
host: Socket.gethostname, allow_inline: false)
|
|
129
127
|
raise ArgumentError, "mode must be auto or guided" unless %w[auto guided].include?(mode.to_s)
|
|
130
128
|
dir = File.expand_path(intent_dir)
|
|
131
129
|
key = resolve_session(session, store: store_for(dir), intent_id: intent_id_for(dir))
|
|
132
130
|
h = home_for(dir, home: home)
|
|
133
131
|
|
|
132
|
+
# Owner rule 2026-08-31: the main session never delivers an intent inline.
|
|
133
|
+
# A conversation session (its tmp directory already exists under the
|
|
134
|
+
# global store) arming an intent it does not already hold the lock for is
|
|
135
|
+
# inline delivery and is refused BEFORE any lock is taken. Re-arming the
|
|
136
|
+
# intent it already holds stays idempotent. A dispatched or headless
|
|
137
|
+
# session has no tmp directory and arms freely. --allow-inline is the
|
|
138
|
+
# explicit owner override.
|
|
139
|
+
if !allow_inline && started_session?(key, home: h) && !Lock.holds?(dir, session: key)
|
|
140
|
+
return { status: :inline_refused, lock: nil, worktree: nil, session: key }
|
|
141
|
+
end
|
|
142
|
+
|
|
134
143
|
status, lock = Lock.acquire(dir, session: key, host: host, now: now,
|
|
135
144
|
harness: harness, agent: agent, model: model,
|
|
136
145
|
thread: thread, run_mode: mode.to_s)
|
|
137
146
|
unless %i[acquired owned].include?(status)
|
|
138
|
-
return { status: status, lock: lock, worktree: nil, session: key
|
|
147
|
+
return { status: status, lock: lock, worktree: nil, session: key }
|
|
139
148
|
end
|
|
140
149
|
|
|
141
|
-
data =
|
|
150
|
+
data = delivery(intent_dir: dir, home: h, with_worktree: false)
|
|
142
151
|
begin
|
|
143
152
|
Worktree.provision(data, home: h, runner: runner)
|
|
144
153
|
rescue StandardError => e
|
|
145
154
|
warn "plastic: worktree provision raised, continuing unprovisioned: #{e.message}"
|
|
146
155
|
end
|
|
147
156
|
|
|
148
|
-
# The pointer is what the capture and record hooks read; they key on the
|
|
149
|
-
# harness's real session id, so a derived key gets no pointer (nothing would
|
|
150
|
-
# ever read it). A pointer failure warns and never undoes an acquired lock.
|
|
151
|
-
pointer = nil
|
|
152
|
-
unless key == derive_key(store_for(dir), intent_id_for(dir))
|
|
153
|
-
begin
|
|
154
|
-
pointer = write_pointer(key, intent_id_for(dir), home: h)
|
|
155
|
-
rescue StandardError => e
|
|
156
|
-
warn "plastic: session pointer not written (#{e.message}); the lock is held regardless"
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
157
|
{ status: status, lock: lock, worktree: worktree_block(intent_dir: dir, home: h),
|
|
160
|
-
session: key
|
|
158
|
+
session: key }
|
|
161
159
|
end
|
|
162
160
|
|
|
163
161
|
# --- disarm ------------------------------------------------------------------
|
|
164
162
|
|
|
165
|
-
# Give the intent back: remove the worktree (when `remove`)
|
|
166
|
-
# lock as its recorded owner (falling back to `session`)
|
|
167
|
-
#
|
|
168
|
-
# the lock release status (:released, :none, :not_owner, or :raised).
|
|
163
|
+
# Give the intent back: remove the worktree (when `remove`) and release the
|
|
164
|
+
# lock as its recorded owner (falling back to `session`). Returns the lock
|
|
165
|
+
# release status (:released, :none, :not_owner, or :raised).
|
|
169
166
|
def disarm(intent_dir:, session:, home: Dir.home, runner: Worktree::ShellRunner.new,
|
|
170
167
|
remove: true, now: Time.now)
|
|
171
168
|
dir = File.expand_path(intent_dir)
|
|
@@ -173,45 +170,36 @@ module Arm
|
|
|
173
170
|
key = resolve_session(session, store: store_for(dir), intent_id: intent_id_for(dir))
|
|
174
171
|
|
|
175
172
|
begin
|
|
176
|
-
Worktree.release(
|
|
173
|
+
Worktree.release(delivery(intent_dir: dir, home: h), home: h, runner: runner, remove: remove)
|
|
177
174
|
rescue StandardError => e
|
|
178
175
|
warn "plastic: worktree release raised, continuing: #{e.message}"
|
|
179
176
|
end
|
|
180
177
|
|
|
181
178
|
lock = Lock.read(dir)
|
|
182
179
|
owner = lock && !blank?(lock["owner_session"]) ? lock["owner_session"] : key
|
|
183
|
-
|
|
180
|
+
begin
|
|
184
181
|
Lock.release(dir, session: owner)
|
|
185
182
|
rescue StandardError => e
|
|
186
183
|
warn "plastic: delivery lock release raised for #{dir}, continuing: #{e.message}"
|
|
187
184
|
:raised
|
|
188
185
|
end
|
|
189
|
-
|
|
190
|
-
begin
|
|
191
|
-
reset_pointer(key, intent_id_for(dir), home: h, now: now)
|
|
192
|
-
rescue StandardError => e
|
|
193
|
-
warn "plastic: session pointer not reset (#{e.message})"
|
|
194
|
-
end
|
|
195
|
-
release_status
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Point the session back at the day ledger, but only when it names `intent_id`.
|
|
199
|
-
def reset_pointer(session, intent_id, home:, now: Time.now)
|
|
200
|
-
current = read_pointer(session, home: home)
|
|
201
|
-
return false unless current == intent_id.to_s
|
|
202
|
-
write_pointer(session, SessionLedger.day_id(now), home: home)
|
|
203
|
-
true
|
|
204
186
|
end
|
|
205
187
|
|
|
206
188
|
# --- repair ------------------------------------------------------------------
|
|
207
189
|
|
|
208
|
-
#
|
|
190
|
+
# A stale foreign lock is reclaimed only by an audited owner step, never by
|
|
191
|
+
# the session that found it.
|
|
192
|
+
def stale_hint(intent_id)
|
|
193
|
+
"reclaiming a stale lock is the owner's audited step; plastic auto lock status #{intent_id} shows it"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# One idempotent repair (the lock half of the repair path):
|
|
209
197
|
# remove a corrupt lock, back off from a fresh foreign lock (`held`), report
|
|
210
198
|
# a stale foreign lock (`stale`) for the explicit reclaim verb, keep and
|
|
211
199
|
# enrich an own lock, heartbeat a delegated one, acquire when none, and
|
|
212
200
|
# provision the worktree so the repaired intent has its checkout.
|
|
213
201
|
def repair(intent_dir:, session:, home: Dir.home, now: Time.now, harness: nil,
|
|
214
|
-
agent: nil, model: nil, thread: nil, run_mode: nil,
|
|
202
|
+
agent: nil, model: nil, thread: nil, run_mode: nil,
|
|
215
203
|
runner: Worktree::ShellRunner.new)
|
|
216
204
|
dir = File.expand_path(intent_dir)
|
|
217
205
|
h = home_for(dir, home: home)
|
|
@@ -231,11 +219,18 @@ module Arm
|
|
|
231
219
|
end
|
|
232
220
|
return { "status" => "stale", "owner" => lock["owner_session"],
|
|
233
221
|
"actions" => actions, "session" => key,
|
|
234
|
-
"hint" =>
|
|
235
|
-
"reclaim the lock to take over with an audit" }
|
|
222
|
+
"hint" => stale_hint(intent_id_for(dir)) }
|
|
236
223
|
end
|
|
237
224
|
|
|
238
|
-
|
|
225
|
+
# A lock rebuilt from nothing (none, or a corrupt one removed) is an auto
|
|
226
|
+
# delivery's lock again; a kept lock keeps whatever mode it had.
|
|
227
|
+
mode = if !blank?(run_mode)
|
|
228
|
+
run_mode.to_s
|
|
229
|
+
elsif lock
|
|
230
|
+
lock["run_mode"]
|
|
231
|
+
else
|
|
232
|
+
"auto"
|
|
233
|
+
end
|
|
239
234
|
if lock
|
|
240
235
|
if lock["owner_session"].to_s == key.to_s
|
|
241
236
|
lock_data = lock.dup
|
|
@@ -259,7 +254,7 @@ module Arm
|
|
|
259
254
|
end
|
|
260
255
|
|
|
261
256
|
begin
|
|
262
|
-
Worktree.provision(
|
|
257
|
+
Worktree.provision(delivery(intent_dir: dir, home: h, with_worktree: false), home: h, runner: runner)
|
|
263
258
|
rescue StandardError => e
|
|
264
259
|
warn "plastic: worktree provision raised during repair, continuing unprovisioned: #{e.message}"
|
|
265
260
|
end
|
|
@@ -268,16 +263,4 @@ module Arm
|
|
|
268
263
|
|
|
269
264
|
{ "status" => "repaired", "actions" => actions, "lock" => lock_data, "session" => key }
|
|
270
265
|
end
|
|
271
|
-
|
|
272
|
-
# The intent directory this session's pointer names, searched in the given
|
|
273
|
-
# stores in order; nil when the pointer is absent, empty, or a day id.
|
|
274
|
-
def intent_dir_from_pointer(session, home:, stores:)
|
|
275
|
-
current = read_pointer(session, home: home)
|
|
276
|
-
return nil if blank?(current) || SessionLedger.valid_day_id?(current)
|
|
277
|
-
stores.each do |store|
|
|
278
|
-
match = Dir.glob(File.join(store, "#{current}--*")).find { |p| File.directory?(p) }
|
|
279
|
-
return match if match
|
|
280
|
-
end
|
|
281
|
-
nil
|
|
282
|
-
end
|
|
283
266
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# AtomicWrite (intent 334, n2, D19r): a sibling-temp-plus-rename write, the
|
|
5
|
+
# same shape as Lock#write (scripts/lib/lock.rb) - a content write is never
|
|
6
|
+
# an in-place truncate, so a crash mid-write can never leave an empty or
|
|
7
|
+
# half-written target on disk. The temp file is a SIBLING in the same
|
|
8
|
+
# directory as the target, never a system tmpdir, because File.rename can
|
|
9
|
+
# raise EXDEV when the temp and the target live on different filesystems.
|
|
10
|
+
# The temp name ends in .lock (post-execution review, non-blocking 5), the
|
|
11
|
+
# same property Lock#write_temp_path documents: an orphan left by a crash
|
|
12
|
+
# between the write and the rename is covered by the store's existing
|
|
13
|
+
# *.lock gitignore rule rather than swept into its git add -A auto-commit.
|
|
14
|
+
#
|
|
15
|
+
# The renamer is injectable so the interrupted-write case (a rename that
|
|
16
|
+
# raises) is testable with dependency injection, never eval or a global
|
|
17
|
+
# (D19r).
|
|
18
|
+
module AtomicWrite
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def write(path, content, renamer: File.method(:rename))
|
|
22
|
+
dir = File.dirname(path)
|
|
23
|
+
temp = File.join(dir, ".#{File.basename(path)}.tmp.#{Process.pid}.#{Time.now.to_f}.#{rand(0xFFFFFF)}.lock")
|
|
24
|
+
File.write(temp, content)
|
|
25
|
+
renamer.call(temp, path)
|
|
26
|
+
true
|
|
27
|
+
rescue StandardError
|
|
28
|
+
File.delete(temp) if temp && File.exist?(temp)
|
|
29
|
+
raise
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -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,92 @@
|
|
|
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.validate_scope
|
|
22
|
+
command.call
|
|
23
|
+
command.flush
|
|
24
|
+
OK
|
|
25
|
+
rescue OptionParser::ParseError, Scope::UnknownProject, Usage => e
|
|
26
|
+
command.output.usage(e.message, self::USAGE_LINE)
|
|
27
|
+
USAGE
|
|
28
|
+
rescue Refusal => e
|
|
29
|
+
command.output.refused(e.message)
|
|
30
|
+
REFUSED
|
|
31
|
+
rescue Failure => e
|
|
32
|
+
command.output.failed(e.message)
|
|
33
|
+
FAILED
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :output
|
|
37
|
+
|
|
38
|
+
def initialize(argv, out:, err:, env: ENV, home: Dir.home, directory: Dir.pwd)
|
|
39
|
+
@argv = argv.dup
|
|
40
|
+
@env = env
|
|
41
|
+
@home = home
|
|
42
|
+
@directory = directory
|
|
43
|
+
@output = Output.new(out: out, err: err, json: @argv.include?("--json"))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call
|
|
47
|
+
raise NoMethodError, "#{self.class} must define call"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def validate_scope
|
|
51
|
+
scope.slug if @argv.grep(/\A--project(?:=|$)/).any?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def flush
|
|
55
|
+
@output.project = scope.slug
|
|
56
|
+
@output.flush(json: options[:json])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def options
|
|
62
|
+
return @options if @options
|
|
63
|
+
|
|
64
|
+
@options = {}
|
|
65
|
+
parser.parse!(@argv)
|
|
66
|
+
@options
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def arguments
|
|
70
|
+
options
|
|
71
|
+
@argv
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parser
|
|
75
|
+
@parser ||= OptionParser.new do |o|
|
|
76
|
+
o.banner = self.class::USAGE_LINE
|
|
77
|
+
o.on("--json") { @options[:json] = true }
|
|
78
|
+
o.on("--project SLUG") { |slug| @options[:project] = slug }
|
|
79
|
+
switches(o)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def switches(_parser)
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def scope
|
|
88
|
+
@scope ||= Scope.new(env: @env, home: @home, slug: options[:project], directory: @directory)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
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
|