@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
package/scripts/end-intent
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
# 2. Move the intent's INDEX.md line from `## Active` into `## Completed`
|
|
38
38
|
# (delivered) or `## Abandoned` (abandoned), dated today, appending
|
|
39
39
|
# --index-note (if given). Accepts a real em dash OR a plain hyphen as
|
|
40
|
-
# the id/title separator on READ (shared matcher,
|
|
40
|
+
# the id/title separator on READ (shared matcher, IndexEntry.match);
|
|
41
41
|
# always EMITS the real em dash on write. An id that resolves to neither
|
|
42
42
|
# `## Active` nor the terminal section is a loud failure (exit 1); an id
|
|
43
43
|
# already correctly in the terminal section is a quiet, idempotent success.
|
|
@@ -46,10 +46,13 @@
|
|
|
46
46
|
# 5. Disarm (intent 188, D2/D16; intent 307): check the code worktree (derived
|
|
47
47
|
# from projects.yml and the intent id, Arm.worktree_block) for uncommitted
|
|
48
48
|
# changes first (exit 5 if dirty, unless --discard-worktree-changes); call the
|
|
49
|
-
# disarm seam (default Arm.disarm: worktree released, lock cleared
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
49
|
+
# disarm seam (default Arm.disarm: worktree released, lock cleared); verify
|
|
50
|
+
# the durable lock file is actually gone afterward, and release it directly
|
|
51
|
+
# when a foreign-keyed disarm left it.
|
|
52
|
+
#
|
|
53
|
+
# A delivered close never merges code. Its code branch must already be merged
|
|
54
|
+
# into the repo checkout's current branch, or the close refuses before any
|
|
55
|
+
# write (exit 9).
|
|
53
56
|
#
|
|
54
57
|
# Exit codes:
|
|
55
58
|
# 0 ok - the intent is closed and no delivery.lock remains.
|
|
@@ -57,7 +60,7 @@
|
|
|
57
60
|
# nor the terminal section (D11). Deliberately double duty; see plan.md.
|
|
58
61
|
# 2 retired in 2.0 (intent 308): the outcome.md guard reports and proceeds.
|
|
59
62
|
# 3 steps 1-4 already committed, but the delivery lock genuinely could not
|
|
60
|
-
# be cleared (
|
|
63
|
+
# be cleared (see step 5 above).
|
|
61
64
|
# 4 pre-flight refusal: a FRESH foreign lock is held by a live,
|
|
62
65
|
# non-delegate session; a FRESH lock that will not parse (corrupt); or a
|
|
63
66
|
# lock file with no resolvable session identity at all. Authors nothing.
|
|
@@ -68,19 +71,37 @@
|
|
|
68
71
|
# 6 retired in 2.0 (intent 308): the structure check reports and proceeds.
|
|
69
72
|
# 7 hollow delivered report refused (317a D7) - intent 308's one deliberate
|
|
70
73
|
# exception to report-and-proceed; --allow-hollow-report overrides.
|
|
74
|
+
# 8 delivered close of an untouched scaffold refused (acceptance N3):
|
|
75
|
+
# nothing was worked, so there is nothing to deliver. Close it as
|
|
76
|
+
# abandoned, or do the work first. Authors nothing.
|
|
77
|
+
# 9 delivered close refused because its code branch is not merged into the
|
|
78
|
+
# repo checkout's current branch. Refused before any write: nothing is
|
|
79
|
+
# merged, written, or released. Merge the branch, then run the close again.
|
|
80
|
+
#
|
|
81
|
+
# --dry-run runs the same refusals as the real close (exits 8, 9, 7 and 5 with
|
|
82
|
+
# a "would refuse" line) and writes nothing: the hollow-report gate is judged
|
|
83
|
+
# on a scratch copy that went through the same outcome generation and
|
|
84
|
+
# backfill.
|
|
71
85
|
|
|
86
|
+
require_relative "lib/store_layout"
|
|
72
87
|
require "fileutils"
|
|
73
88
|
require "date"
|
|
74
89
|
require "open3"
|
|
90
|
+
require "stringio"
|
|
91
|
+
require "tmpdir"
|
|
75
92
|
require "pathname"
|
|
76
|
-
require_relative "lib/bridge"
|
|
77
93
|
require_relative "lib/arm"
|
|
94
|
+
require_relative "lib/exec_worktree"
|
|
95
|
+
require_relative "lib/index_entry"
|
|
78
96
|
require_relative "lib/savepoint"
|
|
79
97
|
require_relative "lib/lock"
|
|
80
98
|
require_relative "lib/intent_validator"
|
|
81
99
|
require_relative "lib/outcome_guard"
|
|
82
100
|
require_relative "lib/report_screen"
|
|
83
101
|
require_relative "lib/backfill_intent"
|
|
102
|
+
require_relative "lib/outcome_report"
|
|
103
|
+
require_relative "lib/untouched_scaffold"
|
|
104
|
+
require_relative "lib/scaffold_intent"
|
|
84
105
|
|
|
85
106
|
DISPOSITIONS = %w[delivered abandoned].freeze
|
|
86
107
|
|
|
@@ -88,7 +109,7 @@ DISPOSITIONS = %w[delivered abandoned].freeze
|
|
|
88
109
|
# dash (U+2014), the existing INDEX.md convention (store files are exempt from
|
|
89
110
|
# the no-em-dash shipped-file rule). Built from the codepoint, not a literal
|
|
90
111
|
# byte in this source file, so the script's own source stays em-dash free.
|
|
91
|
-
# READING accepts a plain hyphen too, via the shared
|
|
112
|
+
# READING accepts a plain hyphen too, via the shared IndexEntry.match
|
|
92
113
|
# (intent 188, D9/D12): see move_index_to_terminal below.
|
|
93
114
|
EM_DASH = "\u2014"
|
|
94
115
|
|
|
@@ -98,6 +119,10 @@ class UnresolvedIndexEntry < StandardError; end
|
|
|
98
119
|
|
|
99
120
|
# --- Explicit flag parsing (no eval, no global injection) -------------------
|
|
100
121
|
|
|
122
|
+
def blank?(value)
|
|
123
|
+
value.nil? || value.to_s.strip.empty?
|
|
124
|
+
end
|
|
125
|
+
|
|
101
126
|
def parse_args(argv)
|
|
102
127
|
opts = {
|
|
103
128
|
store: nil, id: nil, disposition: nil, index: nil,
|
|
@@ -166,14 +191,65 @@ end
|
|
|
166
191
|
# "projects" (the StoreDiscovery convention, scripts/lib/store_discovery.rb), never by
|
|
167
192
|
# hardcoding ".plastic" as a name.
|
|
168
193
|
def resolve_plastic_home_and_scope(store)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
194
|
+
Plastic::StoreLayout.home_and_scope(store)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# --- outcome generation, before the backfill (intent 339, G6, spec D10) -----
|
|
198
|
+
#
|
|
199
|
+
# Generates outcome.md from graph.md, nodes/, and the ledger, BEFORE the
|
|
200
|
+
# backfill step runs, and only into a file that is missing or still the
|
|
201
|
+
# scaffold placeholder (`Savepoint.stage_file_present?` false) - a real,
|
|
202
|
+
# hand-written outcome.md is never touched (row 7.2). Only when a graph.md
|
|
203
|
+
# exists at all (row 7.3): every intent without one closes exactly as it did
|
|
204
|
+
# before this intent. The model is parsed AFTER the `stage_file_present?`
|
|
205
|
+
# guard, not before: a real hand-written outcome.md returns at that guard
|
|
206
|
+
# regardless of graph.md's own health, so a close that was never going to
|
|
207
|
+
# generate anything must never print a parse warning about it. Refuses a
|
|
208
|
+
# report model that reports itself broken (row v1f.2, B2): a malformed
|
|
209
|
+
# `## Graph` fails `model[:ok]` and the close falls straight through to the
|
|
210
|
+
# backfill, rather than adopting parse damage as delivery fact - agreeing
|
|
211
|
+
# with the shipped `outcome-report` CLI, which exits 3 on the same
|
|
212
|
+
# directory. The generated text is then checked against
|
|
213
|
+
# BOTH close gates - `OutcomeGuard` and the hollow-report gate below - before
|
|
214
|
+
# being adopted (row 7.4); when either would refuse it, the write is
|
|
215
|
+
# reverted and the close falls through to intent 308's unchanged backfill
|
|
216
|
+
# (row 7.5), so no close gains an exit 7 it did not have before this intent.
|
|
217
|
+
# Fails open on any exception (row 7.6), the same report-and-proceed shape
|
|
218
|
+
# every other close step in this file uses. `generator:` is the injectable
|
|
219
|
+
# seam (mirrors run_structure_check's `gate:`).
|
|
220
|
+
def run_generate_outcome(intent_dir, disposition:, generator: OutcomeReport.method(:write))
|
|
221
|
+
graph_path = File.join(intent_dir, "graph.md")
|
|
222
|
+
return unless File.exist?(graph_path)
|
|
223
|
+
|
|
224
|
+
outcome_path = File.join(intent_dir, "outcome.md")
|
|
225
|
+
return if Savepoint.stage_file_present?(outcome_path)
|
|
226
|
+
|
|
227
|
+
report_model = OutcomeReport.model(intent_dir)
|
|
228
|
+
unless report_model[:ok]
|
|
229
|
+
warn "end-intent: graph.md failed to parse (#{report_model[:errors].join('; ')}); falling back to the backfill"
|
|
230
|
+
return
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
original_exists = File.exist?(outcome_path)
|
|
234
|
+
original_content = original_exists ? File.read(outcome_path) : nil
|
|
235
|
+
|
|
236
|
+
generator.call(intent_dir, disposition: disposition)
|
|
237
|
+
|
|
238
|
+
guard_reason = OutcomeGuard.reason(intent_dir, disposition)
|
|
239
|
+
hollow = hollow_report_reason(intent_dir, disposition)
|
|
240
|
+
if guard_reason || hollow
|
|
241
|
+
if original_exists
|
|
242
|
+
File.write(outcome_path, original_content)
|
|
243
|
+
elsif File.exist?(outcome_path)
|
|
244
|
+
File.delete(outcome_path)
|
|
245
|
+
end
|
|
246
|
+
warn "end-intent: generated outcome.md would be refused (#{[guard_reason, hollow].compact.join('; ')}); " \
|
|
247
|
+
"falling back to the backfill"
|
|
174
248
|
else
|
|
175
|
-
|
|
249
|
+
warn "end-intent: outcome.md generated from graph.md and the ledger"
|
|
176
250
|
end
|
|
251
|
+
rescue StandardError => e
|
|
252
|
+
warn "end-intent: outcome generation crashed (#{e.message}); proceeding without it"
|
|
177
253
|
end
|
|
178
254
|
|
|
179
255
|
# Backfill (intent 308): write the judgment documents that are missing or still the
|
|
@@ -227,6 +303,48 @@ end
|
|
|
227
303
|
# closed exactly this way and the owner read a screen full of "not recorded".
|
|
228
304
|
# Abandoned closes and machine-backfilled outcomes (the backfill marker) are
|
|
229
305
|
# exempt: the reader cannot expect labels the writer never had.
|
|
306
|
+
#
|
|
307
|
+
# Intent 334 (G1, review A3; post-execution review, blocking 2): the legacy
|
|
308
|
+
# "S3" label grammar applies only to actions/*.md headings, and the node id
|
|
309
|
+
# grammar (kind prefix n/v/d/r plus digits, D1r) applies only to nodes/*.md
|
|
310
|
+
# headings - never both over both directories. This repo's own house words
|
|
311
|
+
# for plan review and post-execution review are "v1" and "v2"; widening the
|
|
312
|
+
# node-id alternative onto actions/ headings turned that prose into a
|
|
313
|
+
# manufactured label and refused legitimate legacy closes at exit 7.
|
|
314
|
+
ACTION_ID_LABEL_RE = /\AS\d+\z/.freeze
|
|
315
|
+
NODE_ID_LABEL_RE = /\A[nvdr][1-9]\d*\z/.freeze
|
|
316
|
+
|
|
317
|
+
# The id-shaped tokens found in `paths`' headings, matched against
|
|
318
|
+
# `id_label_re` only - the caller picks the grammar for the directory.
|
|
319
|
+
def heading_id_labels(paths, id_label_re)
|
|
320
|
+
paths.flat_map do |path|
|
|
321
|
+
ReportScreen.split_by_headings(File.read(path)).flat_map do |heading, _body|
|
|
322
|
+
heading.to_s.sub(/\A#+\s*/, "").split(/[^A-Za-z0-9]+/).select { |tok| tok.match?(id_label_re) }
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# 339 S9 (D18): a node id counts as a label in an actions/*.md heading too,
|
|
328
|
+
# but only when it stands alone as one of the heading's " - "-delimited
|
|
329
|
+
# segments - this repo's own convention, "### S1 - n1 - Title" (this action
|
|
330
|
+
# file's own headings, post-merge) or the bare "### n1 - Title". Never as a
|
|
331
|
+
# free word inside ordinary prose: the comment above ACTION_ID_LABEL_RE
|
|
332
|
+
# records exactly why intent 334 restricted the node-id grammar to
|
|
333
|
+
# nodes/*.md headings in the first place, and that reasoning does not change
|
|
334
|
+
# here - a word-scan widened onto actions/ headings would let this repo's own
|
|
335
|
+
# house words ("v1", "v2" for plan review and post-execution review)
|
|
336
|
+
# manufacture a label out of ordinary prose and refuse a legitimate legacy
|
|
337
|
+
# close. Segment position is what a word-scan cannot see: "v2" embedded
|
|
338
|
+
# mid-sentence is never its own " - "-delimited segment, but a heading built
|
|
339
|
+
# to this convention always puts the node id in one.
|
|
340
|
+
def heading_node_id_segments(paths, id_label_re)
|
|
341
|
+
paths.flat_map do |path|
|
|
342
|
+
ReportScreen.split_by_headings(File.read(path)).flat_map do |heading, _body|
|
|
343
|
+
heading.to_s.sub(/\A#+\s*/, "").split(/\s+-\s+/).map(&:strip).select { |seg| seg.match?(id_label_re) }
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
230
348
|
def hollow_report_reason(intent_dir, disposition)
|
|
231
349
|
return nil unless disposition == "delivered"
|
|
232
350
|
|
|
@@ -235,14 +353,34 @@ def hollow_report_reason(intent_dir, disposition)
|
|
|
235
353
|
text = File.read(outcome)
|
|
236
354
|
return nil if text.include?("<!-- backfilled from the record by end-intent on ")
|
|
237
355
|
|
|
238
|
-
# The gate's evidence bar (A7/B7): it judges only records that CLAIM
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
356
|
+
# The gate's evidence bar (A7/B7): it judges only records that CLAIM a
|
|
357
|
+
# labeled convention - at least one actions/*.md heading carrying an
|
|
358
|
+
# S-label or a node-id segment, or at least one nodes/*.md heading carrying
|
|
359
|
+
# a node id ("n1", "v2", "d3", "r4"). A terse legacy close with no labeled
|
|
360
|
+
# matrix stays report-and-proceed (intent 334, G1, D10r).
|
|
361
|
+
#
|
|
362
|
+
# Every matching token per heading is kept, not just the first (review A3):
|
|
363
|
+
# a legacy actions/ heading can carry BOTH an S-label and prose that reads
|
|
364
|
+
# like a node id ("### The v2 rewrite (S3)"). Restricting the word-scan
|
|
365
|
+
# node-id grammar to nodes/*.md headings (post-execution review, blocking
|
|
366
|
+
# 2) means "v2" in that heading is never even tested against it, so the
|
|
367
|
+
# heading's real "S3" label is never at risk.
|
|
368
|
+
#
|
|
369
|
+
# 339 S9 (D18): a graph-era action file headed by node id alone ("### n1 -
|
|
370
|
+
# Title", no S-label) used to yield an empty label set here and the gate
|
|
371
|
+
# returned before judging anything - every such record closed unjudged by
|
|
372
|
+
# silent exemption. `heading_node_id_segments` widens the actions/*.md side
|
|
373
|
+
# to see that shape without reopening the word-scan hole above: it matches
|
|
374
|
+
# a node id only when the heading's own " - "-delimited segments carry one,
|
|
375
|
+
# never a free word in prose. The node_paths call stays exactly as it was;
|
|
376
|
+
# nodes/*.md headings already resolve node ids correctly (test_node_id_
|
|
377
|
+
# labels_are_recognized) and their heading grammar is controlled, so a
|
|
378
|
+
# word-scan there carries none of the prose risk.
|
|
379
|
+
action_paths = Dir.glob(File.join(intent_dir, "actions", "*.md")).sort
|
|
380
|
+
node_paths = Dir.glob(File.join(intent_dir, "nodes", "*.md")).sort
|
|
381
|
+
heading_labels = (heading_id_labels(action_paths, ACTION_ID_LABEL_RE) +
|
|
382
|
+
heading_node_id_segments(action_paths, NODE_ID_LABEL_RE) +
|
|
383
|
+
heading_id_labels(node_paths, NODE_ID_LABEL_RE)).uniq
|
|
246
384
|
return nil if heading_labels.empty?
|
|
247
385
|
|
|
248
386
|
rows = ReportScreen.delivered_rows(intent_dir)
|
|
@@ -317,10 +455,10 @@ def section_stop(lines, heading_idx)
|
|
|
317
455
|
stop
|
|
318
456
|
end
|
|
319
457
|
|
|
320
|
-
# Shared matcher (
|
|
458
|
+
# Shared matcher (IndexEntry.match, intent 188 D12): accepts a real em
|
|
321
459
|
# dash OR a plain hyphen as the id/title separator on READ.
|
|
322
460
|
def active_line_id(line)
|
|
323
|
-
m =
|
|
461
|
+
m = IndexEntry.match(line)
|
|
324
462
|
m && m[1]
|
|
325
463
|
end
|
|
326
464
|
|
|
@@ -399,7 +537,7 @@ def move_index_to_terminal(content, id, disposition, today:, index_note: nil)
|
|
|
399
537
|
raise UnresolvedIndexEntry, "INDEX.md has no #{target_heading} heading; cannot complete the move"
|
|
400
538
|
end
|
|
401
539
|
|
|
402
|
-
m =
|
|
540
|
+
m = IndexEntry.match(lines[entry_idx])
|
|
403
541
|
title, link = m[2], m[3]
|
|
404
542
|
note_suffix = (index_note && !index_note.to_s.strip.empty?) ? " #{index_note.to_s.strip}" : ""
|
|
405
543
|
new_entry = "- [#{id} #{EM_DASH} #{title}](#{link}) #{EM_DASH} #{today}#{note_suffix}\n"
|
|
@@ -488,13 +626,13 @@ end
|
|
|
488
626
|
# exclusion with a raw backtrace. nil is the only safe "could not resolve"
|
|
489
627
|
# signal; main refuses explicitly on it rather than ever reaching takeover.
|
|
490
628
|
def resolve_end_session(explicit, intent_dir)
|
|
491
|
-
return explicit.to_s.strip unless
|
|
629
|
+
return explicit.to_s.strip unless blank?(explicit)
|
|
492
630
|
env = ENV["CLAUDE_CODE_SESSION_ID"]
|
|
493
|
-
return env.to_s.strip unless
|
|
631
|
+
return env.to_s.strip unless blank?(env)
|
|
494
632
|
lock = Lock.read(intent_dir)
|
|
495
633
|
return nil unless lock
|
|
496
634
|
owner = lock["owner_session"].to_s
|
|
497
|
-
|
|
635
|
+
blank?(owner) ? nil : owner
|
|
498
636
|
end
|
|
499
637
|
|
|
500
638
|
# Read-only pre-flight verdict (D4). Returns [:proceed, lock_or_nil],
|
|
@@ -542,37 +680,15 @@ end
|
|
|
542
680
|
def run_disarm(intent_dir, id, session, discard_worktree_changes:,
|
|
543
681
|
worktree_reader: ->(dir) { Arm.worktree_block(intent_dir: dir) },
|
|
544
682
|
disarm: ->(sess, dir) { Arm.disarm(intent_dir: dir, session: sess) })
|
|
545
|
-
return :ok if
|
|
683
|
+
return :ok if blank?(session)
|
|
546
684
|
|
|
547
685
|
block = worktree_reader.call(intent_dir)
|
|
548
686
|
worktree_code = block.is_a?(Hash) ? block["code"] : nil
|
|
549
687
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
out, err, status = nil, e.message, nil
|
|
555
|
-
end
|
|
556
|
-
|
|
557
|
-
# Fail CLOSED (intent 188, post-review hardening). A failed or impossible
|
|
558
|
-
# `git status` (corrupt per-worktree index, git missing from PATH, not a
|
|
559
|
-
# repo) means we cannot PROVE the worktree is clean, and the removal below
|
|
560
|
-
# force-removes on a plain `git worktree remove` failure, which destroys
|
|
561
|
-
# uncommitted work. Never force-remove an unproven worktree: the guard
|
|
562
|
-
# exists to protect uncommitted code, so an inconclusive check must refuse,
|
|
563
|
-
# not silently proceed as if it were clean.
|
|
564
|
-
if status.nil? || !status.success?
|
|
565
|
-
unless discard_worktree_changes
|
|
566
|
-
warn "end-intent: could not inspect the code worktree at #{worktree_code} " \
|
|
567
|
-
"(git status failed: #{err.to_s.strip}); refusing to remove it, because " \
|
|
568
|
-
"removal would force-discard any uncommitted work. Pass " \
|
|
569
|
-
"--discard-worktree-changes to remove it anyway, or repair the worktree."
|
|
570
|
-
return :dirty
|
|
571
|
-
end
|
|
572
|
-
elsif !out.strip.empty? && !discard_worktree_changes
|
|
573
|
-
warn "end-intent: code worktree #{worktree_code} has uncommitted changes; refusing " \
|
|
574
|
-
"to remove it. Pass --discard-worktree-changes to force the close deliberately, " \
|
|
575
|
-
"or commit/stash the changes and re-run."
|
|
688
|
+
unless discard_worktree_changes
|
|
689
|
+
refusal = worktree_refusal(worktree_code)
|
|
690
|
+
if refusal
|
|
691
|
+
warn refusal
|
|
576
692
|
return :dirty
|
|
577
693
|
end
|
|
578
694
|
end
|
|
@@ -640,6 +756,173 @@ def run_disarm(intent_dir, id, session, discard_worktree_changes:,
|
|
|
640
756
|
:lock_remains
|
|
641
757
|
end
|
|
642
758
|
|
|
759
|
+
# The disarm step's dirty-worktree guard, as a message (nil when the worktree
|
|
760
|
+
# is clean or absent). Shared by run_disarm and --dry-run so the preview
|
|
761
|
+
# refuses exactly what the close refuses.
|
|
762
|
+
#
|
|
763
|
+
# Fail CLOSED (intent 188, post-review hardening). A failed or impossible
|
|
764
|
+
# `git status` (corrupt per-worktree index, git missing from PATH, not a
|
|
765
|
+
# repo) means we cannot PROVE the worktree is clean, and the removal
|
|
766
|
+
# force-removes on a plain `git worktree remove` failure, which destroys
|
|
767
|
+
# uncommitted work. Never force-remove an unproven worktree: the guard
|
|
768
|
+
# exists to protect uncommitted code, so an inconclusive check must refuse,
|
|
769
|
+
# not silently proceed as if it were clean.
|
|
770
|
+
def worktree_refusal(worktree_code)
|
|
771
|
+
return nil unless worktree_code && Dir.exist?(worktree_code)
|
|
772
|
+
|
|
773
|
+
begin
|
|
774
|
+
out, err, status = Open3.capture3("git", "-C", worktree_code, "status", "--porcelain")
|
|
775
|
+
rescue StandardError => e
|
|
776
|
+
out, err, status = nil, e.message, nil
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
if status.nil? || !status.success?
|
|
780
|
+
"end-intent: could not inspect the code worktree at #{worktree_code} " \
|
|
781
|
+
"(git status failed: #{err.to_s.strip}); refusing to remove it, because " \
|
|
782
|
+
"removal would force-discard any uncommitted work. Pass " \
|
|
783
|
+
"--discard-worktree-changes to remove it anyway, or repair the worktree."
|
|
784
|
+
elsif !out.strip.empty?
|
|
785
|
+
"end-intent: code worktree #{worktree_code} has uncommitted changes; refusing " \
|
|
786
|
+
"to remove it. Pass --discard-worktree-changes to force the close deliberately, " \
|
|
787
|
+
"or commit/stash the changes and re-run."
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# True when the intent's code worktree carries any work: uncommitted changes
|
|
792
|
+
# or commits past its base branch. Anything that cannot be proven reads as
|
|
793
|
+
# changed, so an unreadable worktree never turns a close into a refusal.
|
|
794
|
+
def worktree_changed?(intent_dir)
|
|
795
|
+
block = Arm.worktree_block(intent_dir: intent_dir)
|
|
796
|
+
code = block.is_a?(Hash) ? block["code"] : nil
|
|
797
|
+
return false unless code && Dir.exist?(code)
|
|
798
|
+
|
|
799
|
+
out, status = Open3.capture2e("git", "-C", code, "status", "--porcelain")
|
|
800
|
+
return true unless status.success? && out.strip.empty?
|
|
801
|
+
|
|
802
|
+
base = ScaffoldIntent.detect_base_branch(code)
|
|
803
|
+
return true if base.nil?
|
|
804
|
+
|
|
805
|
+
ahead, status = Open3.capture2e("git", "-C", code, "rev-list", "--count", "#{base}..HEAD")
|
|
806
|
+
!status.success? || ahead.strip != "0"
|
|
807
|
+
rescue StandardError
|
|
808
|
+
true
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# A delivered close whose code is not on the repo checkout's current branch,
|
|
812
|
+
# as a refusal message (nil when it is, or for a store-only project).
|
|
813
|
+
# end-intent never merges: the owner merges or releases the work, then runs
|
|
814
|
+
# the close again. A real code worktree must prove that its HEAD commit is
|
|
815
|
+
# merged, so a detached or renamed branch is still checked. The code branch
|
|
816
|
+
# must be merged too, even after its worktree is gone. A git failure refuses.
|
|
817
|
+
# A directory that is not a working git worktree, with no code branch, is left
|
|
818
|
+
# to the dirty-worktree guard, which refuses what it cannot inspect.
|
|
819
|
+
def unmerged_refusal(intent_dir, runner: Worktree::ShellRunner.new)
|
|
820
|
+
paths = Arm.code_paths(intent_dir: intent_dir)
|
|
821
|
+
code = paths["code"]
|
|
822
|
+
return nil if code.nil?
|
|
823
|
+
|
|
824
|
+
repo = ExecWorktree.repo_from_worktree_code(code)
|
|
825
|
+
unless runner.run("-C", repo, "rev-parse", "--git-dir").success?
|
|
826
|
+
# No .git at all is a project without code; a .git that Git cannot read is not.
|
|
827
|
+
return nil unless File.exist?(File.join(repo, ".git"))
|
|
828
|
+
|
|
829
|
+
return "could not inspect the Git repository #{repo}, so the code cannot be shown to be merged. " \
|
|
830
|
+
"Check the repository, then run the close again"
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
unmerged_worktree_head(runner, code, repo, paths["code_branch"]) ||
|
|
834
|
+
unmerged_branch(runner, repo, paths["code_branch"])
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
# True when `code` is the top of its own git work tree, not a plain
|
|
838
|
+
# directory inside the repo or a broken checkout.
|
|
839
|
+
def real_worktree?(runner, code)
|
|
840
|
+
return false unless File.directory?(code)
|
|
841
|
+
|
|
842
|
+
top = runner.run("-C", code, "rev-parse", "--show-toplevel")
|
|
843
|
+
top.success? && File.realpath(top.stdout.strip) == File.realpath(code)
|
|
844
|
+
rescue SystemCallError
|
|
845
|
+
false
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
def unmerged_worktree_head(runner, code, repo, branch)
|
|
849
|
+
return nil unless real_worktree?(runner, code)
|
|
850
|
+
|
|
851
|
+
head = runner.run("-C", code, "rev-parse", "--verify", "--quiet", "HEAD^{commit}")
|
|
852
|
+
unless head.success?
|
|
853
|
+
return "could not read the HEAD commit of the code worktree #{code}, so its work cannot be " \
|
|
854
|
+
"shown to be merged. Commit it, merge it into the current branch of #{repo}, then run the close again"
|
|
855
|
+
end
|
|
856
|
+
sha = head.stdout.strip
|
|
857
|
+
on = runner.run("-C", code, "symbolic-ref", "--quiet", "--short", "HEAD")
|
|
858
|
+
name = on.success? ? on.stdout.strip : nil
|
|
859
|
+
ref = (name == branch) ? branch : sha[0, 12]
|
|
860
|
+
where = name ? "on branch #{name}" : "detached"
|
|
861
|
+
ancestry_refusal(runner, repo, sha, ref, "the code worktree #{code} (#{where} at #{sha[0, 12]})", branch)
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
def unmerged_branch(runner, repo, branch)
|
|
865
|
+
found = runner.run("-C", repo, "show-ref", "--verify", "--quiet", "refs/heads/#{branch}")
|
|
866
|
+
return nil if found.status == 1
|
|
867
|
+
unless found.success?
|
|
868
|
+
return "could not look up code branch #{branch} in #{repo}. Check the repository, then run the close again"
|
|
869
|
+
end
|
|
870
|
+
ancestry_refusal(runner, repo, branch, branch, "code branch #{branch}", branch)
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
# nil when `rev` is an ancestor of the branch the repo checkout is on, else a
|
|
874
|
+
# refusal naming `what` and the ordinary merge of `ref` that fixes it.
|
|
875
|
+
def ancestry_refusal(runner, repo, rev, ref, what, branch)
|
|
876
|
+
target = target_refusal(runner, repo, branch)
|
|
877
|
+
return target if target
|
|
878
|
+
|
|
879
|
+
check = runner.run("-C", repo, "merge-base", "--is-ancestor", rev, "HEAD")
|
|
880
|
+
return nil if check.success?
|
|
881
|
+
|
|
882
|
+
verdict = (check.status == 1) ? "is not merged into" : "could not be checked against"
|
|
883
|
+
"#{what} #{verdict} the current branch of #{repo}. " \
|
|
884
|
+
"Merge it there first (git -C #{repo} merge #{ref}), then run the close again"
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
# Integration means merged into a branch other than the code branch, so a
|
|
888
|
+
# detached checkout, or one on the code branch itself, proves nothing.
|
|
889
|
+
def target_refusal(runner, repo, branch)
|
|
890
|
+
on = runner.run("-C", repo, "symbolic-ref", "--quiet", "--short", "HEAD")
|
|
891
|
+
unless on.success?
|
|
892
|
+
return "the repo checkout #{repo} is not on a branch, so no branch holds the merged code. " \
|
|
893
|
+
"Check out the branch you release from, merge #{branch} into it, then run the close again"
|
|
894
|
+
end
|
|
895
|
+
return nil unless on.stdout.strip == branch
|
|
896
|
+
|
|
897
|
+
"the repo checkout #{repo} is on the code branch #{branch} itself, so the code is not merged anywhere. " \
|
|
898
|
+
"Check out the branch you release from, merge #{branch} into it, then run the close again"
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
# The hollow-report verdict the real close would reach, judged on a scratch
|
|
902
|
+
# copy of the intent after the same outcome generation and backfill, so the
|
|
903
|
+
# dry run writes nothing.
|
|
904
|
+
def predicted_hollow_reason(intent_dir, store:, id:, disposition:, summary:)
|
|
905
|
+
return nil unless disposition == "delivered"
|
|
906
|
+
|
|
907
|
+
Dir.mktmpdir("end-intent-dry-run") do |scratch|
|
|
908
|
+
copy = File.join(scratch, File.basename(intent_dir))
|
|
909
|
+
FileUtils.cp_r(intent_dir, copy)
|
|
910
|
+
quietly do
|
|
911
|
+
run_generate_outcome(copy, disposition: disposition)
|
|
912
|
+
run_backfill(copy, store: store, id: id, disposition: disposition, summary: summary)
|
|
913
|
+
end
|
|
914
|
+
hollow_report_reason(copy, disposition)
|
|
915
|
+
end
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
def quietly
|
|
919
|
+
saved = $stderr
|
|
920
|
+
$stderr = StringIO.new
|
|
921
|
+
yield
|
|
922
|
+
ensure
|
|
923
|
+
$stderr = saved
|
|
924
|
+
end
|
|
925
|
+
|
|
643
926
|
# --- main --------------------------------------------------------------------
|
|
644
927
|
|
|
645
928
|
def main(argv)
|
|
@@ -667,7 +950,7 @@ def main(argv)
|
|
|
667
950
|
# Lock.takeover (blank session reaches Lock.acquire's blank? guard AFTER the
|
|
668
951
|
# existing lock file was already deleted) or silently mis-arbitrate. Refuse
|
|
669
952
|
# rather than guess; nothing has been written yet.
|
|
670
|
-
if
|
|
953
|
+
if blank?(session) && File.exist?(Lock.path(intent_dir))
|
|
671
954
|
warn "end-intent: a delivery lock exists at #{Lock.path(intent_dir)} but no session " \
|
|
672
955
|
"identity could be resolved (--session, CLAUDE_CODE_SESSION_ID, and the lock's " \
|
|
673
956
|
"own recorded owner are all blank); refusing rather than guessing. Pass " \
|
|
@@ -692,7 +975,41 @@ def main(argv)
|
|
|
692
975
|
exit 4
|
|
693
976
|
end
|
|
694
977
|
|
|
978
|
+
# Nothing worked, nothing to deliver (acceptance N3). Before the dry run and
|
|
979
|
+
# before any write, so the preview and the close refuse together.
|
|
980
|
+
if disposition == "delivered"
|
|
981
|
+
untouched = UntouchedScaffold.reason(intent_dir, templates: BackfillIntent.load_templates,
|
|
982
|
+
worktree_changed: method(:worktree_changed?))
|
|
983
|
+
if untouched
|
|
984
|
+
warn "end-intent: refusing to deliver an untouched scaffold: #{untouched}"
|
|
985
|
+
warn "end-intent: do the work first, or close it with --disposition abandoned"
|
|
986
|
+
exit 8
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
unmerged = unmerged_refusal(intent_dir)
|
|
990
|
+
if unmerged
|
|
991
|
+
warn "end-intent: #{opts[:dry_run] ? "dry run: would refuse" : "refusing"} a delivered close: #{unmerged}"
|
|
992
|
+
warn "end-intent: nothing was merged, written, or released"
|
|
993
|
+
exit 9
|
|
994
|
+
end
|
|
995
|
+
end
|
|
996
|
+
|
|
695
997
|
if opts[:dry_run]
|
|
998
|
+
hollow = predicted_hollow_reason(intent_dir, store: store, id: id, disposition: disposition,
|
|
999
|
+
summary: opts[:outcome_summary])
|
|
1000
|
+
if hollow && !opts[:allow_hollow_report]
|
|
1001
|
+
warn "end-intent: dry run: would refuse a hollow delivered close: #{hollow}"
|
|
1002
|
+
exit 7
|
|
1003
|
+
end
|
|
1004
|
+
unless blank?(session) || opts[:discard_worktree_changes]
|
|
1005
|
+
block = Arm.worktree_block(intent_dir: intent_dir)
|
|
1006
|
+
refusal = worktree_refusal(block.is_a?(Hash) ? block["code"] : nil)
|
|
1007
|
+
if refusal
|
|
1008
|
+
warn "end-intent: dry run: would refuse at disarm (exit 5), after the store commit: " \
|
|
1009
|
+
"#{refusal.delete_prefix("end-intent: ")}"
|
|
1010
|
+
exit 5
|
|
1011
|
+
end
|
|
1012
|
+
end
|
|
696
1013
|
puts "end-intent: dry run for #{File.basename(intent_dir)} (#{disposition})"
|
|
697
1014
|
targets = BackfillIntent.targets_for(intent_dir)
|
|
698
1015
|
puts " would backfill: #{targets.empty? ? "(nothing)" : targets.join(", ")}"
|
|
@@ -718,6 +1035,11 @@ def main(argv)
|
|
|
718
1035
|
end
|
|
719
1036
|
end
|
|
720
1037
|
|
|
1038
|
+
# 0a. Generate outcome.md from graph.md and the ledger, when there is one
|
|
1039
|
+
# (intent 339, G6, spec D10) - before the backfill, so a real graph never
|
|
1040
|
+
# falls through to the terse backfill it exists to replace.
|
|
1041
|
+
run_generate_outcome(intent_dir, disposition: disposition)
|
|
1042
|
+
|
|
721
1043
|
# 1. Backfill from the record (intent 308), then the structure self-check and the
|
|
722
1044
|
# outcome guard, both report-only since 308.
|
|
723
1045
|
run_backfill(intent_dir, store: store, id: id, disposition: disposition,
|
package/scripts/exec-worktree
CHANGED
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
# (--session, else CLAUDE_CODE_SESSION_ID, else the existing delivery.lock's own
|
|
22
22
|
# recorded owner_session when non-blank, else nil; the lock-owner fallback IS
|
|
23
23
|
# used here, matching end-intent's teardown resolution order).
|
|
24
|
-
# Derive the worktree block from projects.yml and the intent id (Arm.worktree_block
|
|
25
|
-
#
|
|
24
|
+
# Derive the worktree block from projects.yml and the intent id (Arm.worktree_block).
|
|
25
|
+
# No code worktree on
|
|
26
26
|
# disk means nothing was provisioned (exit 0).
|
|
27
27
|
# 1. The dirty-worktree guard (delivered only): git status --porcelain on the code
|
|
28
28
|
# worktree. Not clean, or the check itself fails, fails CLOSED (exit 3), mirroring
|
|
29
29
|
# end-intent's dirty-worktree handling. abandoned skips this guard entirely: the
|
|
30
30
|
# branch survives and can be reclaimed, so a dirty worktree is not a reason to
|
|
31
31
|
# refuse.
|
|
32
|
-
# 2. Finish: Worktree.finish(
|
|
32
|
+
# 2. Finish: Worktree.finish(delivery, home:, runner:, merge: disposition ==
|
|
33
33
|
# "delivered"). Worktree.finish is fail-open and never raises, so failure is
|
|
34
34
|
# detected by checking, for a delivered disposition only, whether the worktree
|
|
35
35
|
# directory still exists on disk afterward (release removes it on success).
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
# one concern per script (spec D8).
|
|
39
39
|
#
|
|
40
40
|
# Exit codes:
|
|
41
|
-
# 0 finished (merged or removed per disposition), or nothing to finish (no
|
|
42
|
-
# resolved, or no code worktree recorded on
|
|
41
|
+
# 0 finished (merged or removed per disposition), or nothing to finish (no delivery
|
|
42
|
+
# resolved, or no code worktree recorded on it)
|
|
43
43
|
# 1 usage or path-resolution failure
|
|
44
44
|
# 2 retired in 2.0 (intent 302): was the order precondition; never emitted, never reused
|
|
45
45
|
# 3 the worktree is not clean, or the merge/removal did not complete (delivered only)
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# and on any filing error (reported on stderr; the next boot tries again), 2 on
|
|
15
15
|
# a usage error.
|
|
16
16
|
|
|
17
|
+
require_relative "lib/store_layout"
|
|
17
18
|
require_relative "lib/session_ledger"
|
|
18
19
|
require_relative "lib/session_backfill"
|
|
19
20
|
|
|
@@ -48,7 +49,7 @@ if opts[:carry_to] && !SessionLedger.valid_day_id?(opts[:carry_to])
|
|
|
48
49
|
usage_abort("--carry-to must be a real calendar date, YYYYMMDD")
|
|
49
50
|
end
|
|
50
51
|
|
|
51
|
-
store = opts[:store] ||
|
|
52
|
+
store = opts[:store] || Plastic::StoreLayout.global_store(File.expand_path(ENV.fetch("PLASTIC_HOME", "~/.plastic")))
|
|
52
53
|
templates = opts[:templates] || File.expand_path("../templates", __dir__)
|
|
53
54
|
|
|
54
55
|
begin
|