@zalom/plastic 2.0.0-alpha.8 → 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 +23 -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 +62 -1
- 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 +238 -22
- 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 +19 -5
- 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 +30 -16
- 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 +532 -71
- package/scripts/lib/intent_screen.rb +9 -6
- 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 +377 -173
- 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 +1460 -74
- 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 +555 -0
- 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 +192 -30
- 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 +15 -2
- 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 -295
- 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 -173
- 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
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
# rebuilds an empty ledger.
|
|
19
19
|
|
|
20
20
|
require_relative "lib/roadmap_savepoint"
|
|
21
|
+
require_relative "lib/guarded_append"
|
|
21
22
|
|
|
22
23
|
def opt(args, name)
|
|
23
24
|
(i = args.index(name)) && args[i + 1]
|
|
@@ -47,6 +48,12 @@ when "append"
|
|
|
47
48
|
rescue ArgumentError => e
|
|
48
49
|
warn "append: #{e.message}"
|
|
49
50
|
exit 2
|
|
51
|
+
rescue GuardedAppend::Unavailable => e
|
|
52
|
+
# Intent 335 (spec D14): the write guard could not be taken; nothing was written. Exit 4
|
|
53
|
+
# rather than let a backtrace reach the six skill recipes that drive this CLI - it rescues
|
|
54
|
+
# ArgumentError alone today and would otherwise print one straight to the terminal.
|
|
55
|
+
warn "append: #{e.message}"
|
|
56
|
+
exit 4
|
|
50
57
|
end
|
|
51
58
|
|
|
52
59
|
when "rebuild"
|
package/scripts/runner
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "stringio"
|
|
7
|
+
require_relative "lib/savepoint"
|
|
8
|
+
require_relative "lib/ready_set"
|
|
9
|
+
require_relative "lib/runner_core"
|
|
10
|
+
require_relative "lib/harness_adapter"
|
|
11
|
+
require_relative "lib/runner_proposals"
|
|
12
|
+
|
|
13
|
+
# runner - the one executable over the graph-ready loop's declared node graph
|
|
14
|
+
# (intent 340, G7, n1). A subcommand table: the public verbs (step, status,
|
|
15
|
+
# answer) are the only ones the skill body ever names; the internal verbs
|
|
16
|
+
# (ready, sweep, rewind, until-empty) route and work so `step` can compose
|
|
17
|
+
# them and so each can be tested alone, but stay out of the usage text (327
|
|
18
|
+
# spec, 340b n7).
|
|
19
|
+
#
|
|
20
|
+
# `step`, `sweep`, `answer`, `rewind` and `until-empty` route to modules
|
|
21
|
+
# later nodes deliver (RunnerDispatch and RunnerAbsorb for `step`;
|
|
22
|
+
# RunnerSweep, RunnerAnswer, RunnerRewind, RunnerUntilEmpty for the rest).
|
|
23
|
+
# Each is required LAZILY, inside its own verb branch: a verb whose module
|
|
24
|
+
# has not landed yet reports plainly and exits nonzero, and every other verb
|
|
25
|
+
# - most importantly `status`, which an operator polls constantly across all
|
|
26
|
+
# five of this intent's dispatches - keeps working.
|
|
27
|
+
#
|
|
28
|
+
# Usage:
|
|
29
|
+
# runner <step|status|answer> <intent_dir> [--node ID] [--answer TEXT]
|
|
30
|
+
#
|
|
31
|
+
# Exit codes (matching the family scripts/node-transition and
|
|
32
|
+
# scripts/ready-set already use):
|
|
33
|
+
# 0 - success
|
|
34
|
+
# 1 - the verb ran but reports failure (an invalid graph for `ready`)
|
|
35
|
+
# 2 - usage: unknown verb, or not an intent directory
|
|
36
|
+
# 3 - a routed verb's module has not been delivered yet
|
|
37
|
+
module Runner
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
PUBLIC_VERBS = %w[step status answer].freeze
|
|
41
|
+
INTERNAL_VERBS = %w[ready sweep rewind until-empty watch].freeze
|
|
42
|
+
VERBS = (PUBLIC_VERBS + INTERNAL_VERBS).freeze
|
|
43
|
+
|
|
44
|
+
# verb -> [[ModuleName, lib_file], ...], required lazily inside the verb's
|
|
45
|
+
# own branch (row 1.6). `step` composes two modules (dispatching new work,
|
|
46
|
+
# then absorbing what a sub-agent reported back); every other routed verb
|
|
47
|
+
# names exactly one.
|
|
48
|
+
LAZY_MODULES = {
|
|
49
|
+
"step" => [["RunnerSweep", "runner_sweep"], ["RunnerAbsorb", "runner_absorb"],
|
|
50
|
+
["RunnerDispatch", "runner_dispatch"]],
|
|
51
|
+
"sweep" => [["RunnerSweep", "runner_sweep"]],
|
|
52
|
+
"answer" => [["RunnerAnswer", "runner_answer"]],
|
|
53
|
+
"rewind" => [["RunnerRewind", "runner_rewind"]],
|
|
54
|
+
"until-empty" => [["RunnerUntilEmpty", "runner_until_empty"]],
|
|
55
|
+
"watch" => [["RunnerWatch", "runner_watch"]],
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
def usage
|
|
59
|
+
warn "Usage: runner <#{PUBLIC_VERBS.join('|')}> <intent_dir> [--node ID] [--answer TEXT]"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def intent_directory?(dir)
|
|
63
|
+
dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def opt(args, name)
|
|
67
|
+
(i = args.index(name)) && args[i + 1]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# opt_all(args, name) -> every value given for a repeated flag, in the order
|
|
71
|
+
# they appear, or [] when the flag never occurs (intent 340, G7, n8, row
|
|
72
|
+
# 8.1-8.3). `run_step` needs this, not `opt`, for `--return`: one step may
|
|
73
|
+
# carry more than one `--return NODE=PATH` pair (one per node the session
|
|
74
|
+
# is reporting back on), and `opt`'s single-value semantics would silently
|
|
75
|
+
# keep only the first, absorbing one node while stranding the other's
|
|
76
|
+
# lease until it expires.
|
|
77
|
+
def opt_all(args, name)
|
|
78
|
+
args.each_index.select { |i| args[i] == name }.map { |i| args[i + 1] }.compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# KNOWN_FLAGS (minor 2): the exact flag vocabulary each verb accepts. A
|
|
82
|
+
# flag not in a verb's own list is refused rather than silently ignored -
|
|
83
|
+
# a mistyped `--session` used to vanish with no effect and the runner
|
|
84
|
+
# simply acted as whichever session `CLAUDE_CODE_SESSION_ID` names.
|
|
85
|
+
KNOWN_FLAGS = {
|
|
86
|
+
"step" => %w[--return --allow-core-drift --harness],
|
|
87
|
+
"answer" => %w[--node --answer],
|
|
88
|
+
"rewind" => %w[--node --confirm],
|
|
89
|
+
"until-empty" => %w[--harness],
|
|
90
|
+
"watch" => %w[--dispatch --harness --install-timer --home],
|
|
91
|
+
}.freeze
|
|
92
|
+
|
|
93
|
+
# The explicit YAML document-end marker (intent 340b, G7c, n1, row 1.28)
|
|
94
|
+
# separating the dispatch plan `step` has always printed from the
|
|
95
|
+
# harness-rendered block underneath it: stdout has never been one YAML
|
|
96
|
+
# document (absorb/reclaim/extend/reap lines print before the plan,
|
|
97
|
+
# decision/park lines after it), so a reader splits on this marker rather
|
|
98
|
+
# than guessing where the plan ends.
|
|
99
|
+
DOCUMENT_BOUNDARY = "...".freeze
|
|
100
|
+
|
|
101
|
+
# unrecognized_flag(verb, args) -> the first token in `args` that looks
|
|
102
|
+
# like a flag (`--...`) and is not in `verb`'s own KNOWN_FLAGS, or nil. A
|
|
103
|
+
# verb absent from KNOWN_FLAGS (status, ready, sweep) accepts none.
|
|
104
|
+
def unrecognized_flag(verb, args)
|
|
105
|
+
allowed = KNOWN_FLAGS.fetch(verb, [])
|
|
106
|
+
args.find { |a| a.is_a?(String) && a.start_with?("--") && !allowed.include?(a) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def main(argv)
|
|
110
|
+
args = argv.dup
|
|
111
|
+
verb = args.shift
|
|
112
|
+
|
|
113
|
+
unless VERBS.include?(verb)
|
|
114
|
+
warn "runner: unknown verb #{verb.inspect}"
|
|
115
|
+
usage
|
|
116
|
+
exit 2
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
intent_dir_arg = args.shift
|
|
120
|
+
unless intent_directory?(intent_dir_arg && File.expand_path(intent_dir_arg))
|
|
121
|
+
warn "runner: #{intent_dir_arg.inspect} is not an intent directory"
|
|
122
|
+
usage
|
|
123
|
+
exit 2
|
|
124
|
+
end
|
|
125
|
+
intent_dir = File.expand_path(intent_dir_arg)
|
|
126
|
+
|
|
127
|
+
bad_flag = unrecognized_flag(verb, args)
|
|
128
|
+
if bad_flag
|
|
129
|
+
warn "runner: unknown flag #{bad_flag.inspect} for #{verb}"
|
|
130
|
+
usage
|
|
131
|
+
exit 2
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
case verb
|
|
135
|
+
when "status"
|
|
136
|
+
run_status(intent_dir)
|
|
137
|
+
when "ready"
|
|
138
|
+
run_ready(intent_dir)
|
|
139
|
+
else
|
|
140
|
+
run_lazy(verb, intent_dir, args)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def run_status(intent_dir)
|
|
145
|
+
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
146
|
+
rows = RunnerCore.status(context)
|
|
147
|
+
|
|
148
|
+
if rows.empty?
|
|
149
|
+
puts "No declared nodes."
|
|
150
|
+
else
|
|
151
|
+
rows.each do |id, view|
|
|
152
|
+
puts "#{id} kind=#{view[:kind]} state=#{view[:state]} ready=#{view[:ready]}"
|
|
153
|
+
last = view[:last_transition]
|
|
154
|
+
puts " last: #{last[:raw]}" if last
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
graph = context.graph || {}
|
|
159
|
+
fixes = RunnerProposals.review_fix_count(graph[:edges] || {}, graph[:nodes] || {})
|
|
160
|
+
puts "review fixes: #{fixes} of #{RunnerProposals::REVIEW_FIX_CAP}"
|
|
161
|
+
|
|
162
|
+
complete = RunnerCore.complete?(context)
|
|
163
|
+
puts
|
|
164
|
+
puts complete ? "complete" : "stalled"
|
|
165
|
+
unless complete
|
|
166
|
+
rows.each do |id, view|
|
|
167
|
+
next if ReadySet::TERMINAL_STATES.include?(view[:state])
|
|
168
|
+
|
|
169
|
+
view[:blockers].each { |b| puts "#{id} blocked: #{b}" }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
exit 0
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def run_ready(intent_dir)
|
|
177
|
+
analysis = ReadySet.analyze(intent_dir)
|
|
178
|
+
unless analysis[:ok]
|
|
179
|
+
warn "runner: #{analysis[:errors].join('; ')}"
|
|
180
|
+
exit 1
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if analysis[:ranked_ready].empty?
|
|
184
|
+
puts "Ready: (none)"
|
|
185
|
+
else
|
|
186
|
+
puts "Ready: #{analysis[:ranked_ready].map { |r| r[:id] }.join(', ')}"
|
|
187
|
+
end
|
|
188
|
+
exit 0
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Requires this verb's module(s) lazily, then hands off. Nothing in n1
|
|
192
|
+
# delivers runner_dispatch.rb, runner_absorb.rb, runner_sweep.rb,
|
|
193
|
+
# runner_answer.rb, or runner_rewind.rb, so every LAZY_MODULES require
|
|
194
|
+
# raises LoadError today and every routed verb reports "not yet
|
|
195
|
+
# delivered" - the only thing this node needs to prove (row 1.6). Once a
|
|
196
|
+
# later node ships its file, the require succeeds and falls through to
|
|
197
|
+
# dispatch_lazy, which that node replaces with the real call.
|
|
198
|
+
def run_lazy(verb, intent_dir, args)
|
|
199
|
+
# The require target below is a runtime value, never an inline quoted
|
|
200
|
+
# literal, so it falls outside test/install_packaging_test.rb's
|
|
201
|
+
# require-closure guard on purpose: every file LAZY_MODULES names is a
|
|
202
|
+
# genuine later-node deliverable, none of which core_files should track
|
|
203
|
+
# until that node actually lands it.
|
|
204
|
+
LAZY_MODULES.fetch(verb, []).each { |(_name, file)| require_relative(File.join("lib", file)) }
|
|
205
|
+
rescue LoadError
|
|
206
|
+
warn "runner: #{verb} is not yet delivered (its module has not landed)"
|
|
207
|
+
exit 3
|
|
208
|
+
else
|
|
209
|
+
dispatch_lazy(verb, intent_dir, args)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def dispatch_lazy(verb, intent_dir, args)
|
|
213
|
+
case verb
|
|
214
|
+
when "step"
|
|
215
|
+
run_step(intent_dir, args)
|
|
216
|
+
when "sweep"
|
|
217
|
+
run_sweep(intent_dir, args)
|
|
218
|
+
when "answer"
|
|
219
|
+
run_answer(intent_dir, args)
|
|
220
|
+
when "rewind"
|
|
221
|
+
run_rewind(intent_dir, args)
|
|
222
|
+
when "until-empty"
|
|
223
|
+
run_until_empty(intent_dir, args)
|
|
224
|
+
when "watch"
|
|
225
|
+
run_watch(intent_dir, args)
|
|
226
|
+
else
|
|
227
|
+
warn "runner: #{verb}'s module loaded but no dispatcher is wired up yet"
|
|
228
|
+
exit 3
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# run_sweep (intent 340, G7, n8): the standalone `sweep` verb, unreachable
|
|
233
|
+
# from the CLI until this node - `dispatch_lazy` had no `when "sweep"` arm
|
|
234
|
+
# even though `RunnerSweep.run` (n2) is a complete, documented standalone
|
|
235
|
+
# entry point, exactly the shape a bare `runner sweep` call needs (abort
|
|
236
|
+
# check, THEN the lease heartbeat, THEN reclaim - RunnerSweep's own fixed
|
|
237
|
+
# order). `step` never calls this method: it composes `abort_if_merging`
|
|
238
|
+
# and `reclaim` itself, interleaved around absorb.
|
|
239
|
+
def run_sweep(intent_dir, args)
|
|
240
|
+
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
241
|
+
result = RunnerSweep.run(context)
|
|
242
|
+
|
|
243
|
+
unless result[:ok]
|
|
244
|
+
warn "runner: #{result[:error]}"
|
|
245
|
+
warn "runner: run `#{result[:recovery_command]}` before the next sweep can run" if result[:recovery_command]
|
|
246
|
+
exit 1
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
result[:reclaimed].each { |r| puts "reclaimed #{r[:node]} (holder=#{r[:holder]})" }
|
|
250
|
+
result[:extended].each { |e| puts "extended #{e[:node]} (head=#{e[:head]})" }
|
|
251
|
+
puts "nothing to sweep" if result[:reclaimed].empty? && result[:extended].empty?
|
|
252
|
+
exit 0
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# run_answer (intent 340, G7, n6): closes a decision node or unparks a
|
|
256
|
+
# work node the runner parked at `needs_decision` (327 D22, C26), then
|
|
257
|
+
# prints what became ready so the owner knows the loop can continue.
|
|
258
|
+
def run_answer(intent_dir, args)
|
|
259
|
+
node = opt(args, "--node")
|
|
260
|
+
text = opt(args, "--answer")
|
|
261
|
+
|
|
262
|
+
unless node
|
|
263
|
+
warn "runner: answer requires --node ID"
|
|
264
|
+
exit 2
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
268
|
+
result = RunnerAnswer.answer(context, node: node, text: text.to_s)
|
|
269
|
+
|
|
270
|
+
unless result[:ok]
|
|
271
|
+
warn "runner: answer refused (#{result[:reason]})"
|
|
272
|
+
exit 1
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
puts "answered #{node}"
|
|
276
|
+
puts "respun to #{result[:respun_to]}" if result[:respun_to]
|
|
277
|
+
puts "newly ready: #{result[:newly_ready].join(', ')}" if result[:newly_ready].any?
|
|
278
|
+
exit 0
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# run_rewind (intent 340, G7, n6): confirm-gated reset of the intent
|
|
282
|
+
# branch to a node's own recorded commit, superseding every downstream
|
|
283
|
+
# node and respinning the rewound node itself (327 D15's rewind clause).
|
|
284
|
+
def run_rewind(intent_dir, args)
|
|
285
|
+
node = opt(args, "--node")
|
|
286
|
+
confirm = args.include?("--confirm")
|
|
287
|
+
|
|
288
|
+
unless node
|
|
289
|
+
warn "runner: rewind requires --node ID"
|
|
290
|
+
exit 2
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
294
|
+
result = RunnerRewind.rewind(context, node: node, confirm: confirm)
|
|
295
|
+
|
|
296
|
+
unless result[:ok]
|
|
297
|
+
warn "runner: rewind refused (#{result[:reason]})"
|
|
298
|
+
exit 1
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
puts "reset #{node} to #{result[:reset_to]}"
|
|
302
|
+
puts "superseded: #{result[:superseded].join(', ')}" if result[:superseded].any?
|
|
303
|
+
puts "respun to #{result[:respun_to]}"
|
|
304
|
+
exit 0
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# run_until_empty (intent 340b, G7c, n7): the Codex loop. Internal, like
|
|
308
|
+
# `rewind` (row 7.13) - `RunnerUntilEmpty.run` composes `step` and
|
|
309
|
+
# `node-run` itself, since Codex has no session on the other end to make
|
|
310
|
+
# the subagent calls `step` only ever prints a plan for. `--harness`
|
|
311
|
+
# threads into every turn the loop runs (row 7.5), never re-resolved per
|
|
312
|
+
# iteration from a literal. The lock check here is the same defence in
|
|
313
|
+
# depth `run_step_body` carries (B4): `RunnerUntilEmpty.step_once` refuses
|
|
314
|
+
# `lock_not_held` on its own first call regardless, so this exits before
|
|
315
|
+
# even constructing the loop's own state.
|
|
316
|
+
def run_until_empty(intent_dir, args)
|
|
317
|
+
harness_override = opt(args, "--harness")
|
|
318
|
+
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
319
|
+
|
|
320
|
+
unless context.session
|
|
321
|
+
warn "runner: until-empty refused (lock_not_held)"
|
|
322
|
+
exit 1
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
result = RunnerUntilEmpty.run(context, harness: harness_override)
|
|
326
|
+
|
|
327
|
+
case result[:status]
|
|
328
|
+
when "complete", "stalled", "needs_decision", "iteration_cap"
|
|
329
|
+
exit 0
|
|
330
|
+
else
|
|
331
|
+
exit 1
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# run_watch (intent 340a, G7b, n2): one tick over disk truth. Internal,
|
|
336
|
+
# like until-empty and rewind - the timer that carries it lives outside
|
|
337
|
+
# this process (a local `/loop` on Claude Code, a launchd job on Codex,
|
|
338
|
+
# n3/n4). `--dispatch` is unattended start (graph.md D7): refused here,
|
|
339
|
+
# before RunnerWatch.tick ever takes the tick lock or writes anything,
|
|
340
|
+
# when the resolved harness's `HarnessAdapter.unattended_start?` is
|
|
341
|
+
# false, so a refusal never leaves a `watch.record` line behind that
|
|
342
|
+
# looks like a completed tick.
|
|
343
|
+
# --install-timer (intent 340a, G7b, n3, graph.md D9): the Codex carrier.
|
|
344
|
+
# Writes the plist and prints the path plus the `launchctl load` command;
|
|
345
|
+
# never runs `launchctl` itself, so the owner stays in control of when the
|
|
346
|
+
# job actually loads. `--home` defaults to the OS home, as
|
|
347
|
+
# `scripts/meter-watch --install-timer` already does.
|
|
348
|
+
def run_watch(intent_dir, args)
|
|
349
|
+
dispatch = args.include?("--dispatch")
|
|
350
|
+
harness_override = opt(args, "--harness")
|
|
351
|
+
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
352
|
+
agent_config = load_agent_config(context.plastic_home)
|
|
353
|
+
harness = HarnessAdapter.resolve_key(config: agent_config, override: harness_override)
|
|
354
|
+
|
|
355
|
+
if args.include?("--install-timer")
|
|
356
|
+
home = opt(args, "--home") || Dir.home
|
|
357
|
+
plist_path = RunnerWatch.install_timer(context, home: home, harness_key: harness)
|
|
358
|
+
puts "plist: #{plist_path}"
|
|
359
|
+
puts "launchctl load #{plist_path}"
|
|
360
|
+
exit 0
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
if dispatch && !HarnessAdapter.unattended_start?(harness)
|
|
364
|
+
warn HarnessAdapter::UNATTENDED_START_SENTENCE
|
|
365
|
+
exit 1
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
result = RunnerWatch.tick(context, dispatch: dispatch, harness: harness)
|
|
369
|
+
|
|
370
|
+
puts "class: #{result[:class]}"
|
|
371
|
+
result[:blockers].each { |b| puts "blocked: #{b}" }
|
|
372
|
+
puts "ready: #{result[:ready].empty? ? '(none)' : result[:ready].join(', ')}"
|
|
373
|
+
puts "dispatched: #{result[:dispatched].empty? ? '(none)' : result[:dispatched].join(', ')}"
|
|
374
|
+
exit 0
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# run_step (intent 340, G7, n5): one turn of the loop. The real work lives
|
|
378
|
+
# in #run_step_body below; this wrapper (intent 340b, G7c, n1, rows
|
|
379
|
+
# 1.21-1.25 and 1.29-1.31) exists ONLY to capture every byte `step` prints
|
|
380
|
+
# - stdout and stderr both, refusals included - and persist it to
|
|
381
|
+
# `<intent_dir>/runner-step.last` before the process actually exits, so a
|
|
382
|
+
# PreCompact hand-off (n3) always has something to render, even after one
|
|
383
|
+
# of the three pre-plan refusals that used to warn to stderr alone.
|
|
384
|
+
#
|
|
385
|
+
# `run_step_body` still calls Kernel#exit(N) at every one of its own exit
|
|
386
|
+
# points, exactly as before this node; wrapping it in a rescue for
|
|
387
|
+
# SystemExit lets this method capture the intended status, persist the
|
|
388
|
+
# file, replay the captured output onto the REAL stdout/stderr (so a
|
|
389
|
+
# subprocess caller like Open3.capture3 sees byte-identical output to
|
|
390
|
+
# before this node), and only then perform the one real process exit.
|
|
391
|
+
def run_step(intent_dir, args)
|
|
392
|
+
out_buf = StringIO.new
|
|
393
|
+
err_buf = StringIO.new
|
|
394
|
+
real_stdout = $stdout
|
|
395
|
+
real_stderr = $stderr
|
|
396
|
+
exit_code = 0
|
|
397
|
+
begin
|
|
398
|
+
$stdout = out_buf
|
|
399
|
+
$stderr = err_buf
|
|
400
|
+
run_step_body(intent_dir, args)
|
|
401
|
+
rescue SystemExit => e
|
|
402
|
+
exit_code = e.status || 1
|
|
403
|
+
ensure
|
|
404
|
+
$stdout = real_stdout
|
|
405
|
+
$stderr = real_stderr
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
real_stdout.print(out_buf.string)
|
|
409
|
+
real_stderr.print(err_buf.string)
|
|
410
|
+
persist_runner_step_last(intent_dir, out_buf.string, err_buf.string)
|
|
411
|
+
|
|
412
|
+
exit(exit_code)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Row 1.24: lives beside savepoint.md, never inside attempts/ (which the
|
|
416
|
+
# reaper treats as attempt evidence). Row 1.32: registers itself in the
|
|
417
|
+
# store's own top-level `.gitignore` (Worktree.ensure_gitignored's own
|
|
418
|
+
# idempotent, non-raising append) the same way `*.lock` already is, so a
|
|
419
|
+
# file rewritten on every step never dirties the store tree. Row 1.25:
|
|
420
|
+
# wrapped in its own rescue - an unwritable intent directory must never
|
|
421
|
+
# crash a step that has already written its transitions.
|
|
422
|
+
def persist_runner_step_last(intent_dir, stdout_text, stderr_text)
|
|
423
|
+
home_root = Arm.home_for(intent_dir)
|
|
424
|
+
Worktree.ensure_gitignored(File.join(home_root, ".plastic"), "runner-step.last")
|
|
425
|
+
|
|
426
|
+
path = File.join(intent_dir, "runner-step.last")
|
|
427
|
+
File.write(path, "#{stdout_text}#{stderr_text}")
|
|
428
|
+
rescue StandardError => e
|
|
429
|
+
warn "runner: could not persist runner-step.last: #{e.message}"
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Intent 340b, G7c, n1, row 1.1: the config `HarnessAdapter.resolve_key`
|
|
433
|
+
# reads `agent.type` from. `context.plastic_home` is derived from the
|
|
434
|
+
# intent_dir's OWN ancestry (Arm.home_for -> Worktree.home_from_store), so
|
|
435
|
+
# a sandboxed test fixture never reaches the real ~/.plastic/config.yml
|
|
436
|
+
# (Arm.home_for's own docstring: "a sandboxed store never resolves to the
|
|
437
|
+
# real Dir.home") - this stays hermetic for free, the same way every other
|
|
438
|
+
# `context.plastic_home` read in this file already does. A missing or
|
|
439
|
+
# unparseable config.yml reads as {}, never raises.
|
|
440
|
+
def load_agent_config(plastic_home)
|
|
441
|
+
path = File.join(plastic_home.to_s, "config.yml")
|
|
442
|
+
return {} unless File.exist?(path)
|
|
443
|
+
|
|
444
|
+
YAML.safe_load(File.read(path)) || {}
|
|
445
|
+
rescue StandardError
|
|
446
|
+
{}
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# run_step_body (intent 340, G7, n5): one turn of the loop, in the fixed
|
|
450
|
+
# order 327 D14 and RunnerSweep's own docstring name - abort on a stuck
|
|
451
|
+
# merge, the delivery-lease heartbeat, absorb every `--return NODE=PATH`
|
|
452
|
+
# this call carries, THEN reclaim (so a node this very call just absorbed
|
|
453
|
+
# is never reclaimed out from under its own return, matrix row 2.19/2.20) -
|
|
454
|
+
# and only then dispatch whatever is left ready. Never spawns an agent (327
|
|
455
|
+
# D42): the session makes every subagent call from the plan this prints.
|
|
456
|
+
def run_step_body(intent_dir, args)
|
|
457
|
+
context = RunnerCore.context(intent_dir: intent_dir, env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
458
|
+
|
|
459
|
+
# v1 minor 3/row 11.18: RunnerSweep.abort_if_merging already prints the
|
|
460
|
+
# whole sentence itself (its own error PLUS the recovery command) - this
|
|
461
|
+
# used to print both halves again, so the operator read the same merge
|
|
462
|
+
# warning twice for one refusal.
|
|
463
|
+
abort_result = RunnerSweep.abort_if_merging(context)
|
|
464
|
+
exit 1 unless abort_result[:ok]
|
|
465
|
+
|
|
466
|
+
Lock.heartbeat(intent_dir, session: context.session) unless context.session.to_s.strip.empty?
|
|
467
|
+
|
|
468
|
+
# B4: this is the runner's ACTUAL lock check now - the absorb loop below
|
|
469
|
+
# writes node transitions, and RunnerDispatch's own check (row 5.31/5.32)
|
|
470
|
+
# runs too late to stop that: a session holding no lock would otherwise
|
|
471
|
+
# have every `--return` it carries written anyway, attributed to
|
|
472
|
+
# whichever session's holder= happens to be on the node's `running`
|
|
473
|
+
# line. RunnerDispatch keeps its own check too, as defence in depth for
|
|
474
|
+
# a caller that reaches it some other way.
|
|
475
|
+
unless context.session
|
|
476
|
+
warn "runner: step refused (lock_not_held)"
|
|
477
|
+
warn RunnerDispatch.rearm_command(intent_dir)
|
|
478
|
+
exit 1
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# M3/row 10.1: `--allow-core-drift` (D11) - the one escape hatch letting
|
|
482
|
+
# Plastic's own repository, which legitimately drifts from the installed
|
|
483
|
+
# manifest, dogfood its own runner. Threaded straight into the absorb,
|
|
484
|
+
# never defaulted true.
|
|
485
|
+
allow_core_drift = args.include?("--allow-core-drift")
|
|
486
|
+
|
|
487
|
+
# Intent 340b, G7c, n1, row 1.2/1.4: `--harness KEY` overrides config
|
|
488
|
+
# `agent.type` for this one call - how one machine drives both adapters
|
|
489
|
+
# without editing config. `nil` when absent, so HarnessAdapter falls
|
|
490
|
+
# through to config's own value untouched.
|
|
491
|
+
harness_override = opt(args, "--harness")
|
|
492
|
+
|
|
493
|
+
returns = {}
|
|
494
|
+
opt_all(args, "--return").each do |pair|
|
|
495
|
+
node, path = pair.to_s.split("=", 2)
|
|
496
|
+
if node.to_s.empty? || path.to_s.empty?
|
|
497
|
+
warn "runner: malformed --return #{pair.inspect}, expected NODE=PATH"
|
|
498
|
+
exit 2
|
|
499
|
+
end
|
|
500
|
+
returns[node] = path
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
returns.each do |node, path|
|
|
504
|
+
absorbed = RunnerAbsorb.absorb(context, node: node, return_path: path, allow_core_drift: allow_core_drift)
|
|
505
|
+
puts "absorbed #{node}: #{absorbed[:state]}"
|
|
506
|
+
# M4/row 10.3: a refused proposal is surfaced here, not left to a
|
|
507
|
+
# savepoint comment the operator never sees - the exact reason the
|
|
508
|
+
# proposer's next attempt otherwise proposes the same thing again.
|
|
509
|
+
proposal = absorbed[:proposal]
|
|
510
|
+
puts " proposal refused: #{Array(proposal[:errors]).join('; ')}" if proposal && !proposal[:ok]
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
swept = RunnerSweep.reclaim(context, skip: returns.keys)
|
|
514
|
+
swept[:reclaimed].each { |r| puts "reclaimed #{r[:node]} (holder=#{r[:holder]})" }
|
|
515
|
+
swept[:extended].each { |e| puts "extended #{e[:node]} (head=#{e[:head]})" }
|
|
516
|
+
|
|
517
|
+
# M5/row 10.4: NodeWorktree.reap runs once per step, right after the
|
|
518
|
+
# reclaim pass - D26's reaper, unreachable from anywhere until now.
|
|
519
|
+
reaped = NodeWorktree.reap(context)
|
|
520
|
+
reaped[:removed].each { |r| puts "reaped #{r[:node]} worktree" }
|
|
521
|
+
|
|
522
|
+
agent_config = load_agent_config(context.plastic_home)
|
|
523
|
+
result = RunnerDispatch.dispatch(context, config: agent_config, harness: harness_override)
|
|
524
|
+
unless result[:ok]
|
|
525
|
+
warn "runner: step refused (#{result[:reason]}): #{Array(result[:errors]).join('; ')}"
|
|
526
|
+
warn result[:rearm_command] if result[:rearm_command]
|
|
527
|
+
exit 1
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
case result[:status]
|
|
531
|
+
when "dispatched"
|
|
532
|
+
puts result[:plan]
|
|
533
|
+
when "queued"
|
|
534
|
+
puts "queued (concurrency ceiling reached)"
|
|
535
|
+
when "complete"
|
|
536
|
+
puts "complete"
|
|
537
|
+
when "needs_decision"
|
|
538
|
+
# printed below, alongside a dispatch plan when the same step also
|
|
539
|
+
# dispatched (M11/row 10.14) - `result[:stop]` is set whenever
|
|
540
|
+
# status is needs_decision.
|
|
541
|
+
else
|
|
542
|
+
puts "stalled"
|
|
543
|
+
result[:blockers].each { |b| puts "blocked: #{b}" }
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Intent 340b, G7c, n1, rows 1.6/1.13/1.14/1.27/1.28: the YAML plan keeps
|
|
547
|
+
# printing exactly as it did before this node (327's contract) - this is
|
|
548
|
+
# the rendered block UNDERNEATH it, for the harness `result[:harness]`
|
|
549
|
+
# resolved (config's `agent.type`, or this call's own `--harness`
|
|
550
|
+
# override), separated by an explicit YAML document-end marker so a
|
|
551
|
+
# reader splits on that marker rather than a heuristic. `render` returns
|
|
552
|
+
# nil for an empty dispatch list (row 1.14), so nothing extra ever
|
|
553
|
+
# prints when this step dispatched nothing.
|
|
554
|
+
block = HarnessAdapter.render(result[:dispatched], harness: result[:harness],
|
|
555
|
+
return_contract: RunnerDispatch::RETURN_CONTRACT)
|
|
556
|
+
if block
|
|
557
|
+
puts DOCUMENT_BOUNDARY
|
|
558
|
+
puts block
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# M11/row 10.14: a `needs_decision` stop must print even when THIS SAME
|
|
562
|
+
# step also dispatched other nodes ahead of the decision node in ranked
|
|
563
|
+
# order - `result[:status]` reads "dispatched" in that case (dispatched
|
|
564
|
+
# takes priority in RunnerDispatch's own report), which used to swallow
|
|
565
|
+
# the stop and its `runner answer` command entirely.
|
|
566
|
+
if result[:stop]
|
|
567
|
+
stop = result[:stop]
|
|
568
|
+
puts "needs_decision: #{stop[:node]} - #{stop[:question]}"
|
|
569
|
+
puts stop[:answer_command]
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
result[:parked].each do |p|
|
|
573
|
+
puts "parked #{p[:node]} (#{p[:reason]}): #{p[:question]}"
|
|
574
|
+
puts p[:answer_command]
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
exit 0
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
Runner.main(ARGV) if $PROGRAM_NAME == __FILE__
|
package/scripts/savepoint-note
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
-
# savepoint-note - the writer CLI for
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# ledger; the executor's own commits are plain git
|
|
10
|
-
# thin wrapper on
|
|
5
|
+
# savepoint-note - the writer CLI for three savepoint kinds: Review (one line per
|
|
6
|
+
# plan-review or post-execution-review verdict) and Commit (one line per commit landing
|
|
7
|
+
# during Exec), both from intent 317, D17; and Report (one line per report screen printed,
|
|
8
|
+
# intent 331f). None has an automatic seam (session-commit writes the day ledger, not the
|
|
9
|
+
# intent ledger; the executor's own commits are plain git; a printed screen is a skill's own
|
|
10
|
+
# reply, not a file write), so this is an explicit thin wrapper on
|
|
11
|
+
# Savepoint.append_review_savepoint / append_commit_savepoint / append_report_savepoint.
|
|
11
12
|
#
|
|
12
13
|
# Usage:
|
|
13
|
-
# savepoint-note <intent_dir> --kind Review|Commit --text "<text>"
|
|
14
|
+
# savepoint-note <intent_dir> --kind Review|Commit|Report --text "<text>"
|
|
14
15
|
#
|
|
15
16
|
# --text is normalized (D21): runs of two or more spaces collapse to one, so a
|
|
16
17
|
# free-text kind that happens to contain a double space cannot key the dedup
|
|
@@ -46,12 +47,12 @@ while (arg = args.shift)
|
|
|
46
47
|
end
|
|
47
48
|
end
|
|
48
49
|
|
|
49
|
-
usage_abort("usage: savepoint-note <intent_dir> --kind Review|Commit --text \"<text>\"") unless positional.length == 1
|
|
50
|
+
usage_abort("usage: savepoint-note <intent_dir> --kind Review|Commit|Report --text \"<text>\"") unless positional.length == 1
|
|
50
51
|
|
|
51
52
|
intent_dir = File.expand_path(positional.first)
|
|
52
53
|
usage_abort("#{intent_dir} is not an intent directory") unless File.exist?(Savepoint.intent_file(intent_dir))
|
|
53
54
|
|
|
54
|
-
KINDS = %w[Review Commit].freeze
|
|
55
|
+
KINDS = %w[Review Commit Report].freeze
|
|
55
56
|
usage_abort("--kind must be one of #{KINDS.join(', ')}, got #{kind.inspect}") unless KINDS.include?(kind)
|
|
56
57
|
usage_abort("--text is required") if text.nil? || text.empty?
|
|
57
58
|
usage_abort("--text must not contain a newline") if text.include?("\n")
|
|
@@ -62,6 +63,7 @@ usage_abort("--text is empty after normalization") if normalized.empty?
|
|
|
62
63
|
case kind
|
|
63
64
|
when "Review" then Savepoint.append_review_savepoint(intent_dir, normalized)
|
|
64
65
|
when "Commit" then Savepoint.append_commit_savepoint(intent_dir, normalized)
|
|
66
|
+
when "Report" then Savepoint.append_report_savepoint(intent_dir, normalized)
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
exit 0
|
package/scripts/session-commit
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
# 0 - always, except a usage error
|
|
19
19
|
# 2 - usage error (missing --cwd or --summary); nothing is written
|
|
20
20
|
|
|
21
|
+
require_relative "lib/store_layout"
|
|
21
22
|
require "fileutils"
|
|
22
23
|
require_relative "lib/session_git"
|
|
23
24
|
require_relative "lib/session_ledger"
|
|
@@ -78,7 +79,7 @@ def main(argv)
|
|
|
78
79
|
usage_abort("--summary is required") if opts[:summary].nil?
|
|
79
80
|
|
|
80
81
|
plastic_home = expand(opts[:plastic_home] || ENV.fetch("PLASTIC_HOME", "~/.plastic"))
|
|
81
|
-
store = opts[:store] ? expand(opts[:store]) :
|
|
82
|
+
store = opts[:store] ? expand(opts[:store]) : Plastic::StoreLayout.global_store(plastic_home)
|
|
82
83
|
cwd = expand(opts[:cwd])
|
|
83
84
|
|
|
84
85
|
day = opts[:day] || SessionLedger.day_id
|