@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,58 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# hook-stop (intent 340b, G7c, n4, D5): the Stop hook body. Reads the harness
|
|
6
|
+
# payload from stdin, gathers the runtime config StopGate needs, and asks it
|
|
7
|
+
# whether this session may stop. Prints Claude Code's Stop block schema on
|
|
8
|
+
# stdout only when the answer is block; a permit prints nothing at all.
|
|
9
|
+
# Registration is static (hooks/hooks.json, settings.json) - this launcher
|
|
10
|
+
# always installs - and the decision is runtime, off by default
|
|
11
|
+
# (runner.stop_hook defaults false), so the owner flips continuation on or
|
|
12
|
+
# off by editing config with no reinstall. Every error permits: always
|
|
13
|
+
# exits 0, never raises out to the harness.
|
|
14
|
+
#
|
|
15
|
+
# Usage: hook-stop <plastic_home> (stdin: the hook JSON)
|
|
16
|
+
|
|
17
|
+
require_relative "lib/store_layout"
|
|
18
|
+
require "json"
|
|
19
|
+
require_relative "lib/stop_gate"
|
|
20
|
+
|
|
21
|
+
plastic_home = ARGV[0]
|
|
22
|
+
exit 0 if plastic_home.nil? || plastic_home.empty?
|
|
23
|
+
|
|
24
|
+
def read_config_value(key, home, read_config)
|
|
25
|
+
IO.popen({ "PLASTIC_HOME" => home }, [read_config, key], err: File::NULL, &:read).to_s.strip
|
|
26
|
+
rescue StandardError
|
|
27
|
+
""
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
raw = $stdin.tty? ? "" : $stdin.read.to_s
|
|
32
|
+
payload = raw.strip.empty? ? {} : JSON.parse(raw)
|
|
33
|
+
payload = {} unless payload.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
home = File.expand_path(plastic_home)
|
|
36
|
+
global_store = Plastic::StoreLayout.global_store(home)
|
|
37
|
+
read_config = File.expand_path("read-config", __dir__)
|
|
38
|
+
|
|
39
|
+
config_stop_hook = read_config_value("runner.stop_hook", home, read_config)
|
|
40
|
+
roots_raw = read_config_value("project_roots", home, read_config)
|
|
41
|
+
project_roots =
|
|
42
|
+
begin
|
|
43
|
+
roots_raw.strip.empty? ? [] : Array(JSON.parse(roots_raw))
|
|
44
|
+
rescue JSON::ParserError
|
|
45
|
+
[]
|
|
46
|
+
end
|
|
47
|
+
project_roots = project_roots.map { |root| File.expand_path(root.to_s) }
|
|
48
|
+
|
|
49
|
+
session = payload["session_id"].to_s
|
|
50
|
+
|
|
51
|
+
result = StopGate.decide(payload: payload, config_stop_hook: config_stop_hook, session: session,
|
|
52
|
+
global_store: global_store, project_roots: project_roots)
|
|
53
|
+
|
|
54
|
+
puts JSON.generate("decision" => "block", "reason" => result[:reason]) if result[:block]
|
|
55
|
+
rescue StandardError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
exit 0
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# index-projection - the CLI over IndexProjection (intent 337, G4): prints
|
|
6
|
+
# the drift between INDEX.md and the intent ledgers for one store root
|
|
7
|
+
# (INDEX.md plus a store/ directory), exits non-zero when any drift exists.
|
|
8
|
+
# --write renders INDEX.md's four status sections from the projection
|
|
9
|
+
# through AtomicWrite; every other byte, including ## Clusters and
|
|
10
|
+
# ## Relocated, is untouched.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# index-projection <store_root> [--write]
|
|
14
|
+
#
|
|
15
|
+
# Exit codes:
|
|
16
|
+
# 0 - no drift (or, with --write, the write landed cleanly)
|
|
17
|
+
# 1 - drift exists (a real terminal-ledger conflict, an INDEX entry with
|
|
18
|
+
# no directory, or a directory INDEX does not list)
|
|
19
|
+
# 2 - usage: no argument, or the path is not a store root
|
|
20
|
+
|
|
21
|
+
require_relative "lib/index_projection"
|
|
22
|
+
|
|
23
|
+
module IndexProjectionCli
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def usage
|
|
27
|
+
warn "Usage: index-projection <store_root> [--write]"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def main(argv)
|
|
31
|
+
args = argv.dup
|
|
32
|
+
write = !!args.delete("--write")
|
|
33
|
+
|
|
34
|
+
root = args.shift
|
|
35
|
+
unless root && Dir.exist?(root)
|
|
36
|
+
warn "index-projection: #{root.inspect} is not a store root"
|
|
37
|
+
usage
|
|
38
|
+
return 2
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
index_path = File.join(root, "INDEX.md")
|
|
42
|
+
store_dir = File.join(root, "store")
|
|
43
|
+
unless File.exist?(index_path)
|
|
44
|
+
warn "index-projection: no INDEX.md at #{index_path}"
|
|
45
|
+
return 2
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if write
|
|
49
|
+
result = IndexProjection.write(store_dir, index_path: index_path)
|
|
50
|
+
unless result[:ok]
|
|
51
|
+
warn "index-projection: #{result[:error]}"
|
|
52
|
+
return 2
|
|
53
|
+
end
|
|
54
|
+
puts "index-projection: wrote #{index_path} (#{result[:moved].length} moved)"
|
|
55
|
+
return 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
analysis = IndexProjection.analyze(store_dir, index_path: index_path)
|
|
59
|
+
print_report(analysis)
|
|
60
|
+
|
|
61
|
+
(analysis[:drift].empty? && analysis[:index_only].empty? && analysis[:directory_only].empty?) ? 0 : 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def print_report(analysis)
|
|
65
|
+
puts "Drift (#{analysis[:drift].length}):"
|
|
66
|
+
analysis[:drift].each { |r| puts " #{r[:id]}: INDEX=#{r[:index_status]} ledger=#{r[:ledger_status]}" }
|
|
67
|
+
puts "INDEX-only (#{analysis[:index_only].length}):"
|
|
68
|
+
analysis[:index_only].each { |r| puts " #{r[:id]} (#{r[:index_status]})" }
|
|
69
|
+
puts "Directory-only (#{analysis[:directory_only].length}):"
|
|
70
|
+
analysis[:directory_only].each { |r| puts " #{r[:id]}" }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
exit(IndexProjectionCli.main(ARGV)) if $PROGRAM_NAME == __FILE__
|
package/scripts/insight-append
CHANGED
|
@@ -11,8 +11,15 @@
|
|
|
11
11
|
# `now:` seam is the test seam; the CLI uses the default Time.now, which is fine
|
|
12
12
|
# because determinism is covered at the library level (test/insights_test.rb).
|
|
13
13
|
#
|
|
14
|
+
# --rule (intent 341, G8, C37): tags the entry as a rule, not just an
|
|
15
|
+
# observation, by prepending the literal "rule: " onto the text before it
|
|
16
|
+
# ever reaches Insights.append_insight; the library itself stays untouched,
|
|
17
|
+
# since a tag is a text-level convention, not a new field on the ledger.
|
|
18
|
+
# Doctor's unpromoted_rules check (scripts/doctor.rb) later lists any tagged
|
|
19
|
+
# entry whose exact text no docs/help/*.md chapter carries yet.
|
|
20
|
+
#
|
|
14
21
|
# Usage:
|
|
15
|
-
# insight-append <intent_dir> <text> --stage S --author A
|
|
22
|
+
# insight-append <intent_dir> <text> --stage S --author A [--rule]
|
|
16
23
|
#
|
|
17
24
|
# Exit codes: 0 (entry appended), 2 (usage).
|
|
18
25
|
|
|
@@ -21,6 +28,7 @@ require_relative "lib/insights"
|
|
|
21
28
|
def parse_args(argv)
|
|
22
29
|
stage = nil
|
|
23
30
|
author = nil
|
|
31
|
+
rule = false
|
|
24
32
|
positional = []
|
|
25
33
|
i = 0
|
|
26
34
|
while i < argv.length
|
|
@@ -31,21 +39,26 @@ def parse_args(argv)
|
|
|
31
39
|
when "--author"
|
|
32
40
|
author = argv[i + 1]
|
|
33
41
|
i += 2
|
|
42
|
+
when "--rule"
|
|
43
|
+
rule = true
|
|
44
|
+
i += 1
|
|
34
45
|
else
|
|
35
46
|
positional << argv[i]
|
|
36
47
|
i += 1
|
|
37
48
|
end
|
|
38
49
|
end
|
|
39
|
-
[positional[0], positional[1], stage, author]
|
|
50
|
+
[positional[0], positional[1], stage, author, rule]
|
|
40
51
|
end
|
|
41
52
|
|
|
42
|
-
intent_dir, text, stage, author = parse_args(ARGV)
|
|
53
|
+
intent_dir, text, stage, author, rule = parse_args(ARGV)
|
|
43
54
|
|
|
44
55
|
if [intent_dir, text, stage, author].any? { |v| v.nil? || v.to_s.empty? }
|
|
45
|
-
warn "usage: insight-append <intent_dir> <text> --stage S --author A"
|
|
56
|
+
warn "usage: insight-append <intent_dir> <text> --stage S --author A [--rule]"
|
|
46
57
|
exit 2
|
|
47
58
|
end
|
|
48
59
|
|
|
60
|
+
text = "rule: #{text}" if rule
|
|
61
|
+
|
|
49
62
|
entry = Insights.append_insight(File.expand_path(intent_dir), text,
|
|
50
63
|
stage: stage, author: author)
|
|
51
64
|
puts "appended: #{entry}"
|
package/scripts/install.rb
CHANGED
|
@@ -75,12 +75,15 @@ class Install < InstallerCore
|
|
|
75
75
|
# they are reported, never re-installed, so the summary line and the Codex
|
|
76
76
|
# trust reminder only ever count agents that actually changed this run.
|
|
77
77
|
def run(selected:, force: false, reinstall: false, ledger_action: nil, argv: ARGV, input: $stdin,
|
|
78
|
-
already_registered: [])
|
|
78
|
+
already_registered: [], qmd_runner: QmdSync.default_runner, qmd_detector: QmdSync.method(:detect))
|
|
79
79
|
fresh = !installed?
|
|
80
80
|
mode = fresh ? :install : :update # :update here means "re-sync, skip bootstrap"
|
|
81
81
|
|
|
82
82
|
distribute(mode)
|
|
83
83
|
bootstrap if fresh
|
|
84
|
+
git_init_if_absent
|
|
85
|
+
register_with_qmd(runner: qmd_runner, detector: qmd_detector)
|
|
86
|
+
migrate_advisor_config_file(File.join(plastic_home, "config.yml"))
|
|
84
87
|
apply_config_flags(argv)
|
|
85
88
|
|
|
86
89
|
results = selected.map do |key|
|
|
@@ -135,13 +138,13 @@ class Install < InstallerCore
|
|
|
135
138
|
end
|
|
136
139
|
|
|
137
140
|
def git_probe
|
|
138
|
-
!`
|
|
141
|
+
!`git --version`.strip.empty?
|
|
139
142
|
rescue StandardError
|
|
140
143
|
false
|
|
141
144
|
end
|
|
142
145
|
|
|
143
146
|
def mise_probe
|
|
144
|
-
!`
|
|
147
|
+
!`mise --version`.strip.empty?
|
|
145
148
|
rescue StandardError
|
|
146
149
|
false
|
|
147
150
|
end
|
|
@@ -239,10 +242,9 @@ class Install < InstallerCore
|
|
|
239
242
|
--no-advisor Skip installing both advisor agents and the agent-advisor
|
|
240
243
|
skill (advisor.enabled: false)
|
|
241
244
|
--advisor VALUE Which advisor agent is the default: an agent name, or the
|
|
242
|
-
shorthand "
|
|
243
|
-
|
|
244
|
-
unset, the agent-advisor skill falls back to
|
|
245
|
-
plastic-faux-advisor at consult time.
|
|
245
|
+
shorthand "primary" or "secondary". Legacy "real" and
|
|
246
|
+
"faux" aliases remain accepted. Writes advisor.claude.default.
|
|
247
|
+
Left unset, the agent-advisor skill falls back to Primary Advisor.
|
|
246
248
|
-h, --help Show this help
|
|
247
249
|
|
|
248
250
|
Notes:
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "graph_file"
|
|
5
|
+
require_relative "graph_edges"
|
|
6
|
+
require_relative "node_file"
|
|
7
|
+
require_relative "savepoint"
|
|
8
|
+
require_relative "work_graph_validator"
|
|
9
|
+
|
|
10
|
+
# ActionGraphShim (intent 342, G9): a read-time backward shim that presents
|
|
11
|
+
# any intent directory's actions/*.md as a node graph, in the record shape
|
|
12
|
+
# NodeFile.parse returns, so 336, 338 and 339 read a legacy intent through
|
|
13
|
+
# the same call they already use for an authored graph.md (spec.md D1-D18).
|
|
14
|
+
#
|
|
15
|
+
# Writes nothing, anywhere, ever (D1). An authored graph.md always wins over
|
|
16
|
+
# the synthetic chain (D3) - the reverse of 334's forward shim, which
|
|
17
|
+
# resolves content and prefers the older, richer source. This one resolves
|
|
18
|
+
# structure, where an authored graph is the only real graph.
|
|
19
|
+
module ActionGraphShim
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# D10's heading test: unchanged, four tokens, verified correct on the live
|
|
23
|
+
# store. Governs only whether a "## Files ..." heading itself is negated
|
|
24
|
+
# (e.g. "## Files you must NOT change"), never the section body.
|
|
25
|
+
FILES_NEGATION_RE = /\bnot\b|\bnever\b|\bavoid\b|don't/i.freeze
|
|
26
|
+
|
|
27
|
+
# D19's body-truncation vocabulary: the four heading tokens plus the
|
|
28
|
+
# exclusion phrases the live corpus actually uses ("Out of bounds, owned
|
|
29
|
+
# by leads running in parallel right now:" matched none of the original
|
|
30
|
+
# four). Governs whether a body LINE truncates the files harvest -
|
|
31
|
+
# never the heading test above, which stays on FILES_NEGATION_RE.
|
|
32
|
+
FILES_EXCLUSION_RE = /\bnot\b|\bnever\b|\bavoid\b|don't|\bout of bounds\b|\bout of scope\b|\bhands off\b|\bleave alone\b|\bexcluded\b/i.freeze
|
|
33
|
+
|
|
34
|
+
BACKTICK_RE = /`([^`]+)`/.freeze
|
|
35
|
+
PROVEN_BY_LABEL_RE = /\AS\d+\z/.freeze
|
|
36
|
+
|
|
37
|
+
# :authored when graph.md exists, :actions when it does not but the intent
|
|
38
|
+
# holds at least one real action file (D7's exact realness test, reused
|
|
39
|
+
# rather than reimplemented so the shim can never disagree with
|
|
40
|
+
# Savepoint.has_real_action? about whether an intent has work in it), :none
|
|
41
|
+
# otherwise. A directory that does not exist returns :none and never
|
|
42
|
+
# raises.
|
|
43
|
+
def shape(intent_dir)
|
|
44
|
+
return :none unless intent_dir && File.directory?(intent_dir.to_s)
|
|
45
|
+
return :authored if File.exist?(File.join(intent_dir, "graph.md"))
|
|
46
|
+
return :actions if Savepoint.has_real_files_in?("actions", intent_dir)
|
|
47
|
+
|
|
48
|
+
:none
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The seven-key hash GraphFile.parse returns (ok:, goal:, decisions:,
|
|
52
|
+
# graph:, status:, verify:, errors:), for whichever shape the directory
|
|
53
|
+
# is in. On :authored this is literally GraphFile.parse, unmodified (D3).
|
|
54
|
+
# On :actions the same hash is built from the action files, with goal:,
|
|
55
|
+
# decisions: and status: nil (a 2024 action file was never asked for
|
|
56
|
+
# them) and verify: a fixed reason so a trivial-bar check the caller may
|
|
57
|
+
# run stays self-consistent. On :none it is GraphFile.parse's own
|
|
58
|
+
# not-found failure, produced by delegating to it rather than
|
|
59
|
+
# reformatting the message ourselves.
|
|
60
|
+
def view(intent_dir)
|
|
61
|
+
case shape(intent_dir)
|
|
62
|
+
when :authored
|
|
63
|
+
GraphFile.parse(File.join(intent_dir, "graph.md"))
|
|
64
|
+
when :actions
|
|
65
|
+
synth = synthetic_graph(intent_dir)
|
|
66
|
+
{
|
|
67
|
+
ok: synth[:errors].empty?,
|
|
68
|
+
goal: nil,
|
|
69
|
+
decisions: nil,
|
|
70
|
+
graph: synth,
|
|
71
|
+
status: nil,
|
|
72
|
+
verify: { reason: "backward shim: legacy actions/ carry no verify node" },
|
|
73
|
+
errors: synth[:errors],
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
# 342 post-execution review, should-fix 5: a nil intent_dir must never
|
|
77
|
+
# raise. shape(nil) is already nil-guarded, so `view` and `needs`
|
|
78
|
+
# honor the same contract rather than raising a File.join TypeError.
|
|
79
|
+
return GraphFile.failure(["graph file not found: #{intent_dir.inspect}"]) unless intent_dir
|
|
80
|
+
|
|
81
|
+
GraphFile.parse(File.join(intent_dir, "graph.md"))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Node records for either shape (D8), always carrying NodeFile.parse's own
|
|
86
|
+
# key set (ok:, node:, kind:, files:, budget:, report:, body:, errors:) plus
|
|
87
|
+
# needs:, path: and proven_by:. [] on :none, never a raise on a real
|
|
88
|
+
# directory.
|
|
89
|
+
def nodes(intent_dir)
|
|
90
|
+
case shape(intent_dir)
|
|
91
|
+
when :authored
|
|
92
|
+
authored_nodes(intent_dir)
|
|
93
|
+
when :actions
|
|
94
|
+
synthetic_nodes(intent_dir)
|
|
95
|
+
else
|
|
96
|
+
[]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The needs targets for one node id, under either shape; [] for an id the
|
|
101
|
+
# graph does not declare, and [] for a directory with no graph at all.
|
|
102
|
+
def needs(intent_dir, node_id)
|
|
103
|
+
graph = view(intent_dir)[:graph]
|
|
104
|
+
return [] unless graph
|
|
105
|
+
|
|
106
|
+
(graph[:edges] || {})[node_id] || []
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# --- authored shape -------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def authored_nodes(intent_dir)
|
|
112
|
+
parsed = GraphFile.parse(File.join(intent_dir, "graph.md"))
|
|
113
|
+
edges = parsed.dig(:graph, :edges) || {}
|
|
114
|
+
|
|
115
|
+
# 342 post-execution review, nit 8: sort_key numerically over the
|
|
116
|
+
# basename, the same key the synthetic chain uses, so an authored
|
|
117
|
+
# intent with ten or more nodes orders n10 after n9 rather than
|
|
118
|
+
# lexically between n1 and n2.
|
|
119
|
+
Dir.glob(File.join(intent_dir, "nodes", "*.md"))
|
|
120
|
+
.sort_by { |path| sort_key(File.basename(path)) }
|
|
121
|
+
.map do |path|
|
|
122
|
+
nf = NodeFile.parse(path)
|
|
123
|
+
nf.merge(
|
|
124
|
+
needs: edges[nf[:node]] || [],
|
|
125
|
+
path: path,
|
|
126
|
+
proven_by: proven_by_labels(nf[:body].to_s)
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# --- actions shape ----------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
# {nodes:, edges:, errors:} built directly over the minted chain, in the
|
|
134
|
+
# same shape GraphEdges.parse returns, rather than rendering a "## Graph"
|
|
135
|
+
# section and reparsing it.
|
|
136
|
+
def synthetic_graph(intent_dir)
|
|
137
|
+
ids = real_action_files(intent_dir).each_index.map { |i| "n#{i + 1}" }
|
|
138
|
+
edges = {}
|
|
139
|
+
ids.each_with_index { |id, i| edges[id] = i.zero? ? [] : [ids[i - 1]] }
|
|
140
|
+
{ nodes: ids, edges: edges, errors: [] }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def synthetic_nodes(intent_dir)
|
|
144
|
+
files = real_action_files(intent_dir)
|
|
145
|
+
files.each_with_index.map do |path, i|
|
|
146
|
+
id = "n#{i + 1}"
|
|
147
|
+
needs_targets = i.zero? ? [] : ["n#{i}"]
|
|
148
|
+
begin
|
|
149
|
+
text = File.read(path)
|
|
150
|
+
{
|
|
151
|
+
ok: true,
|
|
152
|
+
node: id,
|
|
153
|
+
kind: "work",
|
|
154
|
+
files: files_section_paths(text),
|
|
155
|
+
budget: nil,
|
|
156
|
+
report: nil,
|
|
157
|
+
body: text,
|
|
158
|
+
errors: [],
|
|
159
|
+
needs: needs_targets,
|
|
160
|
+
path: path,
|
|
161
|
+
proven_by: proven_by_labels(text),
|
|
162
|
+
}
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
{
|
|
165
|
+
ok: false,
|
|
166
|
+
node: id,
|
|
167
|
+
kind: "work",
|
|
168
|
+
files: [],
|
|
169
|
+
budget: nil,
|
|
170
|
+
report: nil,
|
|
171
|
+
body: nil,
|
|
172
|
+
errors: ["could not read #{path}: #{e.message}"],
|
|
173
|
+
needs: needs_targets,
|
|
174
|
+
path: path,
|
|
175
|
+
proven_by: [],
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Real action files (File.file?, non-zero size, Savepoint.stage_file_present?
|
|
182
|
+
# - D7's own three-part predicate, applied per file since has_real_files_in?
|
|
183
|
+
# only answers true/false), ordered by the integer trailing the basename,
|
|
184
|
+
# falling back to the basename itself so the key is a total order over
|
|
185
|
+
# every basename shape the live store holds (D4).
|
|
186
|
+
def real_action_files(intent_dir)
|
|
187
|
+
Dir.glob(File.join(intent_dir.to_s, "actions", "*.md"))
|
|
188
|
+
.select { |f| File.file?(f) && File.size(f) > 0 && Savepoint.stage_file_present?(f) }
|
|
189
|
+
.sort_by { |f| sort_key(File.basename(f)) }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def sort_key(basename)
|
|
193
|
+
m = basename.match(/\d+/)
|
|
194
|
+
m ? [0, m[0].to_i, basename] : [1, 0, basename]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# The first heading whose leading token is "Files" (case-insensitively),
|
|
198
|
+
# unless the heading text carries a negation, in which case it is not a
|
|
199
|
+
# files section and the search continues (D10). Paths come out of that
|
|
200
|
+
# section's BODY through extract_files_paths, which is itself
|
|
201
|
+
# negation-aware and fence-aware (D17, D18). No qualifying heading gives
|
|
202
|
+
# [] (D9).
|
|
203
|
+
def files_section_paths(text)
|
|
204
|
+
NodeFile.split_by_headings(text).each do |heading, section|
|
|
205
|
+
next unless files_heading?(heading)
|
|
206
|
+
|
|
207
|
+
return extract_files_paths(section.to_s)
|
|
208
|
+
end
|
|
209
|
+
[]
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Fence-aware (D18) and negation-aware over the section body, not only
|
|
213
|
+
# its heading (D17). D10 already stops a heading like "## Files you must
|
|
214
|
+
# NOT change" from being read as a files section at all; this handles
|
|
215
|
+
# the equally common case where a legitimate "## Files to touch" heading
|
|
216
|
+
# is followed, inside the same section, by an out-of-bounds paragraph -
|
|
217
|
+
# live on this intent's own dogfood fixture. A fenced block is dropped
|
|
218
|
+
# before anything else runs, reusing NodeFile.each_fence_line rather than
|
|
219
|
+
# a bare backtick scan, which is exactly what that helper exists to
|
|
220
|
+
# prevent.
|
|
221
|
+
#
|
|
222
|
+
# Lines are scanned in document order; harvesting stops at the first line
|
|
223
|
+
# an exclusion GOVERNS (D19), not merely mentions - a negation is a false
|
|
224
|
+
# positive as often as a real exclusion clause, since an action file
|
|
225
|
+
# routinely annotates an in-bounds path with a "do NOT rename it" aside.
|
|
226
|
+
# governs_exclusion? draws the line: the match governs when it begins
|
|
227
|
+
# before the line's first backtick (an exclusion clause introducing
|
|
228
|
+
# paths) or when the line carries no backtick at all (a bare warning
|
|
229
|
+
# sentence); it annotates, and the line is kept, when the match falls
|
|
230
|
+
# after the first backtick (a parenthetical about a path already named).
|
|
231
|
+
# Any span that still holds a newline is rejected as a backstop: a path
|
|
232
|
+
# is never more than one line.
|
|
233
|
+
def extract_files_paths(section)
|
|
234
|
+
kept = +""
|
|
235
|
+
NodeFile.each_fence_line(section) do |line, fenced|
|
|
236
|
+
next if fenced
|
|
237
|
+
break if governs_exclusion?(line)
|
|
238
|
+
|
|
239
|
+
kept << line
|
|
240
|
+
end
|
|
241
|
+
kept.scan(BACKTICK_RE).flatten.reject { |s| s.include?("\n") }.uniq
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# True when a FILES_EXCLUSION_RE match on this line governs the paths on
|
|
245
|
+
# it rather than merely annotating one (D19).
|
|
246
|
+
def governs_exclusion?(line)
|
|
247
|
+
match = line.match(FILES_EXCLUSION_RE)
|
|
248
|
+
return false unless match
|
|
249
|
+
|
|
250
|
+
first_backtick = line.index("`")
|
|
251
|
+
first_backtick.nil? || match.begin(0) < first_backtick
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def files_heading?(heading)
|
|
255
|
+
stripped = heading.to_s.sub(/\A#+\s*/, "")
|
|
256
|
+
first_token = stripped.split(/\s+/, 2).first.to_s.sub(/[^A-Za-z]+\z/, "")
|
|
257
|
+
return false unless first_token.casecmp?("Files")
|
|
258
|
+
|
|
259
|
+
!heading.to_s.match?(FILES_NEGATION_RE)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# The S\d+ tokens of a file's headings, in heading order, taking a
|
|
263
|
+
# heading only when it owns at least one table data row (D5) - the same
|
|
264
|
+
# table-owning rule WorkGraphValidator.has_valid_matrix? and ReportScreen
|
|
265
|
+
# already apply, reused unmodified rather than adapted, so a prose
|
|
266
|
+
# mention of an S-label never manufactures a Proven-by source (334's
|
|
267
|
+
# post-execution review defect, in the other direction).
|
|
268
|
+
def proven_by_labels(text)
|
|
269
|
+
labels = []
|
|
270
|
+
NodeFile.split_by_headings(text).each do |heading, section|
|
|
271
|
+
next unless NodeFile.table_rows(section).any?
|
|
272
|
+
|
|
273
|
+
WorkGraphValidator.heading_tokens(heading).each do |token|
|
|
274
|
+
labels << token if token.match?(PROVEN_BY_LABEL_RE)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
labels
|
|
278
|
+
end
|
|
279
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "yaml"
|
|
5
|
+
require_relative "lock"
|
|
6
|
+
|
|
7
|
+
# ActiveDelivery (intent 340b, G7c, n4, D10): which intent a session is
|
|
8
|
+
# delivering. A lock is a file inside an intent directory, and intents live
|
|
9
|
+
# under the global store or under any configured project's store, so
|
|
10
|
+
# answering "which intent" needs a walk, not an index the way node_ids.rb
|
|
11
|
+
# indexes ids: one Dir.glob per configured root (plus one for the global
|
|
12
|
+
# store) and one JSON parse per candidate lock file. No validator, no graph
|
|
13
|
+
# rebuild, nothing else touches disk. Shared by StopGate (the fourth arming
|
|
14
|
+
# condition needs to know the lock's run_mode) and Handoff (the Runner
|
|
15
|
+
# section needs to know which intent's runner-step.last to render).
|
|
16
|
+
#
|
|
17
|
+
# Pure and dependency-injected: every call takes explicit paths; nothing
|
|
18
|
+
# here reads ENV, shells out, or raises across its own boundary.
|
|
19
|
+
module ActiveDelivery
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# resolve(global_store:, project_roots:, session:, ttl:, now:) -> intent_dir or nil.
|
|
23
|
+
# Nil for zero matches or more than one: a session holding two live locks
|
|
24
|
+
# at once is a state nothing downstream may guess through (row 4.25).
|
|
25
|
+
def resolve(global_store:, project_roots:, session:, ttl: Lock::TTL_SECONDS, now: Time.now)
|
|
26
|
+
held = candidate_intent_dirs(global_store: global_store, project_roots: project_roots)
|
|
27
|
+
.uniq
|
|
28
|
+
.select { |dir| held_live?(dir, session: session, ttl: ttl, now: now) }
|
|
29
|
+
held.size == 1 ? held.first : nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Every intent directory under the global store or a configured project
|
|
33
|
+
# root that carries a delivery.lock file, unparsed. One Dir.glob per root
|
|
34
|
+
# (row 4.27): the global store's intents sit directly under store/, a
|
|
35
|
+
# project root's intents sit two levels down, under <root>/<slug>/store/.
|
|
36
|
+
def candidate_intent_dirs(global_store:, project_roots:)
|
|
37
|
+
dirs = []
|
|
38
|
+
dirs.concat(Dir.glob(File.join(global_store.to_s, "*", "delivery.lock"))) unless blank?(global_store)
|
|
39
|
+
Array(project_roots).each do |root|
|
|
40
|
+
next if blank?(root)
|
|
41
|
+
|
|
42
|
+
dirs.concat(Dir.glob(File.join(root.to_s, "*", "store", "*", "delivery.lock")))
|
|
43
|
+
end
|
|
44
|
+
dirs.map { |lock_path| File.dirname(lock_path) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Live and held by this session: a fresh (non-expired) lock whose owner or
|
|
48
|
+
# delegate list names this session. A torn or unparseable lock is skipped,
|
|
49
|
+
# never raised (row 4.26): a module a hook depends on must fail as quietly
|
|
50
|
+
# as the hook itself does.
|
|
51
|
+
def held_live?(intent_dir, session:, ttl:, now:)
|
|
52
|
+
return false unless Lock.fresh?(intent_dir, ttl: ttl, now: now)
|
|
53
|
+
|
|
54
|
+
Lock.holds?(intent_dir, session: session)
|
|
55
|
+
rescue StandardError
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def blank?(value)
|
|
60
|
+
value.nil? || value.to_s.strip.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
DEFAULT_PROJECT_ROOTS = ["~/.plastic/projects", "~/.plastic/stores"].freeze
|
|
64
|
+
|
|
65
|
+
# The configured project roots (344 n2, D6): the `project_roots` list in
|
|
66
|
+
# `<plastic_home>/config.yml`, each expanded, or the default
|
|
67
|
+
# `~/.plastic/projects` when the key is absent or the file cannot be read
|
|
68
|
+
# or parsed. Never raises: a broken config.yml must never crash a hook on
|
|
69
|
+
# every prompt.
|
|
70
|
+
def project_roots(plastic_home)
|
|
71
|
+
path = File.join(plastic_home.to_s, "config.yml")
|
|
72
|
+
data = File.exist?(path) ? YAML.safe_load(File.read(path)) : nil
|
|
73
|
+
roots = data.is_a?(Hash) ? data["project_roots"] : nil
|
|
74
|
+
roots = DEFAULT_PROJECT_ROOTS unless roots.is_a?(Array) && !roots.empty?
|
|
75
|
+
roots.map { |root| File.expand_path(root.to_s) }
|
|
76
|
+
rescue StandardError
|
|
77
|
+
DEFAULT_PROJECT_ROOTS.map { |root| File.expand_path(root) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# resolve_by_short_session(...) -> intent_dir or nil (344 n2). The tmp dir
|
|
81
|
+
# name is the short session id, while lock owners and delegates carry full
|
|
82
|
+
# ids, so this matches a lock whose owner or a delegate session STARTS WITH
|
|
83
|
+
# the short id. Nil for zero or more than one match, mirroring resolve's
|
|
84
|
+
# own uniqueness rule: an ambiguous short id names nothing downstream may
|
|
85
|
+
# guess through.
|
|
86
|
+
def resolve_by_short_session(global_store:, project_roots:, short_session:,
|
|
87
|
+
ttl: Lock::TTL_SECONDS, now: Time.now)
|
|
88
|
+
held = candidate_intent_dirs(global_store: global_store, project_roots: project_roots)
|
|
89
|
+
.uniq
|
|
90
|
+
.select { |dir| held_live_by_short?(dir, short_session: short_session, ttl: ttl, now: now) }
|
|
91
|
+
held.size == 1 ? held.first : nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def held_live_by_short?(intent_dir, short_session:, ttl:, now:)
|
|
95
|
+
return false unless Lock.fresh?(intent_dir, ttl: ttl, now: now)
|
|
96
|
+
|
|
97
|
+
data = Lock.read(intent_dir)
|
|
98
|
+
return false unless data.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
sessions = [data["owner_session"]] + Array(data["delegates"])
|
|
101
|
+
sessions.compact.map(&:to_s).any? { |session| session.start_with?(short_session.to_s) }
|
|
102
|
+
rescue StandardError
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
end
|