@zalom/plastic 2.0.0-alpha.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLASTIC.md +13 -136
- package/README.md +357 -133
- package/agents/plastic-enforcer.md +20 -15
- package/agents/plastic-executor.md +15 -4
- package/agents/plastic-node-research.md +30 -0
- package/agents/plastic-node-verify.md +28 -0
- package/agents/plastic-node-work.md +33 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/assets/plastic-logo.svg +1 -0
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +43 -6
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +6 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/test +24 -4
- package/bin/verify-change +345 -0
- package/config_asks.yml +4 -4
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
- package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
- package/{skills/conventions/references → docs/help}/completion-and-done.md +15 -16
- package/docs/help/human-report-contract.md +152 -0
- package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
- package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +11 -13
- package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
- package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/hooks/call-budget +4 -0
- package/hooks/hooks.json +24 -0
- package/hooks/message-display +55 -2
- package/hooks/session-start +5 -1
- package/hooks/statusline +32 -27
- package/hooks/stop +5 -0
- package/package.json +5 -3
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +267 -16
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +680 -39
- package/scripts/end-intent +153 -30
- package/scripts/exec-worktree +5 -5
- package/scripts/file-session-intent +2 -1
- package/scripts/graph-measure +249 -0
- package/scripts/hook-call-budget +222 -0
- package/scripts/hook-capture +24 -123
- package/scripts/hook-close +2 -1
- package/scripts/hook-message-display +7 -0
- package/scripts/hook-record +24 -16
- package/scripts/hook-savepoint +27 -3
- package/scripts/hook-session-start +359 -321
- package/scripts/hook-stop +58 -0
- package/scripts/index-projection +74 -0
- package/scripts/insight-append +17 -4
- package/scripts/install.rb +9 -7
- package/scripts/lib/action_graph_shim.rb +279 -0
- package/scripts/lib/active_delivery.rb +105 -0
- package/scripts/lib/agent_models.rb +63 -25
- package/scripts/lib/arm.rb +48 -83
- package/scripts/lib/atomic_write.rb +31 -0
- package/scripts/lib/backup.rb +65 -0
- package/scripts/lib/cli/command.rb +92 -0
- package/scripts/lib/cli/commands/auto.rb +18 -0
- package/scripts/lib/cli/commands/auto_brief.rb +44 -0
- package/scripts/lib/cli/commands/auto_lock.rb +99 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +31 -0
- package/scripts/lib/cli/commands/backup.rb +43 -0
- package/scripts/lib/cli/commands/checkout.rb +25 -0
- package/scripts/lib/cli/commands/continue.rb +66 -0
- package/scripts/lib/cli/commands/doctor.rb +81 -0
- package/scripts/lib/cli/commands/feedback.rb +40 -0
- package/scripts/lib/cli/commands/help.rb +69 -0
- package/scripts/lib/cli/commands/hook.rb +32 -0
- package/scripts/lib/cli/commands/index.rb +23 -0
- package/scripts/lib/cli/commands/install.rb +21 -0
- package/scripts/lib/cli/commands/installer_verb.rb +37 -0
- package/scripts/lib/cli/commands/intent.rb +19 -0
- package/scripts/lib/cli/commands/intent_answer.rb +37 -0
- package/scripts/lib/cli/commands/intent_command.rb +53 -0
- package/scripts/lib/cli/commands/intent_end.rb +62 -0
- package/scripts/lib/cli/commands/intent_new.rb +62 -0
- package/scripts/lib/cli/commands/intent_note.rb +43 -0
- package/scripts/lib/cli/commands/intent_rule.rb +36 -0
- package/scripts/lib/cli/commands/intent_show.rb +32 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +71 -0
- package/scripts/lib/cli/commands/intent_verify.rb +26 -0
- package/scripts/lib/cli/commands/migrate.rb +16 -0
- package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
- package/scripts/lib/cli/commands/next.rb +54 -0
- package/scripts/lib/cli/commands/project.rb +19 -0
- package/scripts/lib/cli/commands/project_links.rb +42 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +72 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +33 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +70 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +45 -0
- package/scripts/lib/cli/commands/rollback.rb +20 -0
- package/scripts/lib/cli/commands/search.rb +60 -0
- package/scripts/lib/cli/commands/session.rb +18 -0
- package/scripts/lib/cli/commands/session_commit.rb +42 -0
- package/scripts/lib/cli/commands/session_handoff.rb +34 -0
- package/scripts/lib/cli/commands/session_summary.rb +35 -0
- package/scripts/lib/cli/commands/status.rb +68 -0
- package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
- package/scripts/lib/cli/commands/sync.rb +46 -0
- package/scripts/lib/cli/commands/uninstall.rb +20 -0
- package/scripts/lib/cli/commands/update.rb +20 -0
- package/scripts/lib/cli/commands/version.rb +53 -0
- package/scripts/lib/cli/frontier.rb +86 -0
- package/scripts/lib/cli/intent_progress.rb +36 -0
- package/scripts/lib/cli/legacy.rb +80 -0
- package/scripts/lib/cli/output.rb +137 -0
- package/scripts/lib/cli/scope.rb +134 -0
- package/scripts/lib/cli/table.rb +65 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +198 -0
- package/scripts/lib/compact_instructions.rb +13 -5
- package/scripts/lib/core_integrity.rb +71 -0
- package/scripts/lib/dashboard_screen.rb +40 -0
- package/scripts/lib/data_boundary.rb +132 -0
- package/scripts/lib/day_summary.rb +23 -14
- package/scripts/lib/doctor_core.rb +114 -38
- package/scripts/lib/doctor_session_ledger.rb +5 -53
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_edges.rb +137 -0
- package/scripts/lib/graph_file.rb +246 -0
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +409 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +413 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +98 -0
- package/scripts/lib/guarded_append.rb +155 -0
- package/scripts/lib/handoff.rb +40 -13
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +27 -3
- package/scripts/lib/hook_replay.rb +229 -0
- package/scripts/lib/index_entry.rb +62 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +531 -71
- package/scripts/lib/intent_screen.rb +4 -4
- package/scripts/lib/intent_screen_ansi.rb +73 -12
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/lock.rb +10 -11
- package/scripts/lib/message_display.rb +350 -54
- package/scripts/lib/meter_watch.rb +185 -0
- package/scripts/lib/node_file.rb +234 -0
- package/scripts/lib/node_ids.rb +99 -0
- package/scripts/lib/node_input.rb +913 -0
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +386 -0
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/node_return.rb +204 -0
- package/scripts/lib/node_worktree.rb +337 -0
- package/scripts/lib/outcome_report.rb +440 -0
- package/scripts/lib/preflight.rb +4 -6
- package/scripts/lib/project_config.rb +46 -0
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/ready_set.rb +462 -0
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +18 -0
- package/scripts/lib/report_screen.rb +1371 -47
- package/scripts/lib/rlm/corpus.rb +13 -0
- package/scripts/lib/rlm/probe.rb +29 -0
- package/scripts/lib/rlm/query.rb +22 -0
- package/scripts/lib/roadmap_graph.rb +210 -0
- package/scripts/lib/roadmap_migration.rb +95 -0
- package/scripts/lib/roadmap_queue.rb +158 -8
- package/scripts/lib/roadmap_render.rb +150 -0
- package/scripts/lib/roadmap_savepoint.rb +64 -14
- package/scripts/lib/runner_absorb.rb +703 -0
- package/scripts/lib/runner_answer.rb +206 -0
- package/scripts/lib/runner_core.rb +194 -0
- package/scripts/lib/runner_dispatch.rb +525 -0
- package/scripts/lib/runner_policy.rb +191 -0
- package/scripts/lib/runner_proposals.rb +275 -0
- package/scripts/lib/runner_rewind.rb +201 -0
- package/scripts/lib/runner_sweep.rb +231 -0
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +141 -19
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/screen_paint.rb +365 -28
- package/scripts/lib/screens/dashboard.rb +20 -0
- package/scripts/lib/screens/plan.rb +18 -0
- package/scripts/lib/screens/roadmap.rb +15 -0
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +25 -18
- package/scripts/lib/session_ledger.rb +48 -4
- package/scripts/lib/session_usage.rb +190 -0
- package/scripts/lib/sqlite.rb +22 -0
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/store_discovery.rb +7 -6
- package/scripts/lib/store_layout.rb +54 -0
- package/scripts/lib/store_provisioning.rb +2 -1
- package/scripts/lib/store_sync.rb +85 -0
- package/scripts/lib/stores_move.rb +93 -0
- package/scripts/lib/verify_intent.rb +36 -8
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/lib/worktree.rb +27 -32
- package/scripts/lib/worktree_sweep.rb +6 -5
- package/scripts/link-suggest +2 -1
- package/scripts/meter-watch +57 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +4 -13
- package/scripts/node-input +92 -0
- package/scripts/node-run +225 -0
- package/scripts/node-transition +291 -0
- package/scripts/outcome-report +74 -0
- package/scripts/plastic-lock +42 -33
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +53 -9
- package/scripts/ready-set +126 -0
- package/scripts/release-check +123 -0
- package/scripts/report-screen +177 -16
- package/scripts/roadmap-graph +119 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/runner +581 -0
- package/scripts/savepoint-note +11 -9
- package/scripts/session-commit +2 -1
- package/scripts/session-usage +56 -0
- package/scripts/skill-lint +115 -6
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +25 -4
- package/scripts/validate-work-graph +39 -0
- package/scripts/verify-intent +3 -2
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +7 -7
- package/templates/config.yml +16 -9
- package/templates/dashboard-screen.md +22 -0
- package/templates/display-fixture.md +21 -0
- package/templates/graph.md +16 -0
- package/templates/index.md +1 -1
- package/templates/intent-screen.md +1 -1
- package/templates/node-decision.md +11 -0
- package/templates/node-research.md +13 -0
- package/templates/node-verify.md +13 -0
- package/templates/node-work.md +22 -0
- package/templates/outcome.md +8 -3
- package/templates/project.yml +1 -1
- package/templates/render.css +10 -0
- package/templates/report-plan.md +15 -0
- package/templates/report-roadmap-delivered.md +10 -0
- package/templates/report-roadmap-plan.md +9 -0
- package/templates/report-roadmap-state.md +9 -0
- package/templates/report-state.md +1 -1
- package/templates/roadmap.md +13 -0
- package/bin/plastic.js +0 -70
- package/scripts/lib/bridge.rb +0 -116
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -296
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -66
- package/skills/auto/references/human-report-contract.md +0 -78
- package/skills/conventions/SKILL.md +0 -29
- package/skills/dashboard/SKILL.md +0 -169
- package/skills/dashboard/evals/evals.json +0 -38
- package/skills/dashboard/references/classification.md +0 -22
- package/skills/dashboard/templates/dashboard-global.md +0 -20
- package/skills/dashboard/templates/dashboard-project.md +0 -19
- package/skills/direct/SKILL.md +0 -66
- package/skills/direct/references/request-signals.md +0 -59
- package/skills/doctor/SKILL.md +0 -299
- package/skills/doctor/report.md +0 -102
- package/skills/feedback/SKILL.md +0 -98
- package/skills/feedback/references/transport-and-privacy.md +0 -65
- package/skills/feedback/report.md +0 -36
- package/skills/install/SKILL.md +0 -217
- package/skills/intent-continuing/SKILL.md +0 -154
- package/skills/intent-continuing/references/board-fill.md +0 -43
- package/skills/intent-continuing/references/boarding-matrix.md +0 -34
- package/skills/intent-continuing/references/context-management.md +0 -28
- package/skills/intent-continuing/references/liveness-ranking.md +0 -57
- package/skills/intent-creating/SKILL.md +0 -164
- package/skills/intent-creating/evals/evals.json +0 -72
- package/skills/intent-creating/references/lifecycle.md +0 -81
- package/skills/intent-creating/references/wikilinks.md +0 -8
- package/skills/intent-ending/SKILL.md +0 -176
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -170
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -42
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -130
- package/skills/intent-speccing/evals/evals.json +0 -126
- package/skills/intent-speccing/references/design-principles.md +0 -44
- package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
- package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
- package/skills/project-creating/SKILL.md +0 -162
- package/skills/project-creating/references/hubs-projects.md +0 -55
- package/skills/project-creating/references/project-scaffolding.md +0 -97
- package/skills/releasing/SKILL.md +0 -337
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -66
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -64
- package/skills/roadmap/references/file-format.md +0 -124
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -65
- package/skills/tutorial/evals/evals.json +0 -186
- package/skills/uninstall/SKILL.md +0 -75
- package/skills/update/SKILL.md +0 -126
- /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
- /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "time"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require_relative "ready_set"
|
|
7
|
+
require_relative "node_ledger"
|
|
8
|
+
require_relative "node_file"
|
|
9
|
+
require_relative "node_input"
|
|
10
|
+
require_relative "node_worktree"
|
|
11
|
+
require_relative "work_graph_validator"
|
|
12
|
+
require_relative "runner_core"
|
|
13
|
+
require_relative "runner_policy"
|
|
14
|
+
require_relative "worktree"
|
|
15
|
+
require_relative "savepoint"
|
|
16
|
+
require_relative "guarded_append"
|
|
17
|
+
require_relative "harness_adapter"
|
|
18
|
+
|
|
19
|
+
# RunnerDispatch (intent 340, G7, n5): validates the graph, computes the
|
|
20
|
+
# ready set, applies RunnerPolicy, mints leases, builds node inputs, writes
|
|
21
|
+
# `running`, and returns the dispatch plan `step` prints. Never spawns an
|
|
22
|
+
# agent itself (327 D42): the session does that from the plan this returns.
|
|
23
|
+
#
|
|
24
|
+
# Pure and dependency-injected down to the clock: every side effect - the
|
|
25
|
+
# full validator, the ready-set analyzer, the input builder, the worktree
|
|
26
|
+
# module, the ledger write, git itself - is an injectable keyword argument
|
|
27
|
+
# with a real default, so a test never touches a real repository or a real
|
|
28
|
+
# filesystem outside its own tmpdir.
|
|
29
|
+
module RunnerDispatch
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
DEFAULT_LIMIT = 2
|
|
33
|
+
|
|
34
|
+
# The return-schema instruction (327 D5): rides in the dispatch PLAN, never
|
|
35
|
+
# inside the node input, so `input=<sha>` keeps naming a reproducible input
|
|
36
|
+
# (matrix row 5.23). NodeReturn.parse (n4) is this text's implementation.
|
|
37
|
+
RETURN_CONTRACT = <<~TEXT.freeze
|
|
38
|
+
RETURN CONTRACT: reply with exactly one YAML document as your final
|
|
39
|
+
message, nothing else around it. Keys: node, status, commit, summary,
|
|
40
|
+
findings, report, proposed_nodes, proposed_edges, question, reason. status is one
|
|
41
|
+
of done, failed_verification, needs_decision, blocked. done requires
|
|
42
|
+
commit; needs_decision requires question; failed_verification and
|
|
43
|
+
blocked require reason. Anything that does not parse under this closed
|
|
44
|
+
schema is refused as failed_verification reason=return_unparsable.
|
|
45
|
+
TEXT
|
|
46
|
+
|
|
47
|
+
HARD_CAP_RE = /\Ais at its dispatch cap \((\d+)\/(\d+)\)\z/.freeze
|
|
48
|
+
|
|
49
|
+
# D8 (355, n6): the agent every dispatched, non-decision node names - a
|
|
50
|
+
# role, never a harness (matrix 6.5), and never a consultation advisor, which
|
|
51
|
+
# stays a deliberate, never-auto-dispatched consultation agent.
|
|
52
|
+
SPAWN_AGENT = "plastic-executor"
|
|
53
|
+
|
|
54
|
+
# matrix 6.1/6.2: one spawn block per dispatched node - agent, the model
|
|
55
|
+
# RunnerPolicy.model_for resolved, the node input path, the one test command
|
|
56
|
+
# (NodeInput.test_command_block, n4), and the call cap (n2) - fenced so a
|
|
57
|
+
# session pastes it straight into the Agent tool (327 D42: the runner
|
|
58
|
+
# itself never spawns).
|
|
59
|
+
def spawn_block(model:, input:, test_command:, call_cap:, effort: AgentModels::DEFAULT_EFFORT, agent: SPAWN_AGENT)
|
|
60
|
+
lines = ["agent: #{agent}", "model: #{model}", "effort: #{effort}", "input: #{input}", test_command,
|
|
61
|
+
NodeInput.call_cap_sentence(call_cap)]
|
|
62
|
+
(["```"] + lines + ["```"]).join("\n")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# dispatch(context, limit:) -> a result hash. Always carries :ok, :reason,
|
|
66
|
+
# :errors, :rearm_command, :dispatched, :stop, :parked, :status, :blockers,
|
|
67
|
+
# :plan - fields that do not apply to a given outcome stay nil/empty rather
|
|
68
|
+
# than being omitted, so a caller never has to guard with `dig`.
|
|
69
|
+
def dispatch(context, limit: DEFAULT_LIMIT, now: Time.now, config: {}, harness: nil, caps: ReadySet::DEFAULT_CAPS,
|
|
70
|
+
validator: WorkGraphValidator.method(:validate),
|
|
71
|
+
ready_analyzer: ReadySet.method(:analyze),
|
|
72
|
+
input_builder: NodeInput.method(:build),
|
|
73
|
+
worktree: NodeWorktree,
|
|
74
|
+
ledger: NodeLedger,
|
|
75
|
+
runner: Worktree::ShellRunner.new,
|
|
76
|
+
harness_adapter: HarnessAdapter)
|
|
77
|
+
intent_dir = context.intent_dir
|
|
78
|
+
savepoint_path = File.join(intent_dir.to_s, "savepoint.md")
|
|
79
|
+
|
|
80
|
+
# Intent 340b, G7c, n1, rows 1.19/1.20/D21: resolved ONCE for the whole
|
|
81
|
+
# step through HarnessAdapter, never a literal - `harness:` (this call's
|
|
82
|
+
# `--harness` override, or nil) wins over `config`'s own `agent.type`.
|
|
83
|
+
# Every node this call dispatches carries the SAME value, and the
|
|
84
|
+
# caller (scripts/runner) reads it back off the report to pick which
|
|
85
|
+
# harness's block to render.
|
|
86
|
+
harness_key = harness_adapter.resolve_key(config: config, override: harness)
|
|
87
|
+
|
|
88
|
+
# Row 5.31/5.32: this is RunnerDispatch's OWN lock check, never a shelled
|
|
89
|
+
# `node-transition` call - append_transition below is used in-process
|
|
90
|
+
# (RunnerAbsorb's own pattern), so nothing here inherits node-transition's
|
|
91
|
+
# CLI-level lock refusal (exit 4) for free.
|
|
92
|
+
return lock_refusal(intent_dir) unless context.session
|
|
93
|
+
|
|
94
|
+
content = read_savepoint(savepoint_path)
|
|
95
|
+
entries = NodeLedger.entries_from_content(content)
|
|
96
|
+
|
|
97
|
+
# Row 5.1/5.2/5.3: the full validator runs only when the ledger holds no
|
|
98
|
+
# `running` line for ANY node yet (327 D17's exact precondition) - every
|
|
99
|
+
# later dispatch skips it and relies on the cheaper re-read below.
|
|
100
|
+
#
|
|
101
|
+
# M9: both the validator and the ready analyzer re-parse graph.md fresh
|
|
102
|
+
# (RunnerCore.context's own first read, already guarded, is not reused
|
|
103
|
+
# here on purpose - row 5.4), and a malformed graph.md - non-UTF-8 bytes,
|
|
104
|
+
# say - raises out of both rather than reporting :ok false. Guarded here
|
|
105
|
+
# so `step` refuses cleanly instead of dying with a raw stack trace.
|
|
106
|
+
if entries.none? { |e| e[:state] == "running" }
|
|
107
|
+
full = safe_validate(validator, intent_dir)
|
|
108
|
+
return invalid_graph_result(full[:errors]) unless full[:ok]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Row 5.4: re-read graph.md and cycle-check on EVERY dispatch, first or
|
|
112
|
+
# not. ReadySet.analyze re-parses graph.md and nodes/ from disk itself,
|
|
113
|
+
# so nothing here trusts `context.graph`, which was resolved once,
|
|
114
|
+
# before this step even started.
|
|
115
|
+
analysis = safe_analyze(ready_analyzer, intent_dir, now: now, caps: caps)
|
|
116
|
+
return invalid_graph_result(analysis[:errors]) unless analysis[:ok]
|
|
117
|
+
|
|
118
|
+
loaded = RunnerCore.safe_load_graph(intent_dir)
|
|
119
|
+
return invalid_graph_result(loaded[:errors]) unless loaded[:ok]
|
|
120
|
+
|
|
121
|
+
edges = loaded[:edges]
|
|
122
|
+
nodes_decl = loaded[:nodes]
|
|
123
|
+
|
|
124
|
+
# Minor 9: count `running` only for NODE subjects - an `Intent` subject
|
|
125
|
+
# in a running-like state must never eat a dispatch slot meant for the
|
|
126
|
+
# concurrency ceiling over nodes.
|
|
127
|
+
running_count = NodeLedger.status_from_content(content).count do |subject, s|
|
|
128
|
+
s == "running" && subject.to_s.match?(Savepoint::NODE_SUBJECT_RE)
|
|
129
|
+
end
|
|
130
|
+
slots = [limit.to_i - running_count, 0].max
|
|
131
|
+
|
|
132
|
+
dispatched = []
|
|
133
|
+
parked = []
|
|
134
|
+
stop = nil
|
|
135
|
+
input_failures = []
|
|
136
|
+
ceiling_blocked = false
|
|
137
|
+
|
|
138
|
+
analysis[:ranked_ready].each do |row|
|
|
139
|
+
break if stop
|
|
140
|
+
|
|
141
|
+
node = row[:id].to_s
|
|
142
|
+
kind = (nodes_decl[node] || {})[:kind] || row[:kind]
|
|
143
|
+
|
|
144
|
+
# Row 5.33: the ONLY overlap check anywhere in this loop - re-evaluated
|
|
145
|
+
# against the latest on-disk content on every iteration, so a sibling
|
|
146
|
+
# this very step just dispatched (its `running` line already written)
|
|
147
|
+
# is seen by the very next candidate, without RunnerDispatch ever
|
|
148
|
+
# carrying its own copy of the overlap rule.
|
|
149
|
+
live = ReadySet.ready?(content: content, subject: node, graph: { edges: edges }, nodes: nodes_decl,
|
|
150
|
+
caps: caps)
|
|
151
|
+
next unless live[:ready]
|
|
152
|
+
|
|
153
|
+
if kind.to_s == "decision"
|
|
154
|
+
stop = write_decision_stop(savepoint_path, intent_dir, node, ledger: ledger, now: now)
|
|
155
|
+
content = read_savepoint(savepoint_path)
|
|
156
|
+
next
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
if RunnerPolicy.at_retry_cap?(entries, node, kind)
|
|
160
|
+
parked << write_retry_cap_park(savepoint_path, intent_dir, node, kind, entries, ledger: ledger, now: now)
|
|
161
|
+
content = read_savepoint(savepoint_path)
|
|
162
|
+
entries = NodeLedger.entries_from_content(content)
|
|
163
|
+
next
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# M10: a ready node that cannot dispatch because every slot is taken
|
|
167
|
+
# is QUEUED, not stalled - the graph can still continue, it is merely
|
|
168
|
+
# waiting on the ceiling, matrix row 10.13.
|
|
169
|
+
if slots <= 0
|
|
170
|
+
ceiling_blocked = true
|
|
171
|
+
next
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
result = dispatch_one(context, node: node, kind: kind, now: now, config: config, harness: harness_key,
|
|
175
|
+
caps: caps, edges: edges, nodes_decl: nodes_decl, input_builder: input_builder,
|
|
176
|
+
worktree: worktree, ledger: ledger, runner: runner)
|
|
177
|
+
if result[:input_build_failed]
|
|
178
|
+
input_failures << result
|
|
179
|
+
next
|
|
180
|
+
end
|
|
181
|
+
next unless result[:ok]
|
|
182
|
+
|
|
183
|
+
dispatched << result[:entry]
|
|
184
|
+
slots -= 1
|
|
185
|
+
content = read_savepoint(savepoint_path)
|
|
186
|
+
entries = NodeLedger.entries_from_content(content)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Row 5.34: re-render graph.md's ## Status once, after every write this
|
|
190
|
+
# step made, mirroring RunnerAbsorb's own single call after its own
|
|
191
|
+
# transition.
|
|
192
|
+
RunnerCore.render_status(context) if dispatched.any? || stop || parked.any?
|
|
193
|
+
|
|
194
|
+
build_report(context: context, dispatched: dispatched, stop: stop, parked: parked,
|
|
195
|
+
ceiling_blocked: ceiling_blocked, input_failures: input_failures, running_count: running_count,
|
|
196
|
+
harness: harness_key)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# --- guarded re-entries into graph.md (M9) ----------------------------------
|
|
200
|
+
|
|
201
|
+
def safe_validate(validator, intent_dir)
|
|
202
|
+
validator.call(intent_dir)
|
|
203
|
+
rescue StandardError => e
|
|
204
|
+
{ ok: false, errors: ["graph.md could not be read: #{e.message}"] }
|
|
205
|
+
end
|
|
206
|
+
private_class_method :safe_validate
|
|
207
|
+
|
|
208
|
+
def safe_analyze(ready_analyzer, intent_dir, now:, caps:)
|
|
209
|
+
ready_analyzer.call(intent_dir, now: now, caps: caps)
|
|
210
|
+
rescue StandardError => e
|
|
211
|
+
{ ok: false, errors: ["graph.md could not be read: #{e.message}"] }
|
|
212
|
+
end
|
|
213
|
+
private_class_method :safe_analyze
|
|
214
|
+
|
|
215
|
+
# --- one node's whole dispatch (node input, lease, `running`) -------------------
|
|
216
|
+
|
|
217
|
+
def dispatch_one(context, node:, kind:, now:, config:, harness:, caps:, edges:, nodes_decl:, input_builder:,
|
|
218
|
+
worktree:, ledger:, runner:)
|
|
219
|
+
intent_dir = context.intent_dir
|
|
220
|
+
savepoint_path = File.join(intent_dir.to_s, "savepoint.md")
|
|
221
|
+
|
|
222
|
+
holder = context.session
|
|
223
|
+
model = RunnerPolicy.model_for(kind, config: config, harness: harness)
|
|
224
|
+
effort = RunnerPolicy.effort_for(kind, config: config, harness: harness)
|
|
225
|
+
expires = RunnerPolicy.lease_expires(kind, now: now)
|
|
226
|
+
calls_cap = RunnerPolicy.call_cap(kind, config: config)
|
|
227
|
+
|
|
228
|
+
# Row 10.16/M13: recorded BEFORE provisioning - a worktree this dispatch
|
|
229
|
+
# finds already on disk (kept there by a prior failed_verification
|
|
230
|
+
# attempt, D7) must never be the one a later rollback in this same call
|
|
231
|
+
# deletes; only a worktree THIS call actually creates may be rolled back.
|
|
232
|
+
pre_existing_worktree = worktree_pre_existing?(worktree, context, node, kind)
|
|
233
|
+
|
|
234
|
+
# Row 5.16/5.29: only a `work` node gets a worktree, and this is the
|
|
235
|
+
# node-scoped `worktree_reader:` D23 injects into NodeInput.build - it
|
|
236
|
+
# names THIS node's own worktree and branch, never the intent's.
|
|
237
|
+
provisioned = RunnerPolicy.worktree?(kind) ? worktree.provision(context, node: node, kind: kind, runner: runner)
|
|
238
|
+
: unprovisioned
|
|
239
|
+
node_reader = lambda do |intent_dir:|
|
|
240
|
+
{ "code" => provisioned[:path], "code_branch" => provisioned[:branch], "provisioned" => !!provisioned[:provisioned] }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Row 5.18: build the node input BEFORE writing `running` - a `running` line
|
|
244
|
+
# naming bytes that do not exist yet is worse than a node input nobody reads.
|
|
245
|
+
# Row 5.30: `force: true` always - a fresh attempt number this dispatch
|
|
246
|
+
# computes is, by construction, never one `running` has already claimed,
|
|
247
|
+
# so an existing file at that path is always an orphan from a step that
|
|
248
|
+
# crashed between building the node input and writing `running`, safe to
|
|
249
|
+
# overwrite outright.
|
|
250
|
+
# Row 5.20/10.8: the node's own declared budget: (M7) - nil when the node
|
|
251
|
+
# names none, in which case NodeInput.build falls back to its own
|
|
252
|
+
# default (row 10.9).
|
|
253
|
+
build_result = input_builder.call(intent_dir: intent_dir, node: node, holder: holder, expires: expires,
|
|
254
|
+
model: model, force: true, worktree_reader: node_reader,
|
|
255
|
+
budget_tokens: node_declared_budget(intent_dir, node), call_cap: calls_cap)
|
|
256
|
+
unless build_result[:ok]
|
|
257
|
+
# M6: a failed input build never leaves an orphan worktree behind, and
|
|
258
|
+
# its errors travel back up so the step's report can name the node and
|
|
259
|
+
# the reason instead of a bare "stalled" (row 10.6/10.7).
|
|
260
|
+
rollback_dispatch(context, node: node, kind: kind, input_path: build_result[:path], runner: runner,
|
|
261
|
+
worktree: worktree, created_this_dispatch: !pre_existing_worktree)
|
|
262
|
+
return { ok: false, input_build_failed: true, node: node, errors: build_result[:errors] }
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
precondition = lambda do |c|
|
|
266
|
+
ReadySet.ready?(content: c, subject: node, graph: { edges: edges }, nodes: nodes_decl, caps: caps)[:ready]
|
|
267
|
+
end
|
|
268
|
+
# Row 1.19/1.20/D21: harness= rides alongside model= on every `running`
|
|
269
|
+
# line, resolved once by the caller through HarnessAdapter and threaded
|
|
270
|
+
# straight through here - never re-resolved, never a literal.
|
|
271
|
+
fields = { holder: holder, expires: expires, input: build_result[:sha], model: model, effort: effort, harness: harness,
|
|
272
|
+
calls: calls_cap }
|
|
273
|
+
|
|
274
|
+
result = begin
|
|
275
|
+
ledger.append_transition(savepoint_path, subject: node, state: "running", fields: fields, now: now,
|
|
276
|
+
precondition: precondition)
|
|
277
|
+
rescue GuardedAppend::Unavailable
|
|
278
|
+
:unavailable
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Row 5.20: a refused (or unavailable) `running` write rolls back both
|
|
282
|
+
# side effects this method already produced - the node never ran, so
|
|
283
|
+
# nothing may act like it did.
|
|
284
|
+
unless result == :written
|
|
285
|
+
rollback_dispatch(context, node: node, kind: kind, input_path: build_result[:path], runner: runner,
|
|
286
|
+
worktree: worktree, created_this_dispatch: !pre_existing_worktree)
|
|
287
|
+
return { ok: false }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
test_command = NodeInput.test_command_block(intent_dir: intent_dir, files: (nodes_decl[node] || {})[:files])
|
|
291
|
+
spawn = spawn_block(model: model, effort: effort, input: build_result[:path], test_command: test_command,
|
|
292
|
+
call_cap: calls_cap)
|
|
293
|
+
|
|
294
|
+
{
|
|
295
|
+
ok: true,
|
|
296
|
+
entry: { node: node, kind: kind.to_s, role: role_for(kind), model: model, effort: effort,
|
|
297
|
+
worktree: provisioned[:path],
|
|
298
|
+
input: build_result[:path], spawn: spawn },
|
|
299
|
+
}
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# The node's own declared budget: (frontmatter), or nil when it names
|
|
303
|
+
# none - M7. Parsed directly off the node file, never through
|
|
304
|
+
# `nodes_decl` (ReadySet.load_graph's own decl hash carries only kind and
|
|
305
|
+
# files, never budget), so this stays independent of that module.
|
|
306
|
+
def node_declared_budget(intent_dir, node)
|
|
307
|
+
path = ReadySet.find_node_path(intent_dir, node)
|
|
308
|
+
return nil unless path
|
|
309
|
+
|
|
310
|
+
nf = NodeFile.parse(path)
|
|
311
|
+
nf[:ok] ? nf[:budget] : nil
|
|
312
|
+
end
|
|
313
|
+
private_class_method :node_declared_budget
|
|
314
|
+
|
|
315
|
+
# true iff a `work` node's own worktree already exists BEFORE this call
|
|
316
|
+
# provisions anything - the pre-check `rollback_dispatch` needs to tell a
|
|
317
|
+
# worktree this dispatch created from one it merely found (row 10.16).
|
|
318
|
+
def worktree_pre_existing?(worktree, context, node, kind)
|
|
319
|
+
return false unless RunnerPolicy.worktree?(kind)
|
|
320
|
+
|
|
321
|
+
p = worktree.paths(context, node: node)
|
|
322
|
+
!!(p && p["path"] && Dir.exist?(p["path"]))
|
|
323
|
+
end
|
|
324
|
+
private_class_method :worktree_pre_existing?
|
|
325
|
+
|
|
326
|
+
def role_for(kind)
|
|
327
|
+
kind.to_s == "verify" ? "advisor" : "executor"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def unprovisioned
|
|
331
|
+
{ ok: true, path: nil, branch: nil, provisioned: false }
|
|
332
|
+
end
|
|
333
|
+
private_class_method :unprovisioned
|
|
334
|
+
|
|
335
|
+
# Row 10.16/M13: `created_this_dispatch:` gates the worktree half of the
|
|
336
|
+
# rollback - a worktree this call did not create (kept on disk by a prior
|
|
337
|
+
# attempt's failed_verification, D7) is never touched, only a node input this
|
|
338
|
+
# call's own `input_builder` may have written is ever deleted.
|
|
339
|
+
def rollback_dispatch(context, node:, kind:, input_path:, runner:, worktree:, created_this_dispatch:)
|
|
340
|
+
File.delete(input_path) if input_path && File.exist?(input_path)
|
|
341
|
+
return unless RunnerPolicy.worktree?(kind)
|
|
342
|
+
return unless created_this_dispatch
|
|
343
|
+
|
|
344
|
+
p = worktree.paths(context, node: node)
|
|
345
|
+
return if p["path"].nil? || !Dir.exist?(p["path"])
|
|
346
|
+
|
|
347
|
+
Worktree.remove_worktree(runner, repo: p["repo"], worktree: p["path"])
|
|
348
|
+
Worktree.prune(runner, repo: p["repo"])
|
|
349
|
+
end
|
|
350
|
+
private_class_method :rollback_dispatch
|
|
351
|
+
|
|
352
|
+
# --- decision stop and the retry-cap park -----------------------------------
|
|
353
|
+
|
|
354
|
+
# Row 5.9/5.10: a ready decision node is never dispatched - it stops the
|
|
355
|
+
# WHOLE step (any node ranked after it this step is simply not reached)
|
|
356
|
+
# and writes its own `needs_decision` line so it reads that way from
|
|
357
|
+
# `status` too, carrying the exact `runner answer` command that clears it.
|
|
358
|
+
def write_decision_stop(savepoint_path, intent_dir, node, ledger:, now:)
|
|
359
|
+
question = decision_question(intent_dir, node)
|
|
360
|
+
safe_append(ledger, savepoint_path, node, "needs_decision", { question: question }, now: now)
|
|
361
|
+
{ reason: "decision", node: node, question: question, answer_command: answer_command(intent_dir, node) }
|
|
362
|
+
end
|
|
363
|
+
private_class_method :write_decision_stop
|
|
364
|
+
|
|
365
|
+
# Row 5.11/5.27/5.28: a node at RunnerPolicy's SOFT cap is parked at
|
|
366
|
+
# `needs_decision` with a synthesized `question=` rather than dispatched a
|
|
367
|
+
# further time, carrying the same `runner answer` shape.
|
|
368
|
+
def write_retry_cap_park(savepoint_path, intent_dir, node, kind, entries, ledger:, now:)
|
|
369
|
+
count = RunnerPolicy.retry_count(entries, node)
|
|
370
|
+
cap = RunnerPolicy.retry_cap(kind)
|
|
371
|
+
question = "#{node} has failed verification #{count} time(s), its #{kind} retry cap is #{cap}; " \
|
|
372
|
+
"retry, rewind, or abandon it?"
|
|
373
|
+
safe_append(ledger, savepoint_path, node, "needs_decision", { question: question }, now: now)
|
|
374
|
+
{ reason: "retry_cap", node: node, question: question, answer_command: answer_command(intent_dir, node) }
|
|
375
|
+
end
|
|
376
|
+
private_class_method :write_retry_cap_park
|
|
377
|
+
|
|
378
|
+
def safe_append(ledger, savepoint_path, node, state, fields, now:)
|
|
379
|
+
ledger.append_transition(savepoint_path, subject: node, state: state, fields: fields, now: now)
|
|
380
|
+
rescue GuardedAppend::Unavailable
|
|
381
|
+
nil
|
|
382
|
+
end
|
|
383
|
+
private_class_method :safe_append
|
|
384
|
+
|
|
385
|
+
def decision_question(intent_dir, node)
|
|
386
|
+
path = ReadySet.find_node_path(intent_dir, node)
|
|
387
|
+
fallback = "#{node} needs an owner decision; see its ## Question section"
|
|
388
|
+
return fallback unless path
|
|
389
|
+
|
|
390
|
+
nf = NodeFile.parse(path)
|
|
391
|
+
return fallback unless nf[:ok]
|
|
392
|
+
|
|
393
|
+
section = NodeFile.split_by_headings(nf[:body]).find { |(heading, _)| heading.to_s.strip == "## Question" }
|
|
394
|
+
text = section && section[1].to_s.strip
|
|
395
|
+
text && !text.empty? ? squash(text) : fallback
|
|
396
|
+
end
|
|
397
|
+
private_class_method :decision_question
|
|
398
|
+
|
|
399
|
+
def squash(text)
|
|
400
|
+
text.to_s.gsub(/\s+/, " ").strip
|
|
401
|
+
end
|
|
402
|
+
private_class_method :squash
|
|
403
|
+
|
|
404
|
+
# Row 5.10/5.28: the one command shape every stop and every park prints,
|
|
405
|
+
# matching scripts/runner's own published usage
|
|
406
|
+
# (`runner <step|status|answer> <intent_dir> [--node ID] [--answer TEXT]`).
|
|
407
|
+
def answer_command(intent_dir, node)
|
|
408
|
+
"runner answer #{intent_dir} --node #{node} --answer \"<your answer>\""
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Row 5.32: re-arms delivery.lock for a resumed session with a new id -
|
|
412
|
+
# `plastic-lock arm` is the shipped command that takes ownership again.
|
|
413
|
+
def rearm_command(intent_dir)
|
|
414
|
+
"plastic-lock arm --intent-dir #{intent_dir}"
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# --- refusals and the report -------------------------------------------------
|
|
418
|
+
|
|
419
|
+
def empty_result
|
|
420
|
+
{ ok: true, reason: nil, errors: [], rearm_command: nil, dispatched: [], stop: nil, parked: [],
|
|
421
|
+
status: nil, blockers: [], plan: nil, harness: nil }
|
|
422
|
+
end
|
|
423
|
+
private_class_method :empty_result
|
|
424
|
+
|
|
425
|
+
def lock_refusal(intent_dir)
|
|
426
|
+
empty_result.merge(ok: false, reason: "lock_not_held", rearm_command: rearm_command(intent_dir))
|
|
427
|
+
end
|
|
428
|
+
private_class_method :lock_refusal
|
|
429
|
+
|
|
430
|
+
def invalid_graph_result(errors)
|
|
431
|
+
empty_result.merge(ok: false, reason: "invalid_graph", errors: Array(errors))
|
|
432
|
+
end
|
|
433
|
+
private_class_method :invalid_graph_result
|
|
434
|
+
|
|
435
|
+
def build_report(context:, dispatched:, stop:, parked:, ceiling_blocked: false, input_failures: [],
|
|
436
|
+
running_count: 0, harness: nil)
|
|
437
|
+
base = empty_result.merge(dispatched: dispatched, stop: stop, parked: parked,
|
|
438
|
+
plan: render_plan(dispatched), harness: harness)
|
|
439
|
+
|
|
440
|
+
if dispatched.any?
|
|
441
|
+
base.merge(status: "dispatched")
|
|
442
|
+
elsif stop
|
|
443
|
+
base.merge(status: "needs_decision")
|
|
444
|
+
elsif ceiling_blocked || running_count.to_i.positive?
|
|
445
|
+
# M10/v2 NEW-7: a ready node waiting on the concurrency ceiling is one
|
|
446
|
+
# shape of "still in flight" - a graph where every ready node is
|
|
447
|
+
# ALREADY running (no candidate ever reaches the ceiling check at all,
|
|
448
|
+
# so `ceiling_blocked` never sets) is the ordinary busy case, and it
|
|
449
|
+
# used to fall all the way through to `stalled`. Any node genuinely
|
|
450
|
+
# `running` means the graph can still continue on its own.
|
|
451
|
+
base.merge(status: "queued")
|
|
452
|
+
else
|
|
453
|
+
# Row 5.25: complete iff EVERY declared node is terminal - an empty
|
|
454
|
+
# ready set from parked/blocked nodes must never read as finished.
|
|
455
|
+
complete = RunnerCore.complete?(context)
|
|
456
|
+
if complete
|
|
457
|
+
base.merge(status: "complete")
|
|
458
|
+
else
|
|
459
|
+
# M6/row 10.7: a failed input build writes no ledger line at all,
|
|
460
|
+
# so `named_blockers` (ledger-derived) never sees it on its own -
|
|
461
|
+
# its own node and reason are named here so `stalled` never prints
|
|
462
|
+
# bare.
|
|
463
|
+
blockers = named_blockers(context) + input_failures.map { |f| input_failure_blocker(f) }
|
|
464
|
+
base.merge(status: "stalled", blockers: blockers)
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
private_class_method :build_report
|
|
469
|
+
|
|
470
|
+
def input_failure_blocker(failure)
|
|
471
|
+
"#{failure[:node]}: node input build failed (#{Array(failure[:errors]).join('; ')})"
|
|
472
|
+
end
|
|
473
|
+
private_class_method :input_failure_blocker
|
|
474
|
+
|
|
475
|
+
# Row 5.25a/5.26: every unfinished node's own blockers, with ReadySet's
|
|
476
|
+
# hard-attempt-cap wording renamed so it reads as the named backstop it is
|
|
477
|
+
# (D22: the exit here is `runner answer`, never another dispatch), rather
|
|
478
|
+
# than one more indistinguishable blocker line.
|
|
479
|
+
def named_blockers(context)
|
|
480
|
+
intent_dir = context.intent_dir
|
|
481
|
+
rows = RunnerCore.status(context)
|
|
482
|
+
rows.each_with_object([]) do |(id, view), out|
|
|
483
|
+
next if ReadySet::TERMINAL_STATES.include?(view[:state])
|
|
484
|
+
|
|
485
|
+
view[:blockers].each { |b| out << name_blocker(intent_dir, id, b) }
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
private_class_method :named_blockers
|
|
489
|
+
|
|
490
|
+
def name_blocker(intent_dir, id, blocker)
|
|
491
|
+
rest = blocker.to_s.sub(/\A#{Regexp.escape(id)} /, "")
|
|
492
|
+
m = rest.match(HARD_CAP_RE)
|
|
493
|
+
return "#{id}: #{blocker}" unless m
|
|
494
|
+
|
|
495
|
+
"#{id} has reached its hard attempt backstop (#{m[1]}/#{m[2]}) - #{answer_command(intent_dir, id)}"
|
|
496
|
+
end
|
|
497
|
+
private_class_method :name_blocker
|
|
498
|
+
|
|
499
|
+
# Row 5.22/5.23/5.24: one machine-readable (YAML) document naming, per
|
|
500
|
+
# dispatched node, the node input path, the model, the worktree, the kind and
|
|
501
|
+
# the role, plus the return contract ONCE at the top level - never inside
|
|
502
|
+
# any one node's input. Row 6.4: "spawn" carries the same, already fully
|
|
503
|
+
# rendered spawn block for each dispatched node in order, so any reader of
|
|
504
|
+
# this data (YAML today, JSON if it is ever re-serialized) finds it under
|
|
505
|
+
# `spawn` rather than re-deriving it from the other fields.
|
|
506
|
+
def render_plan(dispatched)
|
|
507
|
+
return nil if dispatched.empty?
|
|
508
|
+
|
|
509
|
+
YAML.dump(
|
|
510
|
+
"return_contract" => RETURN_CONTRACT,
|
|
511
|
+
"dispatch" => dispatched.map do |d|
|
|
512
|
+
{ "node" => d[:node], "kind" => d[:kind], "role" => d[:role], "model" => d[:model],
|
|
513
|
+
"effort" => d[:effort],
|
|
514
|
+
"worktree" => d[:worktree], "input" => d[:input] }
|
|
515
|
+
end,
|
|
516
|
+
"spawn" => dispatched.map { |d| d[:spawn] }
|
|
517
|
+
)
|
|
518
|
+
end
|
|
519
|
+
private_class_method :render_plan
|
|
520
|
+
|
|
521
|
+
def read_savepoint(path)
|
|
522
|
+
File.exist?(path) ? File.read(path) : ""
|
|
523
|
+
end
|
|
524
|
+
private_class_method :read_savepoint
|
|
525
|
+
end
|