@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
package/scripts/lib/worktree.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "json"
|
|
5
6
|
require "yaml"
|
|
6
7
|
require "socket"
|
|
@@ -108,16 +109,16 @@ module Worktree
|
|
|
108
109
|
|
|
109
110
|
# --- provisioning ----------------------------------------------------------
|
|
110
111
|
|
|
111
|
-
# Resolve the slug from the
|
|
112
|
+
# Resolve the slug from the delivery's intent.store, create code + store
|
|
112
113
|
# worktrees (idempotent: reuse an existing worktree path, do not error), write
|
|
113
|
-
# the `worktree` block plus `provisioned: true` onto
|
|
114
|
+
# the `worktree` block plus `provisioned: true` onto delivery, return it.
|
|
114
115
|
#
|
|
115
116
|
# Fails open with a stderr log when the repo is non-git or unresolvable:
|
|
116
117
|
# sets `provisioned: false` and leaves `code: null`. All git ops use
|
|
117
118
|
# `git -C <resolved path>` -- never cwd (decision D6).
|
|
118
|
-
def provision(
|
|
119
|
-
return
|
|
120
|
-
intent =
|
|
119
|
+
def provision(delivery, home: Dir.home, runner: ShellRunner.new)
|
|
120
|
+
return delivery unless delivery.is_a?(Hash)
|
|
121
|
+
intent = delivery["intent"] || {}
|
|
121
122
|
intent_id = intent["id"].to_s
|
|
122
123
|
store = intent["store"].to_s
|
|
123
124
|
intent_slug = slug_from_dir(intent["dir"]) || slug_from_dir(store)
|
|
@@ -173,15 +174,15 @@ module Worktree
|
|
|
173
174
|
# replaced by the fresh unprovisioned block; the guard must not fail harder
|
|
174
175
|
# than the bug it guards.
|
|
175
176
|
if code_ok
|
|
176
|
-
|
|
177
|
+
delivery["worktree"] = block
|
|
177
178
|
else
|
|
178
|
-
existing =
|
|
179
|
+
existing = delivery["worktree"]
|
|
179
180
|
keep = existing.is_a?(Hash) && !blank?(existing["code"]) &&
|
|
180
181
|
Dir.exist?(existing["code"].to_s)
|
|
181
|
-
|
|
182
|
+
delivery["worktree"] = block unless keep
|
|
182
183
|
end
|
|
183
184
|
|
|
184
|
-
|
|
185
|
+
delivery
|
|
185
186
|
end
|
|
186
187
|
|
|
187
188
|
# Remove the worktree (then `git worktree prune`), clear the worktree block.
|
|
@@ -189,21 +190,21 @@ module Worktree
|
|
|
189
190
|
# policy on top via `finish`; this is the plain remove. Pass `remove: false` to
|
|
190
191
|
# clear the block WITHOUT touching git (so `finish` can merge first, then call
|
|
191
192
|
# release to drop the worktree once the code branch is integrated).
|
|
192
|
-
def release(
|
|
193
|
-
return
|
|
194
|
-
block =
|
|
195
|
-
return
|
|
193
|
+
def release(delivery, home: Dir.home, runner: ShellRunner.new, remove: true)
|
|
194
|
+
return delivery unless delivery.is_a?(Hash)
|
|
195
|
+
block = delivery["worktree"]
|
|
196
|
+
return delivery unless block.is_a?(Hash)
|
|
196
197
|
|
|
197
198
|
if remove
|
|
198
|
-
slug = slug_for_store(
|
|
199
|
+
slug = slug_for_store(delivery.dig("intent", "store").to_s, home: home)
|
|
199
200
|
repo = repo_for(slug, home: home)
|
|
200
201
|
|
|
201
202
|
remove_worktree(runner, repo: repo, worktree: block["code"]) if repo && block["code"]
|
|
202
203
|
prune(runner, repo: repo) if repo
|
|
203
204
|
end
|
|
204
205
|
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
delivery.delete("worktree")
|
|
207
|
+
delivery
|
|
207
208
|
end
|
|
208
209
|
|
|
209
210
|
# --- cleanup policy (merge-vs-remove) -------------------------------------
|
|
@@ -222,19 +223,19 @@ module Worktree
|
|
|
222
223
|
# Fail-open and idempotent throughout: a missing block, missing branch, or any
|
|
223
224
|
# git failure never raises and never blocks teardown. All git ops use
|
|
224
225
|
# `git -C <path>`, never cwd (decision D6). No-op when nothing was provisioned.
|
|
225
|
-
def finish(
|
|
226
|
-
return
|
|
227
|
-
block =
|
|
228
|
-
return
|
|
226
|
+
def finish(delivery, home: Dir.home, runner: ShellRunner.new, merge: false)
|
|
227
|
+
return delivery unless delivery.is_a?(Hash)
|
|
228
|
+
block = delivery["worktree"]
|
|
229
|
+
return delivery unless block.is_a?(Hash)
|
|
229
230
|
|
|
230
231
|
if merge
|
|
231
|
-
slug = slug_for_store(
|
|
232
|
+
slug = slug_for_store(delivery.dig("intent", "store").to_s, home: home)
|
|
232
233
|
repo = repo_for(slug, home: home)
|
|
233
234
|
branch = block["code_branch"]
|
|
234
235
|
merge_branch(runner, repo: repo, branch: branch) if repo && !blank?(branch)
|
|
235
236
|
end
|
|
236
237
|
|
|
237
|
-
release(
|
|
238
|
+
release(delivery, home: home, runner: runner, remove: true)
|
|
238
239
|
end
|
|
239
240
|
|
|
240
241
|
# Merge `branch` into the repo's default branch from the main checkout. The
|
|
@@ -298,8 +299,8 @@ module Worktree
|
|
|
298
299
|
# --- lock ------------------------------------------------------------------
|
|
299
300
|
|
|
300
301
|
# True iff ANOTHER session's delivery.lock is FRESH on this intent's dir
|
|
301
|
-
# (intent 108, D2): the durable lock file decides
|
|
302
|
-
#
|
|
302
|
+
# (intent 108, D2): the durable lock file decides alone, and no pid is
|
|
303
|
+
# probed. current_session being the owner or a
|
|
303
304
|
# delegate does not count as "other". A stale lock does not hold (explicit
|
|
304
305
|
# takeover reclaims it).
|
|
305
306
|
def lock_held_by_other?(intent_id:, store:, current_session:, home: Dir.home,
|
|
@@ -377,14 +378,8 @@ module Worktree
|
|
|
377
378
|
# (no project repo). Mirrors qmd_sync's slug_for_store fallback.
|
|
378
379
|
def slug_for_store(store_dir, home: Dir.home)
|
|
379
380
|
return nil if blank?(store_dir)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return nil if store_dir == File.join(plastic_home, "store")
|
|
383
|
-
|
|
384
|
-
parts = store_dir.split(File::SEPARATOR)
|
|
385
|
-
idx = parts.rindex("projects")
|
|
386
|
-
return parts[idx + 1] if idx && parts[idx + 1] && parts[idx + 2] == "store"
|
|
387
|
-
nil
|
|
381
|
+
slug = Plastic::StoreLayout.locate(store_dir).last
|
|
382
|
+
(slug == Plastic::StoreLayout::GLOBAL) ? nil : slug
|
|
388
383
|
end
|
|
389
384
|
|
|
390
385
|
# Best-effort slug for the worktree dir-name from an intent dir/store path:
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require_relative "worktree"
|
|
5
|
-
require_relative "
|
|
6
|
+
require_relative "index_entry"
|
|
6
7
|
|
|
7
8
|
# WorktreeSweep -- the one-time orphan sweep for store worktrees retired by
|
|
8
9
|
# intent 178 (D2). Pure candidate classification plus a thin apply step, both
|
|
@@ -55,14 +56,14 @@ module WorktreeSweep
|
|
|
55
56
|
# Matching on the full name, not just the numeric id, sidesteps id
|
|
56
57
|
# collisions across projects (ids are only unique WITHIN one store).
|
|
57
58
|
def resolve_intent_dir(plastic_home, name)
|
|
58
|
-
candidates = [File.join(plastic_home,
|
|
59
|
-
Dir.glob(File.join(plastic_home, "
|
|
59
|
+
candidates = [File.join(Plastic::StoreLayout.global_store(plastic_home), name)] +
|
|
60
|
+
Dir.glob(File.join(Plastic::StoreLayout.projects_root(plastic_home), "*", "store", name))
|
|
60
61
|
candidates.find { |d| Dir.exist?(d) }
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
# The INDEX.md section heading (e.g. "Active", "Completed") that lists this
|
|
64
65
|
# intent dir, or nil if no INDEX.md entry links to it. Reuses
|
|
65
|
-
#
|
|
66
|
+
# IndexEntry.match so this never drifts from the shared parser.
|
|
66
67
|
def index_status(intent_dir, name)
|
|
67
68
|
store = File.dirname(intent_dir)
|
|
68
69
|
index = File.join(File.dirname(store), "INDEX.md")
|
|
@@ -75,7 +76,7 @@ module WorktreeSweep
|
|
|
75
76
|
section = stripped.sub(/\A##\s*/, "").strip
|
|
76
77
|
next
|
|
77
78
|
end
|
|
78
|
-
m =
|
|
79
|
+
m = IndexEntry.match(stripped)
|
|
79
80
|
next unless m
|
|
80
81
|
return section if m[3].to_s.include?(name)
|
|
81
82
|
end
|
package/scripts/link-suggest
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
# nothing. --edge is explicit and required (sources is decided by origin, never
|
|
27
27
|
# inferred); --rating is required when --edge is chain.
|
|
28
28
|
|
|
29
|
+
require_relative "lib/store_layout"
|
|
29
30
|
require_relative "lib/link_suggestions"
|
|
30
31
|
|
|
31
32
|
module LinkSuggestCLI
|
|
@@ -69,7 +70,7 @@ module LinkSuggestCLI
|
|
|
69
70
|
i += 1
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
|
-
opts[:store_dir] ||=
|
|
73
|
+
opts[:store_dir] ||= Plastic::StoreLayout.global_store(opts[:home])
|
|
73
74
|
opts
|
|
74
75
|
end
|
|
75
76
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# meter-watch (intent 355, n5, D6): reads the rate-limit cache under --home
|
|
6
|
+
# and writes ~/.plastic/.cache/meter-state.json with the state (ok, reduce,
|
|
7
|
+
# stop, resume, stale, unavailable), the two raw percentages, the reset
|
|
8
|
+
# time, and when it was checked. A session watches that one file instead of
|
|
9
|
+
# every session parsing the cache and re-deriving the thresholds.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# meter-watch [--home DIR]
|
|
13
|
+
# meter-watch --install-timer [--home DIR]
|
|
14
|
+
#
|
|
15
|
+
# --install-timer writes a LaunchAgent plist under --home (default
|
|
16
|
+
# ~/.plastic when --home is not given) that runs this tick every 20
|
|
17
|
+
# minutes. It never calls launchctl; load it yourself with the command it
|
|
18
|
+
# prints. The Plastic installer never calls --install-timer on its own.
|
|
19
|
+
#
|
|
20
|
+
# Exit codes: 0 reported (unavailable included); 2 usage error.
|
|
21
|
+
|
|
22
|
+
require "json"
|
|
23
|
+
require_relative "lib/meter_watch"
|
|
24
|
+
|
|
25
|
+
def usage_abort(message)
|
|
26
|
+
warn "meter-watch: #{message}"
|
|
27
|
+
warn "Usage: meter-watch [--home DIR] [--install-timer]"
|
|
28
|
+
exit 2
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
home = File.join(Dir.home, ".plastic")
|
|
32
|
+
install_timer = false
|
|
33
|
+
|
|
34
|
+
argv = ARGV.dup
|
|
35
|
+
until argv.empty?
|
|
36
|
+
case (token = argv.shift)
|
|
37
|
+
when "--home"
|
|
38
|
+
usage_abort("--home requires a value") if argv.empty?
|
|
39
|
+
home = argv.shift
|
|
40
|
+
when "--install-timer"
|
|
41
|
+
install_timer = true
|
|
42
|
+
else
|
|
43
|
+
usage_abort("unknown argument #{token.inspect}")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
home = File.expand_path(home)
|
|
48
|
+
|
|
49
|
+
if install_timer
|
|
50
|
+
plist_path = MeterWatch.install_timer(home: home, script_path: File.expand_path(__FILE__))
|
|
51
|
+
puts "Installed LaunchAgent at #{plist_path}"
|
|
52
|
+
puts "Run `launchctl load #{plist_path}` to activate it."
|
|
53
|
+
exit 0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
state = MeterWatch.new(home: home).tick
|
|
57
|
+
puts JSON.generate(state)
|
|
@@ -17,7 +17,7 @@ GLOBAL_ROOT="$HOME/.plastic"
|
|
|
17
17
|
LOCAL_PLASTIC="$PROJECT_PATH/.plastic"
|
|
18
18
|
|
|
19
19
|
if [ ! -d "$GLOBAL_ROOT" ]; then
|
|
20
|
-
echo "Error: Global Plastic not installed. Run
|
|
20
|
+
echo "Error: Global Plastic not installed. Run 'plastic install' first." >&2
|
|
21
21
|
exit 1
|
|
22
22
|
fi
|
|
23
23
|
|
package/scripts/new-intent
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
# exactly one line, `created <dir>` or `joined <dir>`; exit 0 either way,
|
|
36
36
|
# exit 1 on bad input or failed self-validation.
|
|
37
37
|
|
|
38
|
+
require_relative "lib/store_layout"
|
|
38
39
|
require "fileutils"
|
|
39
40
|
require "date"
|
|
40
41
|
require_relative "lib/savepoint"
|
|
@@ -167,17 +168,7 @@ end
|
|
|
167
168
|
# [nil, "global"] only if the layout is unrecognized, in which case Links
|
|
168
169
|
# projection falls back to single-store resolution rooted at this store.
|
|
169
170
|
def store_context(store)
|
|
170
|
-
|
|
171
|
-
parent = File.dirname(store) # `<home>` or `<home>/projects/<slug>`
|
|
172
|
-
if File.basename(store) == "store" && File.basename(File.dirname(parent)) == "projects"
|
|
173
|
-
slug = File.basename(parent)
|
|
174
|
-
home = File.dirname(File.dirname(parent)) # strip projects/<slug>
|
|
175
|
-
[home, "project:#{slug}"]
|
|
176
|
-
elsif File.basename(store) == "store"
|
|
177
|
-
[parent, "global"] # `<home>/store`
|
|
178
|
-
else
|
|
179
|
-
[parent, "global"]
|
|
180
|
-
end
|
|
171
|
+
Plastic::StoreLayout.home_and_scope(store)
|
|
181
172
|
end
|
|
182
173
|
|
|
183
174
|
# The in-scope stores under `plastic_home`, each { key:, store: }. Delegates to
|
|
@@ -283,7 +274,7 @@ def run_tmp(opts)
|
|
|
283
274
|
abort "new-intent: --sources is not valid with --tmp" unless opts[:sources].empty?
|
|
284
275
|
abort "new-intent: --tags is not valid with --tmp" unless opts[:tags].empty?
|
|
285
276
|
|
|
286
|
-
store = opts[:store] ? expand(opts[:store]) : expand(
|
|
277
|
+
store = opts[:store] ? expand(opts[:store]) : Plastic::StoreLayout.global_store(expand(ENV.fetch("PLASTIC_HOME", "~/.plastic")))
|
|
287
278
|
templates = opts[:templates] ? expand(opts[:templates]) : default_templates_dir
|
|
288
279
|
abort "new-intent: templates dir not found: #{templates}" unless Dir.exist?(templates)
|
|
289
280
|
|
|
@@ -362,7 +353,7 @@ def main(argv)
|
|
|
362
353
|
# 4a. I1 reciprocity: write the child's id into EACH source intent's frontmatter
|
|
363
354
|
# `chain` (the formative-reciprocity backlink), for BOTH the `--parent` and the
|
|
364
355
|
# `--sources` path. `sources` is the redundant-explicit set from step 3 (it already
|
|
365
|
-
#
|
|
356
|
+
# includes `--parent`). Collect the touched source files so their `## Links` can be
|
|
366
357
|
# re-projected once the chain edges are on disk.
|
|
367
358
|
source_files = []
|
|
368
359
|
sources.each do |src_id|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require_relative "lib/node_input"
|
|
6
|
+
require_relative "lib/savepoint"
|
|
7
|
+
|
|
8
|
+
# node-input - the CLI over NodeInput (intent 338, G5). Builds one node's
|
|
9
|
+
# whole input from disk and writes it to attempts/<node>--a<N>.input (or
|
|
10
|
+
# --out), printing a parsable summary and the exact node-transition running
|
|
11
|
+
# command intent 340's runner should record.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# node-input <intent_dir> --node <id> [--budget N] [--hop-tokens N]
|
|
15
|
+
# [--holder H] [--expires E] [--model M] [--attempt N]
|
|
16
|
+
# [--out PATH] [--force]
|
|
17
|
+
#
|
|
18
|
+
# Exit codes (spec D17, shared with node-transition's family):
|
|
19
|
+
# 0 - the node input was written (or the identical bytes already existed)
|
|
20
|
+
# 2 - usage: not an intent directory, missing --node, or an unknown node
|
|
21
|
+
# 3 - an unreadable, cyclic or unparsable graph, node file, or record
|
|
22
|
+
# 4 - overflow past the third cut; nothing was written
|
|
23
|
+
# 5 - an existing attempt file whose bytes differ; nothing was written
|
|
24
|
+
module NodeInputCLI
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def usage
|
|
28
|
+
warn "Usage: node-input <intent_dir> --node <id> [--budget N] [--hop-tokens N] " \
|
|
29
|
+
"[--holder H] [--expires E] [--model M] [--attempt N] [--out PATH] [--force]"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def intent_directory?(dir)
|
|
33
|
+
dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def opt(args, name)
|
|
37
|
+
(i = args.index(name)) && args[i + 1]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def main(argv)
|
|
41
|
+
args = argv.dup
|
|
42
|
+
intent_dir_arg = args.shift
|
|
43
|
+
|
|
44
|
+
unless intent_directory?(intent_dir_arg && File.expand_path(intent_dir_arg))
|
|
45
|
+
warn "node-input: #{intent_dir_arg.inspect} is not an intent directory"
|
|
46
|
+
usage
|
|
47
|
+
exit 2
|
|
48
|
+
end
|
|
49
|
+
intent_dir = File.expand_path(intent_dir_arg)
|
|
50
|
+
|
|
51
|
+
node = opt(args, "--node")
|
|
52
|
+
if node.to_s.strip.empty?
|
|
53
|
+
warn "node-input: --node is required"
|
|
54
|
+
usage
|
|
55
|
+
exit 2
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
budget = opt(args, "--budget")
|
|
59
|
+
hop_tokens = opt(args, "--hop-tokens")
|
|
60
|
+
attempt = opt(args, "--attempt")
|
|
61
|
+
out = opt(args, "--out")
|
|
62
|
+
holder = opt(args, "--holder")
|
|
63
|
+
expires = opt(args, "--expires")
|
|
64
|
+
model = opt(args, "--model")
|
|
65
|
+
force = args.include?("--force")
|
|
66
|
+
|
|
67
|
+
result = NodeInput.build(
|
|
68
|
+
intent_dir: intent_dir,
|
|
69
|
+
node: node,
|
|
70
|
+
budget_tokens: budget ? budget.to_i : NodeInput::DEFAULT_BUDGET_TOKENS,
|
|
71
|
+
hop_tokens: hop_tokens ? hop_tokens.to_i : NodeInput::DEFAULT_HOP_TOKENS,
|
|
72
|
+
holder: holder,
|
|
73
|
+
expires: expires,
|
|
74
|
+
model: model,
|
|
75
|
+
attempt: attempt ? attempt.to_i : nil,
|
|
76
|
+
out: out,
|
|
77
|
+
force: force,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if result[:ok]
|
|
81
|
+
puts NodeInput.summary_line(result)
|
|
82
|
+
puts result[:running_command]
|
|
83
|
+
exit 0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
Array(result[:errors]).each { |e| warn "node-input: #{e}" }
|
|
87
|
+
puts result[:needs_decision_command] if result[:needs_decision_command]
|
|
88
|
+
exit result[:exit_code]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
NodeInputCLI.main(ARGV) if $PROGRAM_NAME == __FILE__
|
package/scripts/node-run
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require "digest"
|
|
6
|
+
require "time"
|
|
7
|
+
require "yaml"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
require_relative "lib/savepoint"
|
|
10
|
+
require_relative "lib/node_ledger"
|
|
11
|
+
require_relative "lib/node_input"
|
|
12
|
+
require_relative "lib/node_worktree"
|
|
13
|
+
require_relative "lib/node_file"
|
|
14
|
+
require_relative "lib/ready_set"
|
|
15
|
+
require_relative "lib/runner_core"
|
|
16
|
+
require_relative "lib/codex_adapter"
|
|
17
|
+
|
|
18
|
+
# node-run - the Codex leg of one node's whole attempt (intent 340b, G7c, n6).
|
|
19
|
+
#
|
|
20
|
+
# On Claude Code a subagent dispatch IS the executor; on Codex the executor
|
|
21
|
+
# is a subprocess, so this script owns the timing of a whole node: it reads
|
|
22
|
+
# the node's live `running` line, resolves the node input that line names,
|
|
23
|
+
# builds the `codex exec` argv for the node's kind (CodexAdapter), runs it
|
|
24
|
+
# in the node's worktree with the node input on stdin, reads the executor's
|
|
25
|
+
# final message, and writes it to the attempt's own return path.
|
|
26
|
+
#
|
|
27
|
+
# It writes NO ledger transition, on any path - success, refusal or error
|
|
28
|
+
# alike. That is the whole point: exactly one thing writes `done` (the
|
|
29
|
+
# runner's own six-check absorb gate), and a second writer here would throw
|
|
30
|
+
# that guarantee away. The caller absorbs with `runner step --return
|
|
31
|
+
# ID=PATH` through the same six checks every other return goes through.
|
|
32
|
+
#
|
|
33
|
+
# Usage:
|
|
34
|
+
# node-run <intent_dir> --node <id> [--session SID] [--now ISO8601]
|
|
35
|
+
# [--timeout-seconds N]
|
|
36
|
+
#
|
|
37
|
+
# Exit codes: 0 - a return file was written (whatever its content actually
|
|
38
|
+
# says; that judgment belongs to the absorb gate, not this script). Every
|
|
39
|
+
# other code is a REFUSAL: nothing is written, node-run never even reached
|
|
40
|
+
# `codex exec`.
|
|
41
|
+
# 2 - usage: not an intent directory, or missing --node
|
|
42
|
+
# 3 - the node has no live `running` line
|
|
43
|
+
# 4 - the running line is held by another session
|
|
44
|
+
# 5 - the node input the running line names is missing from disk
|
|
45
|
+
# 6 - the node input on disk does not hash to the running line's input=
|
|
46
|
+
# 7 - no worktree could be resolved to run in
|
|
47
|
+
module NodeRunCLI
|
|
48
|
+
module_function
|
|
49
|
+
|
|
50
|
+
def usage
|
|
51
|
+
warn "Usage: node-run <intent_dir> --node <id> [--session SID] [--now ISO8601] " \
|
|
52
|
+
"[--timeout-seconds N]"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def opt(args, name)
|
|
56
|
+
(i = args.index(name)) && args[i + 1]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def intent_directory?(dir)
|
|
60
|
+
dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# <intent_dir>/attempts/<node>--a<N>.return.yml (spec): beside the node input,
|
|
64
|
+
# one per attempt, the same treatment input files already get - never
|
|
65
|
+
# reused across attempts, so a second attempt's return can never be
|
|
66
|
+
# mistaken for the first's.
|
|
67
|
+
def return_path(intent_dir, node, attempt)
|
|
68
|
+
File.join(intent_dir, "attempts", "#{node}--a#{attempt}.return.yml")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The node's own declared kind, read straight off its node file - "work"
|
|
72
|
+
# when the file cannot be found or parsed at all (mirroring RunnerPolicy's
|
|
73
|
+
# own unknown-kind fallback), but the LITERAL string it declares
|
|
74
|
+
# otherwise, even an invalid one (matrix row 6.10): CodexAdapter's own
|
|
75
|
+
# kind table only distinguishes "read-only" (verify, research) from
|
|
76
|
+
# everything else, so an unrecognized kind still gets the widest, safest
|
|
77
|
+
# treatment rather than no treatment at all.
|
|
78
|
+
def resolve_kind(intent_dir, node)
|
|
79
|
+
path = ReadySet.find_node_path(intent_dir, node)
|
|
80
|
+
return "work" unless path
|
|
81
|
+
|
|
82
|
+
parsed = NodeFile.parse(path)
|
|
83
|
+
kind = parsed[:kind].to_s.strip
|
|
84
|
+
kind.empty? ? "work" : kind
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The directory `codex exec -C` runs in: a `work` node's own worktree when
|
|
88
|
+
# one is actually provisioned on disk, else the shared intent worktree -
|
|
89
|
+
# verify and research nodes never get a worktree of their own (327 D6,
|
|
90
|
+
# D29), so the only place they could ever leave a diff is the intent
|
|
91
|
+
# worktree itself, exactly where NodeWorktree.changed_paths already looks
|
|
92
|
+
# for one.
|
|
93
|
+
def node_target_dir(context, node:, kind:)
|
|
94
|
+
if NodeWorktree::WORKTREE_KINDS.include?(kind.to_s)
|
|
95
|
+
p = NodeWorktree.paths(context, node: node)
|
|
96
|
+
return p["path"] if p["path"] && Dir.exist?(p["path"])
|
|
97
|
+
end
|
|
98
|
+
context.worktree
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A synthetic, deliberately non-YAML-mapping line the absorb gate reads as
|
|
102
|
+
# unparsable (spec Approach: "a return file the gate reads as
|
|
103
|
+
# unparsable"), covering every subprocess failure the same way: a nonzero
|
|
104
|
+
# exit, a timeout, an empty message file, and a message with no YAML
|
|
105
|
+
# document either way. Carries no colon-shaped `key: value` text on
|
|
106
|
+
# purpose, so it can never accidentally parse as a YAML mapping that
|
|
107
|
+
# happens to satisfy NodeReturn's own schema.
|
|
108
|
+
def unparsable_body(node, result)
|
|
109
|
+
reason =
|
|
110
|
+
if result[:timed_out]
|
|
111
|
+
"node-run - codex exec for #{node} timed out with no usable return"
|
|
112
|
+
elsif !result[:exit_code].nil? && result[:exit_code] != 0
|
|
113
|
+
"node-run - codex exec for #{node} exited #{result[:exit_code]} with no usable return"
|
|
114
|
+
else
|
|
115
|
+
"node-run - codex exec for #{node} produced no parsable YAML document on either " \
|
|
116
|
+
"--output-last-message or stdout"
|
|
117
|
+
end
|
|
118
|
+
"#{reason}\n"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def main(argv)
|
|
122
|
+
args = argv.dup
|
|
123
|
+
intent_dir_arg = args.shift
|
|
124
|
+
|
|
125
|
+
unless intent_directory?(intent_dir_arg && File.expand_path(intent_dir_arg))
|
|
126
|
+
warn "node-run: #{intent_dir_arg.inspect} is not an intent directory"
|
|
127
|
+
usage
|
|
128
|
+
exit 2
|
|
129
|
+
end
|
|
130
|
+
intent_dir = File.expand_path(intent_dir_arg)
|
|
131
|
+
|
|
132
|
+
node = opt(args, "--node")
|
|
133
|
+
if node.to_s.strip.empty?
|
|
134
|
+
warn "node-run: --node is required"
|
|
135
|
+
usage
|
|
136
|
+
exit 2
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
session_flag = opt(args, "--session")
|
|
140
|
+
now_flag = opt(args, "--now")
|
|
141
|
+
timeout_flag = opt(args, "--timeout-seconds")
|
|
142
|
+
|
|
143
|
+
begin
|
|
144
|
+
now_flag ? Time.iso8601(now_flag) : Time.now
|
|
145
|
+
rescue ArgumentError
|
|
146
|
+
warn "node-run: --now must be ISO 8601"
|
|
147
|
+
exit 2
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
context = RunnerCore.context(intent_dir: intent_dir, session: session_flag,
|
|
151
|
+
env: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
152
|
+
|
|
153
|
+
savepoint_path = File.join(intent_dir, "savepoint.md")
|
|
154
|
+
current_status = NodeLedger.status_for(savepoint_path, node)
|
|
155
|
+
running_entry = NodeLedger.last_running(savepoint_path, node)
|
|
156
|
+
unless current_status == "running" && running_entry
|
|
157
|
+
warn "node-run: #{node} has no live running line (state is #{current_status})"
|
|
158
|
+
exit 3
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
fields = running_entry[:fields] || {}
|
|
162
|
+
holder = fields["holder"]
|
|
163
|
+
if context.session.nil? || context.session.to_s != holder.to_s
|
|
164
|
+
warn "node-run: #{node}'s running line is held by #{holder.inspect}, not this session"
|
|
165
|
+
exit 4
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
kind = resolve_kind(intent_dir, node)
|
|
169
|
+
|
|
170
|
+
# matrix row 6.11: the attempt number comes from NodeInput's own
|
|
171
|
+
# counter, never a hand count. `lease_flag_given: false` because the
|
|
172
|
+
# running line this attempt is acting on ALREADY exists on disk (it is
|
|
173
|
+
# what we just read above) - counting it a second time would double it
|
|
174
|
+
# (the same count, plus one, that produced it in the first place when
|
|
175
|
+
# the node input was originally built).
|
|
176
|
+
attempt = NodeInput.compute_attempt_number(intent_dir: intent_dir, node: node, lease_flag_given: false)
|
|
177
|
+
input_path = NodeInput.input_path(intent_dir: intent_dir, node: node, attempt: attempt)
|
|
178
|
+
|
|
179
|
+
unless File.exist?(input_path)
|
|
180
|
+
warn "node-run: #{node}'s node input is missing at #{input_path}"
|
|
181
|
+
exit 5
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
input_bytes = File.binread(input_path)
|
|
185
|
+
actual_sha = Digest::SHA256.hexdigest(input_bytes)[0, 12]
|
|
186
|
+
expected_sha = fields["input"]
|
|
187
|
+
unless expected_sha && actual_sha == expected_sha
|
|
188
|
+
warn "node-run: #{node}'s node input at #{input_path} does not hash to input=#{expected_sha.inspect} " \
|
|
189
|
+
"(computed #{actual_sha})"
|
|
190
|
+
exit 6
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
worktree = node_target_dir(context, node: node, kind: kind)
|
|
194
|
+
if worktree.nil? || !Dir.exist?(worktree)
|
|
195
|
+
warn "node-run: #{node} has no resolvable worktree to run in"
|
|
196
|
+
exit 7
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
out_path = return_path(intent_dir, node, attempt)
|
|
200
|
+
FileUtils.mkdir_p(File.dirname(out_path))
|
|
201
|
+
message_path = "#{out_path}.msg"
|
|
202
|
+
|
|
203
|
+
timeout_seconds = timeout_flag ? timeout_flag.to_i : CodexAdapter.timeout_seconds(kind)
|
|
204
|
+
codex_argv = CodexAdapter.build_argv(kind: kind, worktree: worktree, output_last_message: message_path,
|
|
205
|
+
model: fields["model"], effort: fields["effort"])
|
|
206
|
+
|
|
207
|
+
result = CodexAdapter.execute(codex_argv, stdin_data: input_bytes, timeout_seconds: timeout_seconds,
|
|
208
|
+
output_last_message_path: message_path)
|
|
209
|
+
|
|
210
|
+
# matrix rows 6.20-6.22: a nonzero exit or a timeout is authoritative -
|
|
211
|
+
# any message text the child managed to leave behind is never trusted
|
|
212
|
+
# once either has happened. Only a clean exit with no usable message at
|
|
213
|
+
# all falls to "no parsable YAML document".
|
|
214
|
+
failed = result[:timed_out] || (!result[:exit_code].nil? && result[:exit_code] != 0)
|
|
215
|
+
body = failed ? unparsable_body(node, result) : (result[:message] || unparsable_body(node, result))
|
|
216
|
+
|
|
217
|
+
File.write(out_path, body)
|
|
218
|
+
File.delete(message_path) if File.exist?(message_path)
|
|
219
|
+
|
|
220
|
+
puts out_path
|
|
221
|
+
exit 0
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
NodeRunCLI.main(ARGV) if $PROGRAM_NAME == __FILE__
|