@zalom/plastic 2.0.0-alpha.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLASTIC.md +13 -136
- package/README.md +357 -133
- package/agents/plastic-enforcer.md +20 -15
- package/agents/plastic-executor.md +15 -4
- package/agents/plastic-node-research.md +30 -0
- package/agents/plastic-node-verify.md +28 -0
- package/agents/plastic-node-work.md +33 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/assets/plastic-logo.svg +1 -0
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +43 -6
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +6 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/test +24 -4
- package/bin/verify-change +345 -0
- package/config_asks.yml +4 -4
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
- package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
- package/{skills/conventions/references → docs/help}/completion-and-done.md +15 -16
- package/docs/help/human-report-contract.md +152 -0
- package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
- package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +11 -13
- package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
- package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/hooks/call-budget +4 -0
- package/hooks/hooks.json +24 -0
- package/hooks/message-display +55 -2
- package/hooks/session-start +5 -1
- package/hooks/statusline +32 -27
- package/hooks/stop +5 -0
- package/package.json +5 -3
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +267 -16
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +680 -39
- package/scripts/end-intent +153 -30
- package/scripts/exec-worktree +5 -5
- package/scripts/file-session-intent +2 -1
- package/scripts/graph-measure +249 -0
- package/scripts/hook-call-budget +222 -0
- package/scripts/hook-capture +24 -123
- package/scripts/hook-close +2 -1
- package/scripts/hook-message-display +7 -0
- package/scripts/hook-record +24 -16
- package/scripts/hook-savepoint +27 -3
- package/scripts/hook-session-start +359 -321
- package/scripts/hook-stop +58 -0
- package/scripts/index-projection +74 -0
- package/scripts/insight-append +17 -4
- package/scripts/install.rb +9 -7
- package/scripts/lib/action_graph_shim.rb +279 -0
- package/scripts/lib/active_delivery.rb +105 -0
- package/scripts/lib/agent_models.rb +63 -25
- package/scripts/lib/arm.rb +48 -83
- package/scripts/lib/atomic_write.rb +31 -0
- package/scripts/lib/backup.rb +65 -0
- package/scripts/lib/cli/command.rb +92 -0
- package/scripts/lib/cli/commands/auto.rb +18 -0
- package/scripts/lib/cli/commands/auto_brief.rb +44 -0
- package/scripts/lib/cli/commands/auto_lock.rb +99 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +31 -0
- package/scripts/lib/cli/commands/backup.rb +43 -0
- package/scripts/lib/cli/commands/checkout.rb +25 -0
- package/scripts/lib/cli/commands/continue.rb +66 -0
- package/scripts/lib/cli/commands/doctor.rb +81 -0
- package/scripts/lib/cli/commands/feedback.rb +40 -0
- package/scripts/lib/cli/commands/help.rb +69 -0
- package/scripts/lib/cli/commands/hook.rb +32 -0
- package/scripts/lib/cli/commands/index.rb +23 -0
- package/scripts/lib/cli/commands/install.rb +21 -0
- package/scripts/lib/cli/commands/installer_verb.rb +37 -0
- package/scripts/lib/cli/commands/intent.rb +19 -0
- package/scripts/lib/cli/commands/intent_answer.rb +37 -0
- package/scripts/lib/cli/commands/intent_command.rb +53 -0
- package/scripts/lib/cli/commands/intent_end.rb +62 -0
- package/scripts/lib/cli/commands/intent_new.rb +62 -0
- package/scripts/lib/cli/commands/intent_note.rb +43 -0
- package/scripts/lib/cli/commands/intent_rule.rb +36 -0
- package/scripts/lib/cli/commands/intent_show.rb +32 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +71 -0
- package/scripts/lib/cli/commands/intent_verify.rb +26 -0
- package/scripts/lib/cli/commands/migrate.rb +16 -0
- package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
- package/scripts/lib/cli/commands/next.rb +54 -0
- package/scripts/lib/cli/commands/project.rb +19 -0
- package/scripts/lib/cli/commands/project_links.rb +42 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +72 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +33 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +70 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
- package/scripts/lib/cli/commands/rollback.rb +20 -0
- package/scripts/lib/cli/commands/search.rb +60 -0
- package/scripts/lib/cli/commands/session.rb +18 -0
- package/scripts/lib/cli/commands/session_commit.rb +42 -0
- package/scripts/lib/cli/commands/session_handoff.rb +34 -0
- package/scripts/lib/cli/commands/session_summary.rb +35 -0
- package/scripts/lib/cli/commands/status.rb +68 -0
- package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
- package/scripts/lib/cli/commands/sync.rb +46 -0
- package/scripts/lib/cli/commands/uninstall.rb +20 -0
- package/scripts/lib/cli/commands/update.rb +20 -0
- package/scripts/lib/cli/commands/version.rb +53 -0
- package/scripts/lib/cli/frontier.rb +86 -0
- package/scripts/lib/cli/intent_progress.rb +36 -0
- package/scripts/lib/cli/legacy.rb +80 -0
- package/scripts/lib/cli/output.rb +137 -0
- package/scripts/lib/cli/scope.rb +134 -0
- package/scripts/lib/cli/table.rb +65 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +198 -0
- package/scripts/lib/compact_instructions.rb +13 -5
- package/scripts/lib/core_integrity.rb +71 -0
- package/scripts/lib/dashboard_screen.rb +40 -0
- package/scripts/lib/data_boundary.rb +132 -0
- package/scripts/lib/day_summary.rb +23 -14
- package/scripts/lib/doctor_core.rb +114 -38
- package/scripts/lib/doctor_session_ledger.rb +5 -53
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_edges.rb +137 -0
- package/scripts/lib/graph_file.rb +246 -0
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +409 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +413 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +98 -0
- package/scripts/lib/guarded_append.rb +155 -0
- package/scripts/lib/handoff.rb +40 -13
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +27 -3
- package/scripts/lib/hook_replay.rb +229 -0
- package/scripts/lib/index_entry.rb +62 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +531 -71
- package/scripts/lib/intent_screen.rb +4 -4
- package/scripts/lib/intent_screen_ansi.rb +73 -12
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/lock.rb +10 -11
- package/scripts/lib/message_display.rb +350 -54
- package/scripts/lib/meter_watch.rb +185 -0
- package/scripts/lib/node_file.rb +234 -0
- package/scripts/lib/node_ids.rb +99 -0
- package/scripts/lib/node_input.rb +913 -0
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +386 -0
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/node_return.rb +204 -0
- package/scripts/lib/node_worktree.rb +337 -0
- package/scripts/lib/outcome_report.rb +440 -0
- package/scripts/lib/preflight.rb +4 -6
- package/scripts/lib/project_config.rb +46 -0
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/ready_set.rb +462 -0
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +18 -0
- package/scripts/lib/report_screen.rb +1371 -47
- package/scripts/lib/rlm/corpus.rb +13 -0
- package/scripts/lib/rlm/probe.rb +29 -0
- package/scripts/lib/rlm/query.rb +22 -0
- package/scripts/lib/roadmap_graph.rb +210 -0
- package/scripts/lib/roadmap_migration.rb +95 -0
- package/scripts/lib/roadmap_queue.rb +158 -8
- package/scripts/lib/roadmap_render.rb +150 -0
- package/scripts/lib/roadmap_savepoint.rb +64 -14
- package/scripts/lib/runner_absorb.rb +703 -0
- package/scripts/lib/runner_answer.rb +206 -0
- package/scripts/lib/runner_core.rb +194 -0
- package/scripts/lib/runner_dispatch.rb +525 -0
- package/scripts/lib/runner_policy.rb +191 -0
- package/scripts/lib/runner_proposals.rb +275 -0
- package/scripts/lib/runner_rewind.rb +201 -0
- package/scripts/lib/runner_sweep.rb +231 -0
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +141 -19
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/screen_paint.rb +365 -28
- package/scripts/lib/screens/dashboard.rb +20 -0
- package/scripts/lib/screens/plan.rb +18 -0
- package/scripts/lib/screens/roadmap.rb +15 -0
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +25 -18
- package/scripts/lib/session_ledger.rb +48 -4
- package/scripts/lib/session_usage.rb +190 -0
- package/scripts/lib/sqlite.rb +22 -0
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/store_discovery.rb +7 -6
- package/scripts/lib/store_layout.rb +54 -0
- package/scripts/lib/store_provisioning.rb +2 -1
- package/scripts/lib/store_sync.rb +85 -0
- package/scripts/lib/stores_move.rb +93 -0
- package/scripts/lib/verify_intent.rb +36 -8
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/lib/worktree.rb +27 -32
- package/scripts/lib/worktree_sweep.rb +6 -5
- package/scripts/link-suggest +2 -1
- package/scripts/meter-watch +57 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +4 -13
- package/scripts/node-input +92 -0
- package/scripts/node-run +225 -0
- package/scripts/node-transition +291 -0
- package/scripts/outcome-report +74 -0
- package/scripts/plastic-lock +42 -33
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +53 -9
- package/scripts/ready-set +126 -0
- package/scripts/release-check +123 -0
- package/scripts/report-screen +177 -16
- package/scripts/roadmap-graph +119 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/runner +581 -0
- package/scripts/savepoint-note +11 -9
- package/scripts/session-commit +2 -1
- package/scripts/session-usage +56 -0
- package/scripts/skill-lint +115 -6
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +25 -4
- package/scripts/validate-work-graph +39 -0
- package/scripts/verify-intent +3 -2
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +7 -7
- package/templates/config.yml +16 -9
- package/templates/dashboard-screen.md +22 -0
- package/templates/display-fixture.md +21 -0
- package/templates/graph.md +16 -0
- package/templates/index.md +1 -1
- package/templates/intent-screen.md +1 -1
- package/templates/node-decision.md +11 -0
- package/templates/node-research.md +13 -0
- package/templates/node-verify.md +13 -0
- package/templates/node-work.md +22 -0
- package/templates/outcome.md +8 -3
- package/templates/project.yml +1 -1
- package/templates/render.css +10 -0
- package/templates/report-plan.md +15 -0
- package/templates/report-roadmap-delivered.md +10 -0
- package/templates/report-roadmap-plan.md +9 -0
- package/templates/report-roadmap-state.md +9 -0
- package/templates/report-state.md +1 -1
- package/templates/roadmap.md +13 -0
- package/bin/plastic.js +0 -70
- package/scripts/lib/bridge.rb +0 -116
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -296
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -66
- package/skills/auto/references/human-report-contract.md +0 -78
- package/skills/conventions/SKILL.md +0 -29
- package/skills/dashboard/SKILL.md +0 -169
- package/skills/dashboard/evals/evals.json +0 -38
- package/skills/dashboard/references/classification.md +0 -22
- package/skills/dashboard/templates/dashboard-global.md +0 -20
- package/skills/dashboard/templates/dashboard-project.md +0 -19
- package/skills/direct/SKILL.md +0 -66
- package/skills/direct/references/request-signals.md +0 -59
- package/skills/doctor/SKILL.md +0 -299
- package/skills/doctor/report.md +0 -102
- package/skills/feedback/SKILL.md +0 -98
- package/skills/feedback/references/transport-and-privacy.md +0 -65
- package/skills/feedback/report.md +0 -36
- package/skills/install/SKILL.md +0 -217
- package/skills/intent-continuing/SKILL.md +0 -154
- package/skills/intent-continuing/references/board-fill.md +0 -43
- package/skills/intent-continuing/references/boarding-matrix.md +0 -34
- package/skills/intent-continuing/references/context-management.md +0 -28
- package/skills/intent-continuing/references/liveness-ranking.md +0 -57
- package/skills/intent-creating/SKILL.md +0 -164
- package/skills/intent-creating/evals/evals.json +0 -72
- package/skills/intent-creating/references/lifecycle.md +0 -81
- package/skills/intent-creating/references/wikilinks.md +0 -8
- package/skills/intent-ending/SKILL.md +0 -176
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -170
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -42
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -130
- package/skills/intent-speccing/evals/evals.json +0 -126
- package/skills/intent-speccing/references/design-principles.md +0 -44
- package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
- package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
- package/skills/project-creating/SKILL.md +0 -162
- package/skills/project-creating/references/hubs-projects.md +0 -55
- package/skills/project-creating/references/project-scaffolding.md +0 -97
- package/skills/releasing/SKILL.md +0 -337
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -66
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -64
- package/skills/roadmap/references/file-format.md +0 -124
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -65
- package/skills/tutorial/evals/evals.json +0 -186
- package/skills/uninstall/SKILL.md +0 -75
- package/skills/update/SKILL.md +0 -126
- /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
- /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "open3"
|
|
5
|
-
require_relative "bridge"
|
|
6
5
|
require_relative "arm"
|
|
7
6
|
require_relative "lock"
|
|
8
7
|
require_relative "worktree"
|
|
@@ -29,8 +28,8 @@ require_relative "scaffold_intent"
|
|
|
29
28
|
# fail-open, see the merge-verification note below), `finisher:` (defaults to
|
|
30
29
|
# `Worktree.method(:finish)`), `status_checker:` (defaults to a `git status --porcelain`
|
|
31
30
|
# lambda). Every seam exists so
|
|
32
|
-
# tests run with no real git, no real worktree removal, and no real
|
|
33
|
-
# tmpdir.
|
|
31
|
+
# tests run with no real git, no real worktree removal, and no real delivery-lock file
|
|
32
|
+
# outside a tmpdir.
|
|
34
33
|
#
|
|
35
34
|
# MERGE VERIFICATION (delivered disposition only). `Worktree.merge_branch` is deliberately
|
|
36
35
|
# fail-open: on a conflict it runs `git merge --abort`, warns, and returns false, and
|
|
@@ -39,7 +38,7 @@ require_relative "scaffold_intent"
|
|
|
39
38
|
# module never trusts the discarded return value; it asks git directly, AFTER finish, whether
|
|
40
39
|
# the intent's code branch is now an ancestor of the repo's current HEAD
|
|
41
40
|
# (`merged_into_current_branch?` below). `repo` and `branch` are captured BEFORE the finisher
|
|
42
|
-
# runs, because finish deletes the
|
|
41
|
+
# runs, because finish deletes the delivery's `worktree` block.
|
|
43
42
|
#
|
|
44
43
|
# DOES NOT RUN A TEST SUITE, in any form. `verify-intent` owns test execution; one concern
|
|
45
44
|
# per script (spec D8).
|
|
@@ -60,6 +59,10 @@ module ExecWorktree
|
|
|
60
59
|
# [stdout, stderr, status] triple Open3.capture3 returns (status responds to
|
|
61
60
|
# success?). A raised error (git missing, corrupt repo) is caught and reported as a
|
|
62
61
|
# nil status, so the caller can fail closed the same way a failed exit status would.
|
|
62
|
+
def blank?(value)
|
|
63
|
+
value.nil? || value.to_s.strip.empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
63
66
|
def default_status_checker
|
|
64
67
|
lambda do |worktree_code|
|
|
65
68
|
Open3.capture3("git", "-C", worktree_code, "status", "--porcelain")
|
|
@@ -85,7 +88,7 @@ module ExecWorktree
|
|
|
85
88
|
# `<repo>/.claude/worktrees/<name>` (scripts/lib/worktree.rb:75), so walking up three
|
|
86
89
|
# directories recovers `<repo>` deterministically, with no I/O and no git.
|
|
87
90
|
def repo_from_worktree_code(worktree_code)
|
|
88
|
-
return nil if
|
|
91
|
+
return nil if blank?(worktree_code)
|
|
89
92
|
File.dirname(File.dirname(File.dirname(worktree_code.to_s)))
|
|
90
93
|
end
|
|
91
94
|
|
|
@@ -94,7 +97,7 @@ module ExecWorktree
|
|
|
94
97
|
# i.e. the merge actually landed. Fails CLOSED (not merged) on a blank repo/branch or a
|
|
95
98
|
# raising runner, so an inconclusive check is never reported as a success.
|
|
96
99
|
def merged_into_current_branch?(runner, repo:, branch:)
|
|
97
|
-
return false if
|
|
100
|
+
return false if blank?(repo) || blank?(branch)
|
|
98
101
|
runner.run("-C", repo, "merge-base", "--is-ancestor", branch, "HEAD").success?
|
|
99
102
|
rescue StandardError
|
|
100
103
|
false
|
|
@@ -106,12 +109,12 @@ module ExecWorktree
|
|
|
106
109
|
# The lock-owner fallback is correct here: this is a teardown step on an intent this
|
|
107
110
|
# session has been delivering.
|
|
108
111
|
def resolve_session(explicit, env_session, intent_dir)
|
|
109
|
-
return explicit.to_s.strip unless
|
|
110
|
-
return env_session.to_s.strip unless
|
|
112
|
+
return explicit.to_s.strip unless blank?(explicit)
|
|
113
|
+
return env_session.to_s.strip unless blank?(env_session)
|
|
111
114
|
lock = Lock.read(intent_dir)
|
|
112
115
|
return nil unless lock
|
|
113
116
|
owner = lock["owner_session"].to_s
|
|
114
|
-
|
|
117
|
+
blank?(owner) ? nil : owner
|
|
115
118
|
end
|
|
116
119
|
|
|
117
120
|
# --- result builders (the exit_code/stdout/stderr shape the thin CLIs share) --------
|
|
@@ -181,7 +184,7 @@ module ExecWorktree
|
|
|
181
184
|
merge_line =
|
|
182
185
|
if disposition != "delivered"
|
|
183
186
|
"not attempted (abandoned)"
|
|
184
|
-
elsif
|
|
187
|
+
elsif blank?(target_branch)
|
|
185
188
|
"merged (integration target branch could not be resolved)"
|
|
186
189
|
else
|
|
187
190
|
"merged into #{target_branch}"
|
|
@@ -202,10 +205,10 @@ module ExecWorktree
|
|
|
202
205
|
runner: Worktree::ShellRunner.new,
|
|
203
206
|
finisher: Worktree.method(:finish),
|
|
204
207
|
status_checker: default_status_checker)
|
|
205
|
-
return usage_result("--store is required") if
|
|
206
|
-
return usage_result("--id is required") if
|
|
207
|
-
return usage_result("--home is required") if
|
|
208
|
-
return usage_result("--disposition is required") if
|
|
208
|
+
return usage_result("--store is required") if blank?(store)
|
|
209
|
+
return usage_result("--id is required") if blank?(id)
|
|
210
|
+
return usage_result("--home is required") if blank?(home)
|
|
211
|
+
return usage_result("--disposition is required") if blank?(disposition)
|
|
209
212
|
unless DISPOSITIONS.include?(disposition)
|
|
210
213
|
return usage_result("--disposition must be one of #{DISPOSITIONS.join('|')} (got #{disposition.inspect})")
|
|
211
214
|
end
|
|
@@ -220,16 +223,16 @@ module ExecWorktree
|
|
|
220
223
|
|
|
221
224
|
# Step 0: session resolution + pre-flight refusal (D3-mirroring; see resolve_session).
|
|
222
225
|
key_session = resolve_session(session, env_session, intent_dir)
|
|
223
|
-
if
|
|
226
|
+
if blank?(key_session) && File.exist?(Lock.path(intent_dir))
|
|
224
227
|
return deny_result(EXIT_UNRESOLVED, no_session_message(intent_dir))
|
|
225
228
|
end
|
|
226
229
|
|
|
227
|
-
|
|
228
|
-
worktree_code =
|
|
229
|
-
return ok_result(nothing_provisioned_message(intent_dir)) if
|
|
230
|
+
delivery = Arm.delivery(intent_dir: intent_dir, home: normalized_home)
|
|
231
|
+
worktree_code = delivery.dig("worktree", "code")
|
|
232
|
+
return ok_result(nothing_provisioned_message(intent_dir)) if blank?(worktree_code)
|
|
230
233
|
|
|
231
|
-
branch =
|
|
232
|
-
# Captured now, before `finisher.call` below (Worktree.finish) deletes the
|
|
234
|
+
branch = delivery.dig("worktree", "code_branch")
|
|
235
|
+
# Captured now, before `finisher.call` below (Worktree.finish) deletes the delivery's
|
|
233
236
|
# `worktree` block, so the post-finish merge verification (BLOCKING 1/2) still has
|
|
234
237
|
# both `repo` and `branch` to work with.
|
|
235
238
|
repo = repo_from_worktree_code(worktree_code)
|
|
@@ -254,7 +257,7 @@ module ExecWorktree
|
|
|
254
257
|
# disposition only, whether the worktree directory still exists on disk (teardown
|
|
255
258
|
# incomplete); second, for a delivered disposition only, whether the branch actually
|
|
256
259
|
# merged (BLOCKING 1/2, see merged_into_current_branch? above).
|
|
257
|
-
finisher.call(
|
|
260
|
+
finisher.call(delivery, home: normalized_home, runner: runner,
|
|
258
261
|
merge: disposition == "delivered")
|
|
259
262
|
|
|
260
263
|
removed = !Dir.exist?(worktree_code)
|
|
@@ -18,7 +18,7 @@ require "fileutils"
|
|
|
18
18
|
# Trust model: this class never sends anything anywhere. `compose` returns a
|
|
19
19
|
# Result carrying a local file path and a browser URL; only the human, in
|
|
20
20
|
# their own authenticated browser, submits it. There is no send method here
|
|
21
|
-
# and there must never be one (see
|
|
21
|
+
# and there must never be one (see SECURITY.md).
|
|
22
22
|
class FeedbackReport
|
|
23
23
|
GITHUB_REPO = "zalom/plastic"
|
|
24
24
|
CAP_BYTES = 7500
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# GraphEdges (intent 334, n1): the shared `needs` syntax and cycle-path check
|
|
5
|
+
# (327 D2r-D4r, D12r). Parses a "## Graph" section's edge lines into a node
|
|
6
|
+
# set and an edge map, the one parser an intent's graph.md and a roadmap's
|
|
7
|
+
# own ## Graph section both use unchanged (C1, review A5): the same list-item
|
|
8
|
+
# grammar, the same root keyword, the same cycle walk. Deliberately loose
|
|
9
|
+
# about what an id looks like - a roadmap id is numeric ("334", "340a"), a
|
|
10
|
+
# node id carries a kind prefix ("n1") - the kind-prefix rule belongs to
|
|
11
|
+
# NodeFile and WorkGraphValidator, never here (D12r, review A6).
|
|
12
|
+
#
|
|
13
|
+
# Grammar (D2r/D3r): a list item shaped "- <id> needs <target> [<target>
|
|
14
|
+
# ...]", ending at end of line. The single literal target "nothing" declares
|
|
15
|
+
# a root and must be the ONLY target on the line. Any line that does not
|
|
16
|
+
# start with "- <token> needs" is prose and is skipped in silence. A line
|
|
17
|
+
# that DOES look like an edge is held to the grammar strictly: no target at
|
|
18
|
+
# all, "nothing" alongside other targets, or a target token carrying
|
|
19
|
+
# sentence punctuation (a period, a colon, ...) rather than the loose
|
|
20
|
+
# id-token charset, is an error naming the line, never a silently
|
|
21
|
+
# mis-parsed edge or an invented node (review A7).
|
|
22
|
+
#
|
|
23
|
+
# Pure and side-effect-free; never raises across the boundary, matching the
|
|
24
|
+
# Result-hash convention every library in scripts/lib/ follows.
|
|
25
|
+
module GraphEdges
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
EDGE_LEAD_RE = /\A-\s*(\S+)\s+needs\b(.*)\z/.freeze
|
|
29
|
+
TOKEN_RE = /\A[A-Za-z0-9][A-Za-z0-9_-]*\z/.freeze
|
|
30
|
+
ROOT_TARGET = "nothing"
|
|
31
|
+
|
|
32
|
+
# {nodes:, edges:, errors:} - nodes is the declared ids together with every
|
|
33
|
+
# id any edge targets (review A5), in first-seen order; edges maps a declared
|
|
34
|
+
# id to its target ids (empty for a root); errors names each malformed
|
|
35
|
+
# edge-shaped line. Prose lines contribute nothing and raise nothing.
|
|
36
|
+
def parse(section_text)
|
|
37
|
+
declared_order = []
|
|
38
|
+
edges = {}
|
|
39
|
+
targeted = []
|
|
40
|
+
errors = []
|
|
41
|
+
|
|
42
|
+
section_text.to_s.each_line do |raw_line|
|
|
43
|
+
line = raw_line.chomp("\n").chomp("\r")
|
|
44
|
+
stripped = line.strip
|
|
45
|
+
next if stripped.empty?
|
|
46
|
+
|
|
47
|
+
m = stripped.match(EDGE_LEAD_RE)
|
|
48
|
+
next unless m
|
|
49
|
+
|
|
50
|
+
id = m[1]
|
|
51
|
+
raw_targets = m[2].to_s.strip.split(/\s+/)
|
|
52
|
+
|
|
53
|
+
if raw_targets.empty?
|
|
54
|
+
errors << "malformed edge line, no target: #{stripped.inspect}"
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if raw_targets.include?(ROOT_TARGET) && raw_targets.length > 1
|
|
59
|
+
errors << "root target #{ROOT_TARGET.inspect} mixed with a real target, root must be the only target: #{stripped.inspect}"
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
bad_token = raw_targets.find { |t| t != ROOT_TARGET && !t.match?(TOKEN_RE) }
|
|
64
|
+
if bad_token
|
|
65
|
+
errors << "prose tail after targets: #{stripped.inspect}"
|
|
66
|
+
next
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if edges.key?(id)
|
|
70
|
+
errors << "duplicate node declaration for #{id.inspect}: #{stripped.inspect}"
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
targets = raw_targets == [ROOT_TARGET] ? [] : raw_targets
|
|
75
|
+
declared_order << id
|
|
76
|
+
edges[id] = targets
|
|
77
|
+
targeted.concat(targets)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
nodes = declared_order.dup
|
|
81
|
+
targeted.each { |t| nodes << t unless nodes.include?(t) }
|
|
82
|
+
|
|
83
|
+
{ nodes: nodes, edges: edges, errors: errors }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The cycle as a path with the entry id repeated (D4r), or nil when the
|
|
87
|
+
# graph is acyclic. A node reachable by two distinct paths (a diamond) is
|
|
88
|
+
# never mistaken for a cycle: once a node's whole subtree is walked clean
|
|
89
|
+
# it is marked visited and never re-examined.
|
|
90
|
+
def cycle(edges)
|
|
91
|
+
visiting = {}
|
|
92
|
+
visited = {}
|
|
93
|
+
path = []
|
|
94
|
+
|
|
95
|
+
edges.each_key do |node|
|
|
96
|
+
next if visited[node]
|
|
97
|
+
found = walk(node, edges, visiting, visited, path)
|
|
98
|
+
return found if found
|
|
99
|
+
end
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def walk(node, edges, visiting, visited, path)
|
|
104
|
+
if visiting[node]
|
|
105
|
+
idx = path.index(node)
|
|
106
|
+
return path[idx..] + [node]
|
|
107
|
+
end
|
|
108
|
+
return nil if visited[node]
|
|
109
|
+
|
|
110
|
+
visiting[node] = true
|
|
111
|
+
path.push(node)
|
|
112
|
+
(edges[node] || []).each do |target|
|
|
113
|
+
found = walk(target, edges, visiting, visited, path)
|
|
114
|
+
return found if found
|
|
115
|
+
end
|
|
116
|
+
path.pop
|
|
117
|
+
visiting.delete(node)
|
|
118
|
+
visited[node] = true
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The declared work ids whose `needs` reach a verify node without passing
|
|
123
|
+
# through another verify node (343 D7), in declaration order. `kinds` maps
|
|
124
|
+
# an id to its node kind, so this module stays blind to where a kind lives.
|
|
125
|
+
def review_fixes(edges, kinds)
|
|
126
|
+
edges.keys.select { |id| kinds[id] == "work" && reaches_verify?(id, edges, kinds, {}) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def reaches_verify?(node, edges, kinds, seen)
|
|
130
|
+
(edges[node] || []).any? do |target|
|
|
131
|
+
next false if seen[target]
|
|
132
|
+
|
|
133
|
+
seen[target] = true
|
|
134
|
+
kinds[target] == "verify" || reaches_verify?(target, edges, kinds, seen)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "graph_edges"
|
|
5
|
+
require_relative "atomic_write"
|
|
6
|
+
|
|
7
|
+
# GraphFile (intent 334, n2): the four graph.md sections (## Goal,
|
|
8
|
+
# ## Decisions, ## Graph, ## Status), the verify:-none directive, and the two
|
|
9
|
+
# writers - write_status and append_decision. Both writers refuse a cyclic
|
|
10
|
+
# graph and both go through AtomicWrite (review D19r). Fence-aware everywhere a
|
|
11
|
+
# heading is located, so a fenced example carrying a fake "## " line never
|
|
12
|
+
# splits or ends a real section (review A8's sibling concern, applied to
|
|
13
|
+
# section boundaries rather than edge lines).
|
|
14
|
+
module GraphFile
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
FENCE_LINE_RE = /\A\s{0,3}(`{3,}|~{3,})/.freeze
|
|
18
|
+
DIRECTIVE_RE = /\A-\s*verify:\s*none(?:\s+reason=(.*))?\z/i.freeze
|
|
19
|
+
|
|
20
|
+
# {ok:, goal:, decisions:, graph:, status:, verify:, errors:}. `graph` is
|
|
21
|
+
# GraphEdges.parse's own Result hash, over the Graph section text with any
|
|
22
|
+
# verify:-none directive line stripped first (review A8). A missing ## Graph
|
|
23
|
+
# section, or one that declares no nodes, is an error naming which (review
|
|
24
|
+
# A10).
|
|
25
|
+
def parse(path)
|
|
26
|
+
return failure(["graph file not found: #{path}"]) unless File.exist?(path)
|
|
27
|
+
|
|
28
|
+
content = File.read(path)
|
|
29
|
+
goal = section_body(content, "## Goal")
|
|
30
|
+
decisions = section_body(content, "## Decisions")
|
|
31
|
+
graph_section = section_body(content, "## Graph")
|
|
32
|
+
status = section_body(content, "## Status")
|
|
33
|
+
|
|
34
|
+
errors = []
|
|
35
|
+
if graph_section.nil?
|
|
36
|
+
errors << "missing ## Graph section"
|
|
37
|
+
return { ok: false, goal: goal, decisions: decisions, graph: nil, status: status, verify: nil, errors: errors }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
directive, directive_errors, remaining = extract_verify_directive(graph_section)
|
|
41
|
+
errors.concat(directive_errors)
|
|
42
|
+
|
|
43
|
+
parsed_graph = GraphEdges.parse(strip_fenced_blocks(remaining))
|
|
44
|
+
errors.concat(parsed_graph[:errors])
|
|
45
|
+
errors << "## Graph declares no nodes" if parsed_graph[:nodes].empty?
|
|
46
|
+
|
|
47
|
+
{ ok: errors.empty?, goal: goal, decisions: decisions, graph: parsed_graph, status: status, verify: directive, errors: errors }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def failure(errors)
|
|
51
|
+
{ ok: false, goal: nil, decisions: nil, graph: nil, status: nil, verify: nil, errors: errors }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Rows shaped {node:, state:, detail:} parsed back from the ## Status
|
|
55
|
+
# table (D8r's round-trip guarantee).
|
|
56
|
+
def status_rows(path)
|
|
57
|
+
parsed = parse(path)
|
|
58
|
+
rows_from_table(parsed[:status])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Replace the whole ## Status section with rows, creating the section if
|
|
62
|
+
# absent. Refuses a cyclic graph (D8r); goes through AtomicWrite (D19r).
|
|
63
|
+
def write_status(path, rows, renamer: File.method(:rename))
|
|
64
|
+
guard = refuse_if_cyclic(path)
|
|
65
|
+
return guard if guard
|
|
66
|
+
|
|
67
|
+
content = File.read(path)
|
|
68
|
+
new_content = replace_or_append_section(content, "## Status", render_status_table(rows))
|
|
69
|
+
AtomicWrite.write(path, new_content, renamer: renamer)
|
|
70
|
+
{ ok: true, errors: [] }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Append one answered decision as a list item to ## Decisions (C26),
|
|
74
|
+
# leaving every other section byte-identical. Refuses a cyclic graph.
|
|
75
|
+
def append_decision(path, text, renamer: File.method(:rename))
|
|
76
|
+
guard = refuse_if_cyclic(path)
|
|
77
|
+
return guard if guard
|
|
78
|
+
|
|
79
|
+
content = File.read(path)
|
|
80
|
+
bounds = section_bounds(content, "## Decisions")
|
|
81
|
+
return { ok: false, errors: ["missing ## Decisions section"] } unless bounds
|
|
82
|
+
|
|
83
|
+
start_idx, end_idx = bounds
|
|
84
|
+
heading_line_end = line_end(content, start_idx)
|
|
85
|
+
body = content[heading_line_end...end_idx]
|
|
86
|
+
trimmed = body.rstrip
|
|
87
|
+
trailing = body[trimmed.length..]
|
|
88
|
+
new_body = trimmed.empty? ? "- #{text}#{trailing}" : "#{trimmed}\n- #{text}#{trailing}"
|
|
89
|
+
new_content = content[0...heading_line_end] + new_body + content[end_idx..]
|
|
90
|
+
AtomicWrite.write(path, new_content, renamer: renamer)
|
|
91
|
+
{ ok: true, errors: [] }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def refuse_if_cyclic(path)
|
|
95
|
+
parsed = parse(path)
|
|
96
|
+
return { ok: false, errors: parsed[:errors] } if parsed[:graph].nil?
|
|
97
|
+
|
|
98
|
+
cyc = GraphEdges.cycle(parsed[:graph][:edges])
|
|
99
|
+
return { ok: false, errors: ["cyclic graph, refusing to write: #{cyc.join(' > ')}"] } if cyc
|
|
100
|
+
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# --- verify: none directive --------------------------------------------------
|
|
105
|
+
|
|
106
|
+
def extract_verify_directive(graph_section)
|
|
107
|
+
return [nil, [], graph_section] if graph_section.nil?
|
|
108
|
+
|
|
109
|
+
reason = nil
|
|
110
|
+
present = false
|
|
111
|
+
errors = []
|
|
112
|
+
remaining_lines = []
|
|
113
|
+
|
|
114
|
+
graph_section.each_line do |line|
|
|
115
|
+
stripped = line.strip
|
|
116
|
+
m = stripped.match(DIRECTIVE_RE)
|
|
117
|
+
if m
|
|
118
|
+
present = true
|
|
119
|
+
captured = m[1].to_s.strip
|
|
120
|
+
if captured.empty?
|
|
121
|
+
errors << "verify: none requires reason=<text>: #{stripped.inspect}"
|
|
122
|
+
else
|
|
123
|
+
reason = captured
|
|
124
|
+
end
|
|
125
|
+
else
|
|
126
|
+
remaining_lines << line
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
directive = present ? { reason: reason } : nil
|
|
131
|
+
[directive, errors, remaining_lines.join]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# D18: drop every fenced line from `text`, so a fenced example edge inside a
|
|
135
|
+
# hand-written graph.md or roadmap ## Graph section is never handed to
|
|
136
|
+
# GraphEdges as real edge text. Non-fenced lines pass through unchanged.
|
|
137
|
+
def strip_fenced_blocks(text)
|
|
138
|
+
lines = []
|
|
139
|
+
each_fence_line(text) { |line, fenced| lines << line unless fenced }
|
|
140
|
+
lines.join
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# --- fence-aware section location ---------------------------------------------
|
|
144
|
+
|
|
145
|
+
def each_fence_line(text)
|
|
146
|
+
return enum_for(:each_fence_line, text) unless block_given?
|
|
147
|
+
|
|
148
|
+
marker = nil
|
|
149
|
+
text.to_s.each_line do |line|
|
|
150
|
+
if marker
|
|
151
|
+
yield line, true
|
|
152
|
+
m = line.match(FENCE_LINE_RE)
|
|
153
|
+
next unless m && m[1][0] == marker[0] && m[1].length >= marker[1]
|
|
154
|
+
next unless line.sub(FENCE_LINE_RE, "").strip.empty?
|
|
155
|
+
|
|
156
|
+
marker = nil
|
|
157
|
+
else
|
|
158
|
+
m = line.match(FENCE_LINE_RE)
|
|
159
|
+
if m
|
|
160
|
+
marker = [m[1][0], m[1].length]
|
|
161
|
+
yield line, true
|
|
162
|
+
else
|
|
163
|
+
yield line, false
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# [start_of_heading_line, start_of_next_top_level_heading_or_EOF] byte
|
|
170
|
+
# offsets for the FIRST line, outside any fence, whose stripped text
|
|
171
|
+
# exactly equals heading_text. nil when no such line exists.
|
|
172
|
+
def section_bounds(content, heading_text)
|
|
173
|
+
offset = 0
|
|
174
|
+
heading_start = nil
|
|
175
|
+
|
|
176
|
+
each_fence_line(content) do |line, fenced|
|
|
177
|
+
if !fenced && heading_start.nil? && line.strip == heading_text
|
|
178
|
+
heading_start = offset
|
|
179
|
+
elsif !fenced && heading_start && offset > heading_start && line.match?(/\A##[^#]/)
|
|
180
|
+
return [heading_start, offset]
|
|
181
|
+
end
|
|
182
|
+
offset += line.length
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
heading_start ? [heading_start, offset] : nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def section_body(content, heading_text)
|
|
189
|
+
bounds = section_bounds(content, heading_text)
|
|
190
|
+
return nil unless bounds
|
|
191
|
+
|
|
192
|
+
start_idx, end_idx = bounds
|
|
193
|
+
content[(line_end(content, start_idx))...end_idx].to_s
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def line_end(content, start_idx)
|
|
197
|
+
idx = content.index("\n", start_idx)
|
|
198
|
+
idx ? idx + 1 : content.length
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def replace_or_append_section(content, heading_text, body_text)
|
|
202
|
+
rendered_body = body_text.to_s
|
|
203
|
+
rendered_body += "\n" unless rendered_body.end_with?("\n")
|
|
204
|
+
section = "#{heading_text}\n#{rendered_body}"
|
|
205
|
+
|
|
206
|
+
bounds = section_bounds(content, heading_text)
|
|
207
|
+
if bounds
|
|
208
|
+
start_idx, end_idx = bounds
|
|
209
|
+
tail = content[end_idx..].to_s
|
|
210
|
+
section += "\n" unless tail.empty?
|
|
211
|
+
content[0...start_idx] + section + tail
|
|
212
|
+
else
|
|
213
|
+
head = content.dup
|
|
214
|
+
head += "\n" unless head.end_with?("\n")
|
|
215
|
+
head += "\n" unless head.end_with?("\n\n")
|
|
216
|
+
head + section
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# --- table rendering and reading -----------------------------------------------
|
|
221
|
+
|
|
222
|
+
def render_status_table(rows)
|
|
223
|
+
lines = ["| Node | State | Detail |", "| --- | --- | --- |"]
|
|
224
|
+
rows.each { |r| lines << "| #{r[:node]} | #{r[:state]} | #{r[:detail]} |" }
|
|
225
|
+
"#{lines.join("\n")}\n"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def rows_from_table(text)
|
|
229
|
+
return [] if text.nil?
|
|
230
|
+
|
|
231
|
+
lines = []
|
|
232
|
+
each_fence_line(text) do |line, fenced|
|
|
233
|
+
next if fenced
|
|
234
|
+
|
|
235
|
+
stripped = line.strip
|
|
236
|
+
lines << stripped if stripped.start_with?("|")
|
|
237
|
+
end
|
|
238
|
+
sep_idx = lines.index { |l| l.match?(/\A\|[\s:|-]+\|?\z/) }
|
|
239
|
+
return [] unless sep_idx
|
|
240
|
+
|
|
241
|
+
lines[(sep_idx + 1)..].map do |l|
|
|
242
|
+
cells = l.split("|", -1).map(&:strip)[1..-2].to_a
|
|
243
|
+
{ node: cells[0], state: cells[1], detail: cells[2] }
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|