@zalom/plastic 2.0.0-alpha.9 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLASTIC.md +13 -136
- package/README.md +357 -133
- package/agents/plastic-enforcer.md +21 -16
- package/agents/plastic-executor.md +15 -4
- package/agents/plastic-node-research.md +30 -0
- package/agents/plastic-node-verify.md +28 -0
- package/agents/plastic-node-work.md +33 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/assets/plastic-logo.svg +1 -0
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +43 -6
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +8 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/test +24 -4
- package/bin/verify-change +345 -0
- package/config_asks.yml +4 -4
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
- package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
- package/docs/help/completion-and-done.md +53 -0
- package/docs/help/human-report-contract.md +152 -0
- package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
- package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +11 -13
- package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
- package/{skills/conventions/references → docs/help}/roadmaps.md +5 -4
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/hooks/call-budget +4 -0
- package/hooks/hooks.json +24 -0
- package/hooks/message-display +55 -2
- package/hooks/session-start +5 -1
- package/hooks/statusline +32 -27
- package/hooks/stop +5 -0
- package/package.json +5 -3
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +267 -16
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +680 -39
- package/scripts/end-intent +378 -56
- package/scripts/exec-worktree +5 -5
- package/scripts/file-session-intent +2 -1
- package/scripts/graph-measure +249 -0
- package/scripts/hook-call-budget +222 -0
- package/scripts/hook-capture +24 -123
- package/scripts/hook-close +2 -1
- package/scripts/hook-message-display +7 -0
- package/scripts/hook-record +24 -16
- package/scripts/hook-savepoint +27 -3
- package/scripts/hook-session-start +359 -321
- package/scripts/hook-stop +58 -0
- package/scripts/index-projection +74 -0
- package/scripts/insight-append +17 -4
- package/scripts/install.rb +9 -7
- package/scripts/lib/action_graph_shim.rb +279 -0
- package/scripts/lib/active_delivery.rb +105 -0
- package/scripts/lib/agent_models.rb +63 -25
- package/scripts/lib/arm.rb +74 -91
- package/scripts/lib/atomic_write.rb +31 -0
- package/scripts/lib/backup.rb +65 -0
- package/scripts/lib/cli/command.rb +92 -0
- package/scripts/lib/cli/commands/auto.rb +18 -0
- package/scripts/lib/cli/commands/auto_brief.rb +44 -0
- package/scripts/lib/cli/commands/auto_lock.rb +111 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +68 -0
- package/scripts/lib/cli/commands/backup.rb +43 -0
- package/scripts/lib/cli/commands/checkout.rb +25 -0
- package/scripts/lib/cli/commands/continue.rb +66 -0
- package/scripts/lib/cli/commands/doctor.rb +81 -0
- package/scripts/lib/cli/commands/feedback.rb +40 -0
- package/scripts/lib/cli/commands/help.rb +69 -0
- package/scripts/lib/cli/commands/hook.rb +32 -0
- package/scripts/lib/cli/commands/index.rb +23 -0
- package/scripts/lib/cli/commands/install.rb +21 -0
- package/scripts/lib/cli/commands/installer_verb.rb +37 -0
- package/scripts/lib/cli/commands/intent.rb +19 -0
- package/scripts/lib/cli/commands/intent_answer.rb +37 -0
- package/scripts/lib/cli/commands/intent_command.rb +58 -0
- package/scripts/lib/cli/commands/intent_end.rb +75 -0
- package/scripts/lib/cli/commands/intent_new.rb +62 -0
- package/scripts/lib/cli/commands/intent_note.rb +43 -0
- package/scripts/lib/cli/commands/intent_rule.rb +36 -0
- package/scripts/lib/cli/commands/intent_show.rb +32 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +79 -0
- package/scripts/lib/cli/commands/intent_verify.rb +26 -0
- package/scripts/lib/cli/commands/migrate.rb +16 -0
- package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
- package/scripts/lib/cli/commands/next.rb +54 -0
- package/scripts/lib/cli/commands/project.rb +19 -0
- package/scripts/lib/cli/commands/project_links.rb +46 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +95 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +35 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +77 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +65 -0
- package/scripts/lib/cli/commands/rollback.rb +20 -0
- package/scripts/lib/cli/commands/search.rb +60 -0
- package/scripts/lib/cli/commands/session.rb +18 -0
- package/scripts/lib/cli/commands/session_commit.rb +42 -0
- package/scripts/lib/cli/commands/session_handoff.rb +34 -0
- package/scripts/lib/cli/commands/session_summary.rb +35 -0
- package/scripts/lib/cli/commands/status.rb +68 -0
- package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
- package/scripts/lib/cli/commands/sync.rb +55 -0
- package/scripts/lib/cli/commands/uninstall.rb +20 -0
- package/scripts/lib/cli/commands/update.rb +20 -0
- package/scripts/lib/cli/commands/version.rb +53 -0
- package/scripts/lib/cli/frontier.rb +86 -0
- package/scripts/lib/cli/intent_progress.rb +36 -0
- package/scripts/lib/cli/legacy.rb +80 -0
- package/scripts/lib/cli/output.rb +137 -0
- package/scripts/lib/cli/scope.rb +134 -0
- package/scripts/lib/cli/table.rb +65 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +198 -0
- package/scripts/lib/compact_instructions.rb +13 -5
- package/scripts/lib/core_integrity.rb +71 -0
- package/scripts/lib/dashboard_screen.rb +40 -0
- package/scripts/lib/data_boundary.rb +132 -0
- package/scripts/lib/day_summary.rb +23 -14
- package/scripts/lib/doctor_core.rb +114 -38
- package/scripts/lib/doctor_session_ledger.rb +5 -53
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_edges.rb +137 -0
- package/scripts/lib/graph_file.rb +246 -0
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +409 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +413 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +98 -0
- package/scripts/lib/guarded_append.rb +155 -0
- package/scripts/lib/handoff.rb +40 -13
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +27 -3
- package/scripts/lib/hook_replay.rb +235 -0
- package/scripts/lib/index_entry.rb +62 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +543 -71
- package/scripts/lib/intent_screen.rb +4 -4
- package/scripts/lib/intent_screen_ansi.rb +73 -12
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/lock.rb +10 -11
- package/scripts/lib/message_display.rb +350 -54
- package/scripts/lib/meter_watch.rb +185 -0
- package/scripts/lib/node_file.rb +234 -0
- package/scripts/lib/node_ids.rb +99 -0
- package/scripts/lib/node_input.rb +921 -0
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +386 -0
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/node_return.rb +204 -0
- package/scripts/lib/node_worktree.rb +337 -0
- package/scripts/lib/outcome_report.rb +440 -0
- package/scripts/lib/preflight.rb +4 -6
- package/scripts/lib/project_config.rb +46 -0
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/ready_set.rb +462 -0
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +18 -0
- package/scripts/lib/report_screen.rb +1371 -47
- package/scripts/lib/rlm/corpus.rb +13 -0
- package/scripts/lib/rlm/probe.rb +29 -0
- package/scripts/lib/rlm/query.rb +22 -0
- package/scripts/lib/roadmap_graph.rb +210 -0
- package/scripts/lib/roadmap_migration.rb +95 -0
- package/scripts/lib/roadmap_queue.rb +170 -12
- package/scripts/lib/roadmap_render.rb +150 -0
- package/scripts/lib/roadmap_savepoint.rb +64 -14
- package/scripts/lib/runner_absorb.rb +703 -0
- package/scripts/lib/runner_answer.rb +206 -0
- package/scripts/lib/runner_core.rb +194 -0
- package/scripts/lib/runner_dispatch.rb +526 -0
- package/scripts/lib/runner_policy.rb +191 -0
- package/scripts/lib/runner_proposals.rb +275 -0
- package/scripts/lib/runner_rewind.rb +201 -0
- package/scripts/lib/runner_sweep.rb +231 -0
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +141 -19
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/screen_paint.rb +365 -28
- package/scripts/lib/screens/dashboard.rb +20 -0
- package/scripts/lib/screens/plan.rb +18 -0
- package/scripts/lib/screens/roadmap.rb +15 -0
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +67 -19
- package/scripts/lib/session_ledger.rb +48 -4
- package/scripts/lib/session_usage.rb +190 -0
- package/scripts/lib/sqlite.rb +22 -0
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/store_discovery.rb +7 -6
- package/scripts/lib/store_layout.rb +54 -0
- package/scripts/lib/store_provisioning.rb +2 -1
- package/scripts/lib/store_sync.rb +85 -0
- package/scripts/lib/stores_move.rb +93 -0
- package/scripts/lib/untouched_scaffold.rb +51 -0
- package/scripts/lib/verify_intent.rb +36 -8
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/lib/worktree.rb +27 -32
- package/scripts/lib/worktree_sweep.rb +6 -5
- package/scripts/link-suggest +2 -1
- package/scripts/meter-watch +57 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +4 -13
- package/scripts/node-input +92 -0
- package/scripts/node-run +225 -0
- package/scripts/node-transition +291 -0
- package/scripts/outcome-report +74 -0
- package/scripts/plastic-lock +65 -60
- package/scripts/project-links +6 -21
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +53 -9
- package/scripts/ready-set +126 -0
- package/scripts/release-check +123 -0
- package/scripts/report-screen +177 -16
- package/scripts/roadmap-graph +125 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/rollback.rb +5 -1
- package/scripts/runner +581 -0
- package/scripts/savepoint-note +11 -9
- package/scripts/session-commit +2 -1
- package/scripts/session-usage +56 -0
- package/scripts/skill-lint +115 -6
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +31 -4
- package/scripts/validate-work-graph +39 -0
- package/scripts/verify-intent +3 -2
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +7 -7
- package/templates/config.yml +16 -9
- package/templates/dashboard-screen.md +22 -0
- package/templates/display-fixture.md +21 -0
- package/templates/graph.md +16 -0
- package/templates/index.md +1 -1
- package/templates/intent-screen.md +1 -1
- package/templates/node-decision.md +11 -0
- package/templates/node-research.md +13 -0
- package/templates/node-verify.md +13 -0
- package/templates/node-work.md +22 -0
- package/templates/outcome.md +8 -3
- package/templates/project.yml +1 -1
- package/templates/render.css +10 -0
- package/templates/report-plan.md +15 -0
- package/templates/report-roadmap-delivered.md +10 -0
- package/templates/report-roadmap-plan.md +9 -0
- package/templates/report-roadmap-state.md +9 -0
- package/templates/report-state.md +1 -1
- package/templates/roadmap.md +13 -0
- package/bin/plastic.js +0 -70
- package/scripts/lib/bridge.rb +0 -116
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -296
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -66
- package/skills/auto/references/human-report-contract.md +0 -78
- package/skills/conventions/SKILL.md +0 -29
- package/skills/conventions/references/completion-and-done.md +0 -43
- package/skills/dashboard/SKILL.md +0 -169
- package/skills/dashboard/evals/evals.json +0 -38
- package/skills/dashboard/references/classification.md +0 -22
- package/skills/dashboard/templates/dashboard-global.md +0 -20
- package/skills/dashboard/templates/dashboard-project.md +0 -19
- package/skills/direct/SKILL.md +0 -66
- package/skills/direct/references/request-signals.md +0 -59
- package/skills/doctor/SKILL.md +0 -299
- package/skills/doctor/report.md +0 -102
- package/skills/feedback/SKILL.md +0 -98
- package/skills/feedback/references/transport-and-privacy.md +0 -65
- package/skills/feedback/report.md +0 -36
- package/skills/install/SKILL.md +0 -217
- package/skills/intent-continuing/SKILL.md +0 -154
- package/skills/intent-continuing/references/board-fill.md +0 -43
- package/skills/intent-continuing/references/boarding-matrix.md +0 -34
- package/skills/intent-continuing/references/context-management.md +0 -28
- package/skills/intent-continuing/references/liveness-ranking.md +0 -57
- package/skills/intent-creating/SKILL.md +0 -164
- package/skills/intent-creating/evals/evals.json +0 -72
- package/skills/intent-creating/references/lifecycle.md +0 -81
- package/skills/intent-creating/references/wikilinks.md +0 -8
- package/skills/intent-ending/SKILL.md +0 -176
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -170
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -42
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -130
- package/skills/intent-speccing/evals/evals.json +0 -126
- package/skills/intent-speccing/references/design-principles.md +0 -44
- package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
- package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
- package/skills/project-creating/SKILL.md +0 -162
- package/skills/project-creating/references/hubs-projects.md +0 -55
- package/skills/project-creating/references/project-scaffolding.md +0 -97
- package/skills/releasing/SKILL.md +0 -337
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -66
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -64
- package/skills/roadmap/references/file-format.md +0 -124
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -65
- package/skills/tutorial/evals/evals.json +0 -186
- package/skills/uninstall/SKILL.md +0 -75
- package/skills/update/SKILL.md +0 -126
- /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
- /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require "time"
|
|
6
|
+
require_relative "lib/savepoint"
|
|
7
|
+
require_relative "lib/node_ledger"
|
|
8
|
+
require_relative "lib/ready_set"
|
|
9
|
+
require_relative "lib/guarded_append"
|
|
10
|
+
require_relative "lib/lock"
|
|
11
|
+
require_relative "lib/arm"
|
|
12
|
+
|
|
13
|
+
# node-transition - the CLI over NodeLedger that REFUSES what the graph does not
|
|
14
|
+
# allow (intent 335, G2). An orchestrator can run this by hand today, before the
|
|
15
|
+
# runner (intent 340) exists.
|
|
16
|
+
#
|
|
17
|
+
# Usage:
|
|
18
|
+
# node-transition <intent_dir> --node <id|Intent> --state <state> [--field k=v]...
|
|
19
|
+
# [--comment "..."] [--needs a,b] [--session SID] [--now ISO8601]
|
|
20
|
+
# node-transition report <intent_dir>
|
|
21
|
+
#
|
|
22
|
+
# Exit codes (spec D18, matching the tree: scripts/append-ledger:125 uses 3 for a
|
|
23
|
+
# guard that could not be taken, scripts/end-intent:675 uses 4 for a lock-ownership
|
|
24
|
+
# refusal):
|
|
25
|
+
# 0 - the line was appended
|
|
26
|
+
# 2 - usage: not an intent directory, bad subject, unknown state, missing
|
|
27
|
+
# required field, a tab or newline in a value
|
|
28
|
+
# 3 - the ledger guard was unavailable; nothing was written
|
|
29
|
+
# 4 - running, and the caller does not hold delivery.lock
|
|
30
|
+
# 5 - running, and the subject is not ready
|
|
31
|
+
# 6 - reclaimed, and the subject's last running line has not expired
|
|
32
|
+
module NodeTransition
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# The readiness precondition for `running` (intent 336, G3): ReadySet.ready?
|
|
36
|
+
# applied to `subject` against `edges`/`nodes` already parsed once, before
|
|
37
|
+
# the guard is ever taken (spec: "parsed once, before the guard"). Takes
|
|
38
|
+
# `content` (the ledger's raw text), never a path, so this method can be run
|
|
39
|
+
# twice: once as a cheap unguarded pre-check for a fast, readable refusal,
|
|
40
|
+
# and once - the AUTHORITATIVE decision - as the guard's precondition,
|
|
41
|
+
# evaluated against the exact content GuardedAppend just read under its
|
|
42
|
+
# lock hold (post-execution review row 7.1, carried forward by 336). A
|
|
43
|
+
# path-based re-read here would defeat the guard: two callers could each
|
|
44
|
+
# open their own handle, both see "planned", and both go on to append
|
|
45
|
+
# `running`.
|
|
46
|
+
def running_ready?(content, subject, edges, nodes, caps: ReadySet::DEFAULT_CAPS)
|
|
47
|
+
ReadySet.ready?(content: content, subject: subject, graph: { edges: edges }, nodes: nodes, caps: caps)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# graph.md and nodes/ for `subject`'s readiness (intent 336, G3): a genuinely
|
|
51
|
+
# missing graph.md degrades gracefully to no known needs/kind/files (the
|
|
52
|
+
# same fallback the temporary needs_from_graph reader gave, so a legacy
|
|
53
|
+
# intent with no graph.md still dispatches ad hoc node ids), while a
|
|
54
|
+
# PRESENT but broken graph.md (no ## Graph section, a cycle) is a clean
|
|
55
|
+
# refusal, never a raise - {edges:, nodes:, error:}.
|
|
56
|
+
def resolve_graph_and_nodes(intent_dir)
|
|
57
|
+
graph_path = File.join(intent_dir, "graph.md")
|
|
58
|
+
return { edges: {}, nodes: {}, error: nil } unless File.exist?(graph_path)
|
|
59
|
+
|
|
60
|
+
loaded = ReadySet.load_graph(intent_dir)
|
|
61
|
+
return { edges: loaded[:edges] || {}, nodes: loaded[:nodes] || {}, error: nil } if loaded[:ok]
|
|
62
|
+
|
|
63
|
+
{ edges: {}, nodes: {}, error: loaded[:errors].join("; ") }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Session resolution per spec D16: an explicit --session is authoritative and
|
|
67
|
+
# NEVER falls through (an explicit non-owner is refused outright, never
|
|
68
|
+
# silently granted ownership through a fallback). Without it, try the
|
|
69
|
+
# environment's CLAUDE_CODE_SESSION_ID, then the derived `auto-` key, and keep
|
|
70
|
+
# the first candidate that actually HOLDS the lock (owner or delegate,
|
|
71
|
+
# Lock.holds?) - so the fallback can never grant ownership to a session that
|
|
72
|
+
# does not have it.
|
|
73
|
+
def resolve_owning_session(intent_dir, explicit:, env_session:)
|
|
74
|
+
if explicit && !explicit.to_s.strip.empty?
|
|
75
|
+
return Lock.holds?(intent_dir, session: explicit) ? explicit : nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
store = Arm.store_for(intent_dir)
|
|
79
|
+
intent_id = Arm.intent_id_for(intent_dir)
|
|
80
|
+
candidates = [env_session, Arm.derive_key(store, intent_id)].reject { |c| c.to_s.strip.empty? }
|
|
81
|
+
candidates.find { |c| Lock.holds?(intent_dir, session: c) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def intent_directory?(dir)
|
|
85
|
+
dir && File.directory?(dir) && File.exist?(Savepoint.intent_file(dir))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def valid_subject?(subject)
|
|
89
|
+
subject == Savepoint::INTENT_SUBJECT || subject.to_s.match?(Savepoint::NODE_SUBJECT_RE)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def opt(args, name)
|
|
93
|
+
(i = args.index(name)) && args[i + 1]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def opt_all(args, name)
|
|
97
|
+
args.each_index.select { |i| args[i] == name }.map { |i| args[i + 1] }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_fields(args)
|
|
101
|
+
opt_all(args, "--field").each_with_object({}) do |pair, memo|
|
|
102
|
+
key, value = pair.to_s.split("=", 2)
|
|
103
|
+
memo[key] = value if key && !key.empty?
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def usage
|
|
108
|
+
warn "Usage: node-transition <intent_dir> --node <id|Intent> --state <state> " \
|
|
109
|
+
"[--field k=v]... [--comment \"...\"] [--needs a,b] [--session SID] [--now ISO8601]"
|
|
110
|
+
warn " node-transition report <intent_dir>"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def run_report(intent_dir)
|
|
114
|
+
unless intent_directory?(intent_dir)
|
|
115
|
+
warn "node-transition: #{intent_dir.inspect} is not an intent directory"
|
|
116
|
+
usage
|
|
117
|
+
exit 2
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
savepoint_path = File.join(intent_dir, "savepoint.md")
|
|
121
|
+
anomalies = NodeLedger.anomalies(savepoint_path)
|
|
122
|
+
if anomalies.empty?
|
|
123
|
+
puts "node-transition report: no torn or unattributed transition lines"
|
|
124
|
+
else
|
|
125
|
+
anomalies.each { |a| puts "#{a[:reason]}: #{a[:line]}" }
|
|
126
|
+
end
|
|
127
|
+
exit 0
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def main(argv)
|
|
131
|
+
args = argv.dup
|
|
132
|
+
head = args.shift
|
|
133
|
+
|
|
134
|
+
if head == "report"
|
|
135
|
+
report_dir_arg = args.shift
|
|
136
|
+
run_report(report_dir_arg && File.expand_path(report_dir_arg))
|
|
137
|
+
return
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
intent_dir_arg = head
|
|
141
|
+
unless intent_directory?(intent_dir_arg && File.expand_path(intent_dir_arg))
|
|
142
|
+
warn "node-transition: #{intent_dir_arg.inspect} is not an intent directory"
|
|
143
|
+
usage
|
|
144
|
+
exit 2
|
|
145
|
+
end
|
|
146
|
+
intent_dir = File.expand_path(intent_dir_arg)
|
|
147
|
+
|
|
148
|
+
subject = opt(args, "--node")
|
|
149
|
+
state = opt(args, "--state")
|
|
150
|
+
comment = opt(args, "--comment")
|
|
151
|
+
needs_flag = opt(args, "--needs")
|
|
152
|
+
session_flag = opt(args, "--session")
|
|
153
|
+
now_flag = opt(args, "--now")
|
|
154
|
+
fields = parse_fields(args)
|
|
155
|
+
|
|
156
|
+
if subject.nil? || state.nil?
|
|
157
|
+
warn "node-transition: --node and --state are required"
|
|
158
|
+
usage
|
|
159
|
+
exit 2
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
unless valid_subject?(subject)
|
|
163
|
+
warn "node-transition: #{subject.inspect} is neither #{Savepoint::INTENT_SUBJECT} nor a valid node id"
|
|
164
|
+
exit 2
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
unless NodeLedger::STATES.include?(state)
|
|
168
|
+
warn "node-transition: unknown state #{state.inspect}"
|
|
169
|
+
exit 2
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
now = begin
|
|
173
|
+
now_flag ? Time.iso8601(now_flag) : Time.now
|
|
174
|
+
rescue ArgumentError
|
|
175
|
+
warn "node-transition: --now must be ISO 8601"
|
|
176
|
+
exit 2
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
begin
|
|
180
|
+
fields.each_value { |v| NodeLedger.normalize_value(v) }
|
|
181
|
+
NodeLedger.normalize_value(comment) if comment
|
|
182
|
+
rescue ArgumentError => e
|
|
183
|
+
warn "node-transition: #{e.message}"
|
|
184
|
+
exit 2
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
missing = NodeLedger.missing_fields(state, fields)
|
|
188
|
+
unless missing.empty?
|
|
189
|
+
warn "node-transition: state #{state} requires #{missing.join(', ')}"
|
|
190
|
+
exit 2
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
savepoint_path = File.join(intent_dir, "savepoint.md")
|
|
194
|
+
precondition = nil
|
|
195
|
+
|
|
196
|
+
case state
|
|
197
|
+
when "running"
|
|
198
|
+
owning_session = resolve_owning_session(intent_dir, explicit: session_flag,
|
|
199
|
+
env_session: ENV["CLAUDE_CODE_SESSION_ID"])
|
|
200
|
+
unless owning_session
|
|
201
|
+
warn "node-transition: the caller does not hold delivery.lock for #{intent_dir}"
|
|
202
|
+
exit 4
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
if subject == Savepoint::INTENT_SUBJECT
|
|
206
|
+
# The node readiness rule (four conditions over the work graph) is
|
|
207
|
+
# scoped to node subjects; the intent-scope subject is never gated by
|
|
208
|
+
# it, so a legacy intent with no graph.md at all can still write an
|
|
209
|
+
# Intent line (spec Acceptance Criteria; matrix "Transition the
|
|
210
|
+
# Intent subject").
|
|
211
|
+
precondition = nil
|
|
212
|
+
else
|
|
213
|
+
resolved = resolve_graph_and_nodes(intent_dir)
|
|
214
|
+
if resolved[:error]
|
|
215
|
+
warn "node-transition: #{resolved[:error]}"
|
|
216
|
+
exit 5
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
needs_override = needs_flag ? needs_flag.split(",").map(&:strip).reject(&:empty?) : nil
|
|
220
|
+
edges = needs_override ? resolved[:edges].merge(subject => needs_override) : resolved[:edges]
|
|
221
|
+
nodes = resolved[:nodes]
|
|
222
|
+
|
|
223
|
+
# Fast, unguarded pre-check: a cheap, readable refusal for the common
|
|
224
|
+
# case, but NEVER the authoritative decision (post-execution review
|
|
225
|
+
# row 7.1, carried forward by 336). The authoritative check is the
|
|
226
|
+
# `precondition` below, run inside GuardedAppend's lock hold against
|
|
227
|
+
# the content it actually read. `edges` and `nodes` are parsed once,
|
|
228
|
+
# above, before the guard is ever taken (spec D1).
|
|
229
|
+
precheck_content = File.exist?(savepoint_path) ? File.read(savepoint_path) : ""
|
|
230
|
+
result = running_ready?(precheck_content, subject, edges, nodes)
|
|
231
|
+
unless result[:ready]
|
|
232
|
+
warn "node-transition: #{result[:blockers].join('; ')}"
|
|
233
|
+
exit 5
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
precondition = ->(content) { running_ready?(content, subject, edges, nodes)[:ready] }
|
|
237
|
+
end
|
|
238
|
+
when "reclaimed"
|
|
239
|
+
running_entry = NodeLedger.last_running(savepoint_path, subject)
|
|
240
|
+
unless running_entry
|
|
241
|
+
warn "node-transition: #{subject} has no running line to reclaim"
|
|
242
|
+
exit 6
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Post-execution review row 7.4: expiry alone is not enough. A `done`
|
|
246
|
+
# node whose last `running` line's expires= happens to be in the past
|
|
247
|
+
# must not revert to `planned` and become re-dispatchable - reclaim is
|
|
248
|
+
# deliberately lock-free (spec D43), so this status check is the only
|
|
249
|
+
# thing that stops it. Row 7.5 pins that a genuinely running, expired
|
|
250
|
+
# subject is still reclaimable, so the crash sweep keeps working.
|
|
251
|
+
current_status = NodeLedger.status_for(savepoint_path, subject)
|
|
252
|
+
unless current_status == "running"
|
|
253
|
+
warn "node-transition: #{subject} is #{current_status}, not running; nothing to reclaim"
|
|
254
|
+
exit 6
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
expires_raw = (running_entry[:fields] || {})["expires"]
|
|
258
|
+
expiry_time = begin
|
|
259
|
+
expires_raw && Time.iso8601(expires_raw)
|
|
260
|
+
rescue ArgumentError
|
|
261
|
+
nil
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
unless expiry_time && now > expiry_time
|
|
265
|
+
warn "node-transition: #{subject}'s running line has not expired yet"
|
|
266
|
+
exit 6
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
result = begin
|
|
271
|
+
NodeLedger.append_transition(savepoint_path, subject: subject, state: state, fields: fields,
|
|
272
|
+
comment: comment, now: now, precondition: precondition)
|
|
273
|
+
rescue GuardedAppend::Unavailable => e
|
|
274
|
+
warn "node-transition: #{e.message}"
|
|
275
|
+
exit 3
|
|
276
|
+
rescue ArgumentError => e
|
|
277
|
+
warn "node-transition: #{e.message}"
|
|
278
|
+
exit 2
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
if result == :refused
|
|
282
|
+
warn "node-transition: #{subject} was no longer ready by the time the write lock was taken"
|
|
283
|
+
exit 5
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
puts "node-transition: appended #{state} for #{subject}"
|
|
287
|
+
exit 0
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
NodeTransition.main(ARGV) if $PROGRAM_NAME == __FILE__
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# outcome-report - the CLI over scripts/lib/outcome_report.rb (intent 339, G6).
|
|
6
|
+
# Prints the generated outcome.md by default; --write puts it on disk through
|
|
7
|
+
# AtomicWrite. `end-intent` calls the library directly (never shells out to
|
|
8
|
+
# this), so this command is for a human or an agent checking the generated
|
|
9
|
+
# report before or instead of a close.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# outcome-report <intent_dir> [--write] [--disposition delivered|abandoned]
|
|
13
|
+
#
|
|
14
|
+
# Exit codes:
|
|
15
|
+
# 0 - printed (or written) successfully
|
|
16
|
+
# 2 - usage error: no path given, an unknown flag, or the path is not an
|
|
17
|
+
# intent directory
|
|
18
|
+
# 3 - the report model could not be built (a malformed or absent graph.md)
|
|
19
|
+
|
|
20
|
+
require_relative "lib/outcome_report"
|
|
21
|
+
require_relative "lib/intent_screen"
|
|
22
|
+
require_relative "lib/outcome_guard"
|
|
23
|
+
|
|
24
|
+
def usage_abort(message)
|
|
25
|
+
warn "outcome-report: #{message}"
|
|
26
|
+
exit 2
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
args = ARGV.dup
|
|
30
|
+
write_flag = false
|
|
31
|
+
disposition = "delivered"
|
|
32
|
+
positional = []
|
|
33
|
+
|
|
34
|
+
while (arg = args.shift)
|
|
35
|
+
case arg
|
|
36
|
+
when "--write"
|
|
37
|
+
write_flag = true
|
|
38
|
+
when "--disposition"
|
|
39
|
+
disposition = args.shift or usage_abort("--disposition needs a value")
|
|
40
|
+
else
|
|
41
|
+
usage_abort("unknown flag #{arg.inspect}") if arg.start_with?("--")
|
|
42
|
+
positional << arg
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
target = positional.first
|
|
47
|
+
usage_abort("usage: outcome-report <intent_dir> [--write] [--disposition delivered|abandoned]") unless target
|
|
48
|
+
# Row v1f.9 (N3): refuse an unknown --disposition here rather than write a
|
|
49
|
+
# record OutcomeGuard then refuses. OutcomeGuard::DISPOSITIONS is already
|
|
50
|
+
# loaded in-process, so this is one comparison, not a duplicated list.
|
|
51
|
+
unless OutcomeGuard::DISPOSITIONS.include?(disposition)
|
|
52
|
+
usage_abort("--disposition must be one of #{OutcomeGuard::DISPOSITIONS.join('|')}, got #{disposition.inspect}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
intent_dir = File.expand_path(target)
|
|
56
|
+
usage_abort("#{intent_dir} is not an intent directory") unless IntentScreen.intent_dir?(intent_dir)
|
|
57
|
+
|
|
58
|
+
model = OutcomeReport.model(intent_dir)
|
|
59
|
+
unless model[:ok]
|
|
60
|
+
warn "outcome-report: the report model could not be built: #{model[:errors].join('; ')}"
|
|
61
|
+
exit 3
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if write_flag
|
|
65
|
+
OutcomeReport.write(intent_dir, disposition: disposition)
|
|
66
|
+
puts File.join(intent_dir, "outcome.md")
|
|
67
|
+
else
|
|
68
|
+
outcome_path = File.join(intent_dir, "outcome.md")
|
|
69
|
+
existing = File.exist?(outcome_path) ? File.read(outcome_path) : nil
|
|
70
|
+
puts OutcomeReport.render(model, disposition: disposition, existing: existing,
|
|
71
|
+
findings: OutcomeReport.findings(intent_dir), intent_dir: intent_dir)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
exit 0
|
package/scripts/plastic-lock
CHANGED
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
# arm take the intent for this session (intent 307): acquire
|
|
16
16
|
# delivery.lock stamped with --mode (default auto) and the
|
|
17
17
|
# provenance flags, provision the code worktree, and point the
|
|
18
|
-
# session at the intent; exit
|
|
19
|
-
# corrupt
|
|
18
|
+
# session at the intent; exit 3 on a held, stale, or excluded
|
|
19
|
+
# lock (another owner's), exit 1 on a corrupt one, each with the
|
|
20
|
+
# public command that resolves it
|
|
20
21
|
# who compact, read-only durable owner/activity view
|
|
21
22
|
# status report the lock file, its freshness, the derived worktree,
|
|
22
|
-
# whether this session
|
|
23
|
-
# live per-artifact claims
|
|
23
|
+
# whether this session is the one delivering the intent, and
|
|
24
|
+
# any live per-artifact claims
|
|
24
25
|
# fix idempotent repair of the lock for the current session; never
|
|
25
26
|
# touches a fresh foreign lock
|
|
26
|
-
# release owner clears the lock
|
|
27
|
-
# ledger (End tail / abandoning a boarding)
|
|
27
|
+
# release owner clears the lock (End tail / abandoning a boarding)
|
|
28
28
|
# reclaim explicit takeover of a stale lock, audited in savepoint.md
|
|
29
29
|
# delegate owner registers a subagent session under the lock (D4)
|
|
30
30
|
# claim acquire the named --artifact's claim token (intent 111);
|
|
@@ -32,18 +32,20 @@
|
|
|
32
32
|
# including this same session; takes over a stale claim
|
|
33
33
|
# release-claim free the named --artifact's claim token
|
|
34
34
|
#
|
|
35
|
-
# Without --intent-dir the intent is resolved from this session's
|
|
36
|
-
# (
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
35
|
+
# Without --intent-dir the intent is resolved from this session's held
|
|
36
|
+
# delivery lock (ActiveDelivery.resolve over the global store and the
|
|
37
|
+
# configured project roots): exactly one fresh lock this session owns or
|
|
38
|
+
# delegates for. Session resolution no longer reads any per-session state
|
|
39
|
+
# file; that path was retired in 2.0 (intent 307) and 344 G11.
|
|
40
|
+
# Exit 0 on success/report; exit 3 when another session owns the lock (arm,
|
|
41
|
+
# fix, release, reclaim); exit 1 when the verb is otherwise blocked.
|
|
40
42
|
|
|
43
|
+
require_relative "lib/store_layout"
|
|
41
44
|
require "json"
|
|
42
45
|
require "yaml"
|
|
43
|
-
require_relative "lib/bridge"
|
|
44
46
|
require_relative "lib/lock"
|
|
45
47
|
require_relative "lib/arm"
|
|
46
|
-
require_relative "lib/
|
|
48
|
+
require_relative "lib/active_delivery"
|
|
47
49
|
|
|
48
50
|
VERBS = %w[arm status who fix release reclaim delegate claim release-claim].freeze
|
|
49
51
|
|
|
@@ -70,6 +72,7 @@ until ARGV.empty?
|
|
|
70
72
|
when "--model" then opts[:model] = ARGV.shift
|
|
71
73
|
when "--thread" then opts[:thread] = ARGV.shift
|
|
72
74
|
when "--mode" then opts[:mode] = ARGV.shift
|
|
75
|
+
when "--allow-inline" then opts[:allow_inline] = true
|
|
73
76
|
when "--status" then opts[:status] = ARGV.shift
|
|
74
77
|
else
|
|
75
78
|
warn "unknown flag #{flag}"
|
|
@@ -81,22 +84,12 @@ usage! if opts[:mode] && !%w[auto guided].include?(opts[:mode])
|
|
|
81
84
|
session = opts[:session]
|
|
82
85
|
session = ENV["CLAUDE_CODE_SESSION_ID"] if session.nil? || session.strip.empty?
|
|
83
86
|
harness = opts[:harness]
|
|
84
|
-
hint_harness = harness || "claude"
|
|
85
87
|
# The plastic home every hook uses (PLASTIC_HOME, else ~/.plastic); Arm takes the
|
|
86
88
|
# OS home above it, the same convention Worktree resolves projects.yml from.
|
|
87
89
|
plastic_home_env = ENV["PLASTIC_HOME"].to_s.strip
|
|
88
90
|
home = plastic_home_env.empty? ? Dir.home : File.dirname(File.expand_path(plastic_home_env))
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
# the working directory (projects.yml, longest path match), then the global one.
|
|
92
|
-
def candidate_stores(home, cwd)
|
|
93
|
-
plastic_home = File.join(home, ".plastic")
|
|
94
|
-
stores = []
|
|
95
|
-
slug = SessionLedger.project_slug(cwd, plastic_home: plastic_home)
|
|
96
|
-
stores << File.join(plastic_home, "projects", slug, "store") if slug && slug != "global"
|
|
97
|
-
stores << File.join(plastic_home, "store")
|
|
98
|
-
stores
|
|
99
|
-
end
|
|
92
|
+
plastic_home = File.join(home, ".plastic")
|
|
100
93
|
|
|
101
94
|
dir = opts[:dir]
|
|
102
95
|
if dir.nil? || dir.strip.empty?
|
|
@@ -104,11 +97,21 @@ if dir.nil? || dir.strip.empty?
|
|
|
104
97
|
warn "plastic-lock: #{verb} needs --intent-dir <intent dir>"
|
|
105
98
|
exit 1
|
|
106
99
|
end
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
100
|
+
roots = ActiveDelivery.project_roots(plastic_home)
|
|
101
|
+
global_store = Plastic::StoreLayout.global_store(plastic_home)
|
|
102
|
+
dir = ActiveDelivery.resolve(global_store: global_store, project_roots: roots, session: session)
|
|
103
|
+
if dir.nil?
|
|
104
|
+
candidates = ActiveDelivery.candidate_intent_dirs(global_store: global_store, project_roots: roots).uniq
|
|
105
|
+
held = candidates.select do |candidate|
|
|
106
|
+
Lock.fresh?(candidate) && Lock.holds?(candidate, session: session)
|
|
107
|
+
end
|
|
108
|
+
if held.size > 1
|
|
109
|
+
warn "plastic-lock: more than one held delivery lock names this session; pass --intent-dir <intent dir>"
|
|
110
|
+
else
|
|
111
|
+
warn "plastic-lock: no intent resolved; pass --intent-dir <intent dir>"
|
|
112
|
+
end
|
|
113
|
+
exit 1
|
|
114
|
+
end
|
|
112
115
|
end
|
|
113
116
|
dir = File.expand_path(dir)
|
|
114
117
|
|
|
@@ -143,45 +146,54 @@ if verb == "who"
|
|
|
143
146
|
exit 0
|
|
144
147
|
end
|
|
145
148
|
|
|
146
|
-
intent_id =
|
|
149
|
+
intent_id = Arm.intent_id_for(dir)
|
|
147
150
|
store = File.dirname(dir)
|
|
148
151
|
key = Arm.resolve_session(session, store: store, intent_id: intent_id)
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
# Exit 3 is Plastic's refusal: the lock belongs to another session, and only
|
|
154
|
+
# its owner (or the person who owns the work) may resolve it.
|
|
155
|
+
def refuse(message)
|
|
156
|
+
warn "plastic-lock: #{message}"
|
|
157
|
+
exit 3
|
|
152
158
|
end
|
|
153
159
|
|
|
154
160
|
case verb
|
|
155
161
|
when "arm"
|
|
156
162
|
result = Arm.arm(intent_dir: dir, session: key, mode: opts[:mode] || "auto", home: home,
|
|
157
|
-
harness: harness, agent: opts[:agent], model: opts[:model], thread: opts[:thread]
|
|
163
|
+
harness: harness, agent: opts[:agent], model: opts[:model], thread: opts[:thread],
|
|
164
|
+
allow_inline: opts[:allow_inline] || false)
|
|
158
165
|
case result[:status]
|
|
159
166
|
when :acquired, :owned
|
|
160
167
|
wt = result[:worktree] || {}
|
|
161
168
|
puts JSON.pretty_generate(
|
|
162
169
|
"intent_dir" => dir, "session" => result[:session], "status" => result[:status].to_s,
|
|
163
170
|
"run_mode" => result[:lock] && result[:lock]["run_mode"],
|
|
164
|
-
"worktree" => wt
|
|
171
|
+
"worktree" => wt
|
|
165
172
|
)
|
|
166
173
|
when :held
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
exit 1
|
|
174
|
+
refuse "delivery lock for intent #{intent_id} is held by session " \
|
|
175
|
+
"#{result[:lock] && result[:lock]['owner_session']}; plastic auto lock status #{intent_id} shows it"
|
|
170
176
|
when :stale
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
exit 1
|
|
177
|
+
refuse "delivery lock for intent #{intent_id} is stale (owner " \
|
|
178
|
+
"#{result[:lock] && result[:lock]['owner_session']}); #{Arm.stale_hint(intent_id)}"
|
|
174
179
|
when :excluded
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
exit 1
|
|
180
|
+
refuse "a #{result[:lock] && result[:lock]['type']} lock is active on intent " \
|
|
181
|
+
"#{intent_id}; plastic auto lock status #{intent_id} shows it"
|
|
178
182
|
when :corrupt
|
|
179
|
-
warn "plastic-lock: delivery.lock for intent #{intent_id} is unreadable;
|
|
183
|
+
warn "plastic-lock: delivery.lock for intent #{intent_id} is unreadable; plastic auto lock fix #{intent_id} rewrites it"
|
|
180
184
|
exit 1
|
|
185
|
+
when :inline_refused
|
|
186
|
+
warn "plastic-lock: refusing to arm intent #{intent_id} from a conversation session " \
|
|
187
|
+
"(owner rule 2026-08-31: no inline intent delivery). Dispatch the delivery team " \
|
|
188
|
+
"instead (plastic-auto), or pass --allow-inline with explicit owner approval."
|
|
189
|
+
exit 3
|
|
181
190
|
end
|
|
182
191
|
when "status"
|
|
183
192
|
lock = Lock.read(dir)
|
|
184
|
-
|
|
193
|
+
resolved = ActiveDelivery.resolve(global_store: Plastic::StoreLayout.global_store(plastic_home),
|
|
194
|
+
project_roots: ActiveDelivery.project_roots(plastic_home),
|
|
195
|
+
session: key)
|
|
196
|
+
delivering = !resolved.nil? && File.expand_path(resolved) == File.expand_path(dir)
|
|
185
197
|
report = {
|
|
186
198
|
"intent_dir" => dir,
|
|
187
199
|
"session" => key,
|
|
@@ -189,41 +201,34 @@ when "status"
|
|
|
189
201
|
"lock_fresh" => Lock.fresh?(dir),
|
|
190
202
|
"lock_corrupt" => Lock.corrupt?(dir),
|
|
191
203
|
"worktree" => Arm.worktree_block(intent_dir: dir, home: home),
|
|
192
|
-
"
|
|
193
|
-
"pointer_names_intent" => pointer == intent_id,
|
|
204
|
+
"delivering" => delivering,
|
|
194
205
|
"claims" => Claim.claims_status(dir),
|
|
195
206
|
}
|
|
196
207
|
puts JSON.pretty_generate(report)
|
|
197
208
|
when "fix"
|
|
198
209
|
report = Arm.repair(intent_dir: dir, session: key, home: home, harness: harness,
|
|
199
210
|
agent: opts[:agent], model: opts[:model], thread: opts[:thread],
|
|
200
|
-
run_mode: opts[:mode]
|
|
211
|
+
run_mode: opts[:mode])
|
|
201
212
|
puts JSON.pretty_generate(report)
|
|
202
213
|
unless report["status"] == "repaired"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
exit 1
|
|
214
|
+
refuse "#{report['status']} by #{report['owner']}" \
|
|
215
|
+
"#{report['hint'] ? " (#{report['hint']})" : ''}"
|
|
206
216
|
end
|
|
207
217
|
when "release"
|
|
208
218
|
result = Lock.release(dir, session: key)
|
|
209
|
-
if result == :not_owner
|
|
210
|
-
warn "plastic-lock: not the owner; run plastic-lock status"
|
|
211
|
-
exit 1
|
|
212
|
-
end
|
|
213
|
-
Arm.reset_pointer(key, intent_id, home: home)
|
|
219
|
+
refuse "not the owner; plastic auto lock status #{intent_id} shows who is" if result == :not_owner
|
|
214
220
|
puts "released (#{result})"
|
|
215
221
|
when "reclaim"
|
|
216
222
|
status, lock_data = Lock.takeover(dir, session: key, harness: harness,
|
|
217
223
|
agent: opts[:agent], model: opts[:model],
|
|
218
224
|
thread: opts[:thread], run_mode: opts[:mode])
|
|
219
225
|
if status == :fresh
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
exit 1
|
|
226
|
+
refuse "lock is FRESH and held by #{lock_data['owner_session']}; " \
|
|
227
|
+
"back off (no silent reclaim)"
|
|
223
228
|
end
|
|
224
229
|
report = Arm.repair(intent_dir: dir, session: key, home: home, harness: harness,
|
|
225
230
|
agent: opts[:agent], model: opts[:model], thread: opts[:thread],
|
|
226
|
-
run_mode: opts[:mode]
|
|
231
|
+
run_mode: opts[:mode])
|
|
227
232
|
puts JSON.pretty_generate(report)
|
|
228
233
|
when "delegate"
|
|
229
234
|
usage! if opts[:delegate].nil?
|
package/scripts/project-links
CHANGED
|
@@ -48,10 +48,6 @@ require_relative "lib/revisions_writer"
|
|
|
48
48
|
class ProjectLinks
|
|
49
49
|
DEFAULT_HOME = File.join(Dir.home, ".plastic")
|
|
50
50
|
|
|
51
|
-
# The 72 intent dir audit destination (relative to plastic_home).
|
|
52
|
-
DEFAULT_AUDIT_REL =
|
|
53
|
-
"projects/plastic/store/72--links-graph-projection/resources/audit--links-projection.md"
|
|
54
|
-
|
|
55
51
|
def initialize(plastic_home: DEFAULT_HOME, dry_run: false, audit_path: nil,
|
|
56
52
|
drop_unbacked_links: false, intent: nil, store: nil)
|
|
57
53
|
@plastic_home = plastic_home
|
|
@@ -60,20 +56,7 @@ class ProjectLinks
|
|
|
60
56
|
@intent = intent && intent.to_s
|
|
61
57
|
@store = store
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
# the plan). With no explicit --audit-path, a dry run writes a `.dry-run.md`
|
|
65
|
-
# sibling, leaving the canonical real-run audit untouched. An explicit
|
|
66
|
-
# --audit-path is always honored verbatim (tests inject it). Mirrors
|
|
67
|
-
# RebuildGraph's discipline exactly.
|
|
68
|
-
canonical = File.join(plastic_home, DEFAULT_AUDIT_REL)
|
|
69
|
-
@audit_path =
|
|
70
|
-
if audit_path
|
|
71
|
-
audit_path
|
|
72
|
-
elsif dry_run
|
|
73
|
-
canonical.sub(/\.md\z/, ".dry-run.md")
|
|
74
|
-
else
|
|
75
|
-
canonical
|
|
76
|
-
end
|
|
59
|
+
@audit_path = audit_path || File.join(Plastic::StoreLayout.global_root(plastic_home), "resources", "audit--links-projection.md")
|
|
77
60
|
end
|
|
78
61
|
|
|
79
62
|
attr_reader :plastic_home, :dry_run, :audit_path, :drop_unbacked_links, :intent, :store
|
|
@@ -330,9 +313,11 @@ class ProjectLinks
|
|
|
330
313
|
"(ambiguous: multiple ## Links headings)"
|
|
331
314
|
end
|
|
332
315
|
|
|
333
|
-
#
|
|
316
|
+
# Return the audit for previews; save it only when applying changes.
|
|
334
317
|
def emit_audit(store_list, results)
|
|
335
318
|
text = render_audit(store_list, results)
|
|
319
|
+
return text if dry_run
|
|
320
|
+
|
|
336
321
|
FileUtils.mkdir_p(File.dirname(audit_path))
|
|
337
322
|
File.write(audit_path, text)
|
|
338
323
|
text
|
|
@@ -348,7 +333,7 @@ class ProjectLinks
|
|
|
348
333
|
|
|
349
334
|
def render_audit(store_list, results)
|
|
350
335
|
lines = []
|
|
351
|
-
lines << "# Audit: store-wide
|
|
336
|
+
lines << "# Audit: store-wide Links projection"
|
|
352
337
|
lines << ""
|
|
353
338
|
lines << "Generated: #{Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")}#{dry_run ? " (DRY RUN)" : ""}"
|
|
354
339
|
lines << "Scoped to intent #{intent} only (--intent)." if intent
|
|
@@ -482,6 +467,6 @@ if $PROGRAM_NAME == __FILE__
|
|
|
482
467
|
"regenerated #{totals[:regenerated]}, added #{totals[:added]}, " \
|
|
483
468
|
"unchanged #{totals[:unchanged]}, failed #{totals[:failed]} " \
|
|
484
469
|
"across #{results.size} store(s)."
|
|
485
|
-
puts "Audit: #{tool.audit_path}"
|
|
470
|
+
puts(dry ? "Dry run: no files written." : "Audit: #{tool.audit_path}")
|
|
486
471
|
exit 1 if tool.any_failed?(results)
|
|
487
472
|
end
|