@zalom/plastic 2.0.0-alpha.26 → 2.0.0-alpha.28
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 -139
- package/README.md +337 -133
- package/agents/plastic-enforcer.md +12 -12
- package/agents/plastic-executor.md +2 -1
- package/agents/plastic-node-research.md +4 -2
- package/agents/plastic-node-verify.md +1 -0
- package/agents/plastic-node-work.md +1 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +6 -8
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +35 -1
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +6 -0
- package/bin/plastic-skill-census +114 -0
- 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 +7 -7
- package/{skills/conventions/references → docs/help}/completion-and-done.md +1 -1
- package/{skills/auto/references → docs/help}/human-report-contract.md +17 -19
- package/{skills/conventions/references → docs/help}/roadmaps.md +2 -2
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +5 -5
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/package.json +3 -2
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +10 -9
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +57 -49
- package/scripts/end-intent +2 -8
- package/scripts/file-session-intent +2 -1
- package/scripts/hook-capture +4 -3
- package/scripts/hook-close +2 -1
- package/scripts/hook-record +3 -2
- package/scripts/hook-savepoint +3 -2
- package/scripts/hook-session-start +5 -4
- package/scripts/hook-stop +2 -1
- package/scripts/insight-append +1 -2
- package/scripts/install.rb +7 -5
- package/scripts/lib/action_graph_shim.rb +3 -1
- package/scripts/lib/active_delivery.rb +1 -1
- package/scripts/lib/agent_models.rb +44 -26
- package/scripts/lib/arm.rb +2 -1
- package/scripts/lib/backup.rb +63 -0
- package/scripts/lib/cli/command.rb +85 -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 +60 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +25 -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 +20 -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 +61 -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 +25 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +43 -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 +49 -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 +25 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +44 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +34 -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 +84 -0
- package/scripts/lib/cli/legacy.rb +50 -0
- package/scripts/lib/cli/output.rb +102 -0
- package/scripts/lib/cli/scope.rb +127 -0
- package/scripts/lib/cli/table.rb +64 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +5 -4
- package/scripts/lib/compact_instructions.rb +8 -0
- package/scripts/lib/day_summary.rb +4 -3
- package/scripts/lib/doctor_core.rb +7 -32
- package/scripts/lib/doctor_session_ledger.rb +2 -1
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_measure_models.rb +4 -2
- package/scripts/lib/index_entry.rb +9 -0
- package/scripts/lib/installer_core.rb +175 -56
- package/scripts/lib/intent_screen.rb +3 -3
- package/scripts/lib/lock.rb +2 -2
- package/scripts/lib/node_file.rb +22 -2
- package/scripts/lib/node_input.rb +11 -6
- package/scripts/lib/node_return.rb +8 -3
- package/scripts/lib/preflight.rb +2 -4
- package/scripts/lib/project_config.rb +2 -1
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +2 -0
- package/scripts/lib/report_screen.rb +4 -3
- 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_queue.rb +2 -2
- package/scripts/lib/roadmap_savepoint.rb +1 -1
- package/scripts/lib/runner_absorb.rb +59 -2
- package/scripts/lib/runner_dispatch.rb +12 -8
- package/scripts/lib/runner_policy.rb +28 -10
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_git.rb +4 -3
- package/scripts/lib/sqlite.rb +22 -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 +2 -7
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/worktree.rb +3 -8
- package/scripts/lib/worktree_sweep.rb +3 -2
- package/scripts/link-suggest +2 -1
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +3 -12
- package/scripts/node-run +2 -1
- package/scripts/plastic-lock +3 -2
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +44 -6
- package/scripts/release-check +10 -5
- package/scripts/report-screen +1 -1
- package/scripts/session-commit +2 -1
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +25 -4
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +6 -6
- package/templates/config.yml +13 -6
- package/templates/node-research.md +4 -2
- package/templates/render.css +10 -0
- package/bin/plastic.js +0 -70
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -297
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -64
- package/skills/conventions/SKILL.md +0 -29
- package/skills/dashboard/SKILL.md +0 -180
- 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 -305
- 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 -156
- package/skills/intent-continuing/references/board-fill.md +0 -52
- package/skills/intent-continuing/references/boarding-matrix.md +0 -35
- 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 -89
- 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 -182
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -87
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -47
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -136
- 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 -376
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -70
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -90
- package/skills/roadmap/references/file-format.md +0 -134
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -66
- 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}/knowledge-graph.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +0 -0
- /package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "yaml"
|
|
5
6
|
|
|
6
7
|
# ProjectValidator - the single source of truth for "is a project spawn
|
|
@@ -10,7 +11,7 @@ require "yaml"
|
|
|
10
11
|
# missing the pieces a real project needs: project.yml, a root AGENTS.md.
|
|
11
12
|
# The intent-26 spawn shipped exactly that shape and was caught only by a
|
|
12
13
|
# much later, pull-only plastic-doctor sweep. This module lets
|
|
13
|
-
# plastic
|
|
14
|
+
# `plastic project new` verify a spawn BEFORE announcing it as done,
|
|
14
15
|
# mirroring how scripts/new-intent already runs IntentValidator before
|
|
15
16
|
# announcing a new intent (validate-intent).
|
|
16
17
|
#
|
|
@@ -52,7 +53,7 @@ module ProjectValidator
|
|
|
52
53
|
errors << "registered project directory does not exist: #{project_path}"
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
project_dir =
|
|
56
|
+
project_dir = Plastic::StoreLayout.project_root(plastic_home, slug)
|
|
56
57
|
|
|
57
58
|
# Invariant 3: project.yml exists AND parses as YAML.
|
|
58
59
|
project_yml_path = File.join(project_dir, "project.yml")
|
package/scripts/lib/qmd_sync.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "yaml"
|
|
5
6
|
require "json"
|
|
6
7
|
|
|
@@ -50,7 +51,7 @@ module QmdSync
|
|
|
50
51
|
# to the directory's parent name when no registry match exists.
|
|
51
52
|
def collection_name(store_dir, plastic_home:)
|
|
52
53
|
store_dir = File.expand_path(store_dir)
|
|
53
|
-
global_store = File.expand_path(
|
|
54
|
+
global_store = File.expand_path(Plastic::StoreLayout.global_store(plastic_home))
|
|
54
55
|
return "plastic-global" if store_dir == global_store
|
|
55
56
|
|
|
56
57
|
if File.basename(store_dir) == "roadmaps"
|
|
@@ -69,7 +70,7 @@ module QmdSync
|
|
|
69
70
|
def enumerate_stores(plastic_home:)
|
|
70
71
|
stores = [{
|
|
71
72
|
collection: "plastic-global",
|
|
72
|
-
dir: File.expand_path(
|
|
73
|
+
dir: File.expand_path(Plastic::StoreLayout.global_store(plastic_home)),
|
|
73
74
|
}]
|
|
74
75
|
|
|
75
76
|
projects = load_projects(plastic_home)
|
|
@@ -80,7 +81,7 @@ module QmdSync
|
|
|
80
81
|
# Project stores live under ~/.plastic/projects/<slug>/store as the mirror;
|
|
81
82
|
# registry `path` is the project code dir, so the tactical store is the
|
|
82
83
|
# plastic_home projects mirror.
|
|
83
|
-
mirror_store = File.expand_path(File.join(plastic_home,
|
|
84
|
+
mirror_store = File.expand_path(File.join(Plastic::StoreLayout.project_root(plastic_home, slug.to_s), "store"))
|
|
84
85
|
dir = Dir.exist?(mirror_store) ? mirror_store : project_store
|
|
85
86
|
stores << { collection: "plastic-#{slug}", dir: dir }
|
|
86
87
|
end
|
|
@@ -245,13 +246,13 @@ module QmdSync
|
|
|
245
246
|
path = info.is_a?(Hash) ? info["path"] : nil
|
|
246
247
|
next unless path
|
|
247
248
|
project_root = File.expand_path(path)
|
|
248
|
-
mirror = File.expand_path(File.join(plastic_home,
|
|
249
|
+
mirror = File.expand_path(File.join(Plastic::StoreLayout.project_root(plastic_home, slug.to_s), "store"))
|
|
249
250
|
return slug.to_s if store_dir == File.join(project_root, "store") || store_dir == mirror
|
|
250
251
|
end
|
|
251
252
|
# Fallback: <...>/projects/<slug>/store -> slug, else parent dir name.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
slug = Plastic::StoreLayout.locate(store_dir).last
|
|
254
|
+
return slug unless slug == Plastic::StoreLayout::GLOBAL
|
|
255
|
+
|
|
255
256
|
File.basename(File.dirname(store_dir))
|
|
256
257
|
end
|
|
257
258
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require_relative "sqlite"
|
|
5
|
+
|
|
6
|
+
module Plastic
|
|
7
|
+
module ReferenceArchive
|
|
8
|
+
NAME = "references.db"
|
|
9
|
+
SCHEMA = <<~SQL
|
|
10
|
+
CREATE TABLE IF NOT EXISTS sqlar(name TEXT PRIMARY KEY, mode INT, mtime INT, sz INT, data BLOB, intent_id TEXT, sha256 TEXT);
|
|
11
|
+
SQL
|
|
12
|
+
INTENT_ID = %r{/([^/-]+)--[^/]+/}
|
|
13
|
+
SKIPPED = %r{(\A|/)(\.git|\.tmp)/|\.DS_Store\z|\.lock\z}
|
|
14
|
+
|
|
15
|
+
def self.path(home)
|
|
16
|
+
File.join(home, NAME)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.files(home)
|
|
20
|
+
Dir.glob("{store,projects,stores}/**/*", File::FNM_DOTMATCH, base: home)
|
|
21
|
+
.select { |file| File.file?(File.join(home, file)) && File.extname(file) != ".md" && !file.match?(SKIPPED) }.sort
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.build(home)
|
|
25
|
+
known = Sqlite.call(path(home), "#{SCHEMA}SELECT name, mtime, sz FROM sqlar;").to_h { |row| [row["name"], row.values_at("mtime", "sz")] }
|
|
26
|
+
changed = files(home).reject { |file| known[file] == stamp(File.join(home, file)) }
|
|
27
|
+
Sqlite.call(path(home), ["BEGIN;", *changed.map { |file| insert(home, file) }, "COMMIT;"].join("\n"))
|
|
28
|
+
changed.size
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.stamp(source)
|
|
32
|
+
stat = File.stat(source)
|
|
33
|
+
[stat.mtime.to_i, stat.size]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.insert(home, file)
|
|
37
|
+
source = File.join(home, file)
|
|
38
|
+
stat = File.stat(source)
|
|
39
|
+
id = file[INTENT_ID, 1]
|
|
40
|
+
read = "readfile(#{Sqlite.quote(source)})"
|
|
41
|
+
"INSERT OR REPLACE INTO sqlar VALUES(#{Sqlite.quote(file)}, #{stat.mode}, #{stat.mtime.to_i}, length(#{read}), " \
|
|
42
|
+
"sqlar_compress(#{read}), #{id ? Sqlite.quote(id) : "NULL"}, #{Sqlite.quote(Digest::SHA256.file(source).hexdigest)});"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -52,6 +52,8 @@ module ReleaseGuard
|
|
|
52
52
|
# to the stable channel. Pure function, no ENV reads, shared by
|
|
53
53
|
# scripts/release-check and the test suite so the channel rule has exactly
|
|
54
54
|
# one implementation (intent 347).
|
|
55
|
+
CHANNELS = {"alpha" => "alpha", "beta" => "beta", "main" => "latest"}.freeze
|
|
56
|
+
|
|
55
57
|
def self.dist_tag(version)
|
|
56
58
|
suffix = version[/-(.+)\z/, 1]
|
|
57
59
|
return "latest" if suffix.nil?
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
# out. Dependency injection for anything reaching outside the fixture: the
|
|
9
9
|
# clock is passed as `now:`, git tag reading as `tag_reader:`, and the ANSI
|
|
10
10
|
# renderer path as `renderer_path:` (D2).
|
|
11
|
+
require_relative "store_layout"
|
|
11
12
|
require "time"
|
|
12
13
|
require "json"
|
|
13
14
|
require "date"
|
|
@@ -805,14 +806,14 @@ def self.matching_action_heading(intent_dir, label)
|
|
|
805
806
|
# (<home>/projects/<slug>/store/<id--slug>); nil otherwise (a global-store
|
|
806
807
|
# intent, a project with no `flow:` key, or malformed YAML). Pure: no git,
|
|
807
808
|
# no shell-out, just the one file this intent's own layout already reads.
|
|
808
|
-
PROJECT_LAYOUT_RE = %r{\A(.*)/projects/([^/]+)/store/[^/]+\z}.freeze
|
|
809
|
+
PROJECT_LAYOUT_RE = %r{\A(.*)/(?:projects|stores)/([^/]+)/store/[^/]+\z}.freeze
|
|
809
810
|
|
|
810
811
|
def self.flow_base(intent_dir)
|
|
811
812
|
m = intent_dir.to_s.match(PROJECT_LAYOUT_RE)
|
|
812
813
|
return nil unless m
|
|
813
814
|
|
|
814
815
|
home, slug = m[1], m[2]
|
|
815
|
-
path = File.join(home,
|
|
816
|
+
path = File.join(Plastic::StoreLayout.project_root(home, slug), "project.yml")
|
|
816
817
|
return nil unless File.exist?(path)
|
|
817
818
|
|
|
818
819
|
require "yaml"
|
|
@@ -1480,7 +1481,7 @@ def self.matching_action_heading(intent_dir, label)
|
|
|
1480
1481
|
|
|
1481
1482
|
# D5: "global" is <home> itself; any other slug is <home>/projects/<slug>.
|
|
1482
1483
|
def self.store_for_slug(home, slug)
|
|
1483
|
-
slug == "global" ? home :
|
|
1484
|
+
slug == "global" ? home : Plastic::StoreLayout.project_root(home, slug)
|
|
1484
1485
|
end
|
|
1485
1486
|
|
|
1486
1487
|
# D4: the newest valid day directory that is not in the future, when
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "corpus"
|
|
4
|
+
|
|
5
|
+
module RLM
|
|
6
|
+
class Probe
|
|
7
|
+
STOP_WORDS = %w[a an and are as at be by do does for from how in is it of on or that the this to was what when where which who why with].freeze
|
|
8
|
+
DEFAULT_LIMIT = 10
|
|
9
|
+
|
|
10
|
+
def initialize(corpus, limit: DEFAULT_LIMIT)
|
|
11
|
+
@corpus = corpus
|
|
12
|
+
@limit = limit
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(question)
|
|
16
|
+
hits = terms(question).flat_map { |term| @corpus.call([term], @limit) }
|
|
17
|
+
hits.group_by { |row| row["id"] }.values
|
|
18
|
+
.map { |rows| rows.first.merge("score" => rows.size) }
|
|
19
|
+
.sort_by.with_index { |row, index| [-row["score"], index] }
|
|
20
|
+
.first(@limit)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def terms(question)
|
|
26
|
+
question.downcase.scan(/[[:alnum:]]+/).uniq - STOP_WORDS
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "probe"
|
|
4
|
+
|
|
5
|
+
module RLM
|
|
6
|
+
class Query
|
|
7
|
+
DEFAULT_LIMIT = 3
|
|
8
|
+
|
|
9
|
+
def initialize(probe, judge: nil, limit: DEFAULT_LIMIT)
|
|
10
|
+
@probe = probe
|
|
11
|
+
@judge = judge
|
|
12
|
+
@limit = limit
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(question)
|
|
16
|
+
rows = @probe.call(question)
|
|
17
|
+
return rows unless @judge && rows.size > @limit
|
|
18
|
+
|
|
19
|
+
@judge.call(question, rows)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -411,8 +411,8 @@ class RoadmapQueue
|
|
|
411
411
|
# --- scope + payload -----------------------------------------------------------
|
|
412
412
|
|
|
413
413
|
def scope_label
|
|
414
|
-
m = @roadmaps_dir.to_s.match(%r{/projects/([^/]+)/roadmaps/?\z})
|
|
415
|
-
m ? "project:#{m[1]}" : "global"
|
|
414
|
+
m = @roadmaps_dir.to_s.match(%r{/(?:projects|stores)/([^/]+)/roadmaps/?\z})
|
|
415
|
+
(m && m[1] != "global") ? "project:#{m[1]}" : "global"
|
|
416
416
|
end
|
|
417
417
|
|
|
418
418
|
def payload(mode:, state:, roadmap:, frontier_wave:, dispatchable:, in_flight:, blocked:, tie:, tie_candidates:)
|
|
@@ -224,7 +224,7 @@ module RoadmapSavepoint
|
|
|
224
224
|
|
|
225
225
|
# The tier root is the parent of `roadmaps/` (a live roadmap's grandparent, or, for one
|
|
226
226
|
# already moved to `roadmaps/archived/`, its great-grandparent); INDEX.md is that root's
|
|
227
|
-
# sibling file, matching the layout
|
|
227
|
+
# sibling file, matching the layout docs/help/roadmaps.md defines.
|
|
228
228
|
def index_path_for(roadmap_path)
|
|
229
229
|
dir = File.dirname(roadmap_path)
|
|
230
230
|
dir = File.dirname(dir) if File.basename(dir) == "archived"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "yaml"
|
|
5
6
|
require "date"
|
|
6
7
|
require "time"
|
|
@@ -59,7 +60,7 @@ module RunnerAbsorb
|
|
|
59
60
|
# to plain strings), this cap is purely about line length.
|
|
60
61
|
FINDING_CAP = 200
|
|
61
62
|
|
|
62
|
-
PROJECT_LAYOUT_RE = %r{\A(.*)/projects/([^/]+)/store/[^/]+\z}.freeze
|
|
63
|
+
PROJECT_LAYOUT_RE = %r{\A(.*)/(?:projects|stores)/([^/]+)/store/[^/]+\z}.freeze
|
|
63
64
|
|
|
64
65
|
# absorb(context, node:, return_path:) -> a result hash describing the one
|
|
65
66
|
# transition it wrote (or attempted to write). Never raises across its own
|
|
@@ -100,6 +101,7 @@ module RunnerAbsorb
|
|
|
100
101
|
node_decl = ((context.graph || {})[:nodes] || {})[node] || {}
|
|
101
102
|
kind = node_decl[:kind]
|
|
102
103
|
declared_files = normalize_files(node_decl[:files])
|
|
104
|
+
report_path = declared_report_path(intent_dir, node)
|
|
103
105
|
|
|
104
106
|
# v2 NEW-2: a `graph.md` that failed to parse, or a node whose kind
|
|
105
107
|
# cannot be resolved from it, refuses right here - before ANY check
|
|
@@ -143,6 +145,11 @@ module RunnerAbsorb
|
|
|
143
145
|
return fail_check(savepoint_path, context, node, "node_mismatch", checks_ran, holder, extra_fields, now, ledger)
|
|
144
146
|
end
|
|
145
147
|
|
|
148
|
+
report_reason = report_contract_violation(kind, report_path, parsed)
|
|
149
|
+
if report_reason
|
|
150
|
+
return fail_check(savepoint_path, context, node, report_reason, checks_ran, holder, extra_fields, now, ledger)
|
|
151
|
+
end
|
|
152
|
+
|
|
146
153
|
append_findings(intent_dir, node, parsed.findings, now: now)
|
|
147
154
|
|
|
148
155
|
proposal_result = nil
|
|
@@ -267,6 +274,14 @@ module RunnerAbsorb
|
|
|
267
274
|
end
|
|
268
275
|
|
|
269
276
|
commit_value = merge_commit || parsed.commit
|
|
277
|
+
if report_path
|
|
278
|
+
report_result = persist_report(intent_dir, report_path, parsed.report)
|
|
279
|
+
unless report_result[:ok]
|
|
280
|
+
return finish.call(fail_check(savepoint_path, context, node, report_result[:reason], gates.split("+") + ["report"],
|
|
281
|
+
holder, extra_fields, now, ledger))
|
|
282
|
+
end
|
|
283
|
+
gates = "#{gates}+report"
|
|
284
|
+
end
|
|
270
285
|
fields = { gates: gates, commit: commit_value, holder: holder, suite: suite_value }.merge(extra_fields)
|
|
271
286
|
result = write_transition(savepoint_path, context, node, "done", fields, now: now, ledger: ledger)
|
|
272
287
|
|
|
@@ -420,6 +435,48 @@ module RunnerAbsorb
|
|
|
420
435
|
end
|
|
421
436
|
private_class_method :normalize_scope_path
|
|
422
437
|
|
|
438
|
+
# --- research report -------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
def declared_report_path(intent_dir, node)
|
|
441
|
+
path = ReadySet.find_node_path(intent_dir, node)
|
|
442
|
+
return nil unless path
|
|
443
|
+
|
|
444
|
+
parsed = NodeFile.parse(path)
|
|
445
|
+
parsed[:ok] ? parsed[:report] : nil
|
|
446
|
+
end
|
|
447
|
+
private_class_method :declared_report_path
|
|
448
|
+
|
|
449
|
+
def report_contract_violation(kind, declared, parsed)
|
|
450
|
+
return "report_not_declared" if parsed.report && declared.nil?
|
|
451
|
+
return nil unless parsed.status == "done"
|
|
452
|
+
return "report_missing" if declared && parsed.report.to_s.strip.empty?
|
|
453
|
+
return "report_not_research" if parsed.report && kind.to_s != "research"
|
|
454
|
+
|
|
455
|
+
nil
|
|
456
|
+
end
|
|
457
|
+
private_class_method :report_contract_violation
|
|
458
|
+
|
|
459
|
+
def persist_report(intent_dir, relative_path, content)
|
|
460
|
+
resources_root = File.expand_path(File.join(intent_dir, "resources"))
|
|
461
|
+
target = File.expand_path(File.join(intent_dir, relative_path.to_s))
|
|
462
|
+
unless target.start_with?("#{resources_root}/") && target.end_with?(".md")
|
|
463
|
+
return { ok: false, reason: "report_path_invalid" }
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
if File.exist?(target)
|
|
467
|
+
return { ok: true, path: target } if File.binread(target) == content.to_s
|
|
468
|
+
|
|
469
|
+
return { ok: false, reason: "report_conflict" }
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
473
|
+
AtomicWrite.write(target, content.to_s)
|
|
474
|
+
{ ok: true, path: target }
|
|
475
|
+
rescue StandardError
|
|
476
|
+
{ ok: false, reason: "report_write_failed" }
|
|
477
|
+
end
|
|
478
|
+
private_class_method :persist_report
|
|
479
|
+
|
|
423
480
|
# --- named tests -----------------------------------------------------------
|
|
424
481
|
|
|
425
482
|
# Row 4.19/minor 1: read the node's own failure-mode matrix table, take
|
|
@@ -619,7 +676,7 @@ module RunnerAbsorb
|
|
|
619
676
|
return nil unless m
|
|
620
677
|
|
|
621
678
|
home, slug = m[1], m[2]
|
|
622
|
-
path = File.join(home,
|
|
679
|
+
path = File.join(Plastic::StoreLayout.project_root(home, slug), "project.yml")
|
|
623
680
|
return nil unless File.exist?(path)
|
|
624
681
|
|
|
625
682
|
data = begin
|
|
@@ -37,7 +37,7 @@ module RunnerDispatch
|
|
|
37
37
|
RETURN_CONTRACT = <<~TEXT.freeze
|
|
38
38
|
RETURN CONTRACT: reply with exactly one YAML document as your final
|
|
39
39
|
message, nothing else around it. Keys: node, status, commit, summary,
|
|
40
|
-
findings, proposed_nodes, proposed_edges, question, reason. status is one
|
|
40
|
+
findings, report, proposed_nodes, proposed_edges, question, reason. status is one
|
|
41
41
|
of done, failed_verification, needs_decision, blocked. done requires
|
|
42
42
|
commit; needs_decision requires question; failed_verification and
|
|
43
43
|
blocked require reason. Anything that does not parse under this closed
|
|
@@ -47,7 +47,7 @@ module RunnerDispatch
|
|
|
47
47
|
HARD_CAP_RE = /\Ais at its dispatch cap \((\d+)\/(\d+)\)\z/.freeze
|
|
48
48
|
|
|
49
49
|
# D8 (355, n6): the agent every dispatched, non-decision node names - a
|
|
50
|
-
# role, never a harness (matrix 6.5), and never
|
|
50
|
+
# role, never a harness (matrix 6.5), and never a consultation advisor, which
|
|
51
51
|
# stays a deliberate, never-auto-dispatched consultation agent.
|
|
52
52
|
SPAWN_AGENT = "plastic-executor"
|
|
53
53
|
|
|
@@ -56,8 +56,8 @@ module RunnerDispatch
|
|
|
56
56
|
# (NodeInput.test_command_block, n4), and the call cap (n2) - fenced so a
|
|
57
57
|
# session pastes it straight into the Agent tool (327 D42: the runner
|
|
58
58
|
# itself never spawns).
|
|
59
|
-
def spawn_block(model:, input:, test_command:, call_cap:, agent: SPAWN_AGENT)
|
|
60
|
-
lines = ["agent: #{agent}", "model: #{model}", "input: #{input}", test_command,
|
|
59
|
+
def spawn_block(model:, input:, test_command:, call_cap:, effort: AgentModels::DEFAULT_EFFORT, agent: SPAWN_AGENT)
|
|
60
|
+
lines = ["agent: #{agent}", "model: #{model}", "effort: #{effort}", "input: #{input}", test_command,
|
|
61
61
|
NodeInput.call_cap_sentence(call_cap)]
|
|
62
62
|
(["```"] + lines + ["```"]).join("\n")
|
|
63
63
|
end
|
|
@@ -220,7 +220,8 @@ module RunnerDispatch
|
|
|
220
220
|
savepoint_path = File.join(intent_dir.to_s, "savepoint.md")
|
|
221
221
|
|
|
222
222
|
holder = context.session
|
|
223
|
-
model = RunnerPolicy.model_for(kind, config: config)
|
|
223
|
+
model = RunnerPolicy.model_for(kind, config: config, harness: harness)
|
|
224
|
+
effort = RunnerPolicy.effort_for(kind, config: config, harness: harness)
|
|
224
225
|
expires = RunnerPolicy.lease_expires(kind, now: now)
|
|
225
226
|
calls_cap = RunnerPolicy.call_cap(kind, config: config)
|
|
226
227
|
|
|
@@ -267,7 +268,7 @@ module RunnerDispatch
|
|
|
267
268
|
# Row 1.19/1.20/D21: harness= rides alongside model= on every `running`
|
|
268
269
|
# line, resolved once by the caller through HarnessAdapter and threaded
|
|
269
270
|
# straight through here - never re-resolved, never a literal.
|
|
270
|
-
fields = { holder: holder, expires: expires, input: build_result[:sha], model: model, harness: harness,
|
|
271
|
+
fields = { holder: holder, expires: expires, input: build_result[:sha], model: model, effort: effort, harness: harness,
|
|
271
272
|
calls: calls_cap }
|
|
272
273
|
|
|
273
274
|
result = begin
|
|
@@ -287,11 +288,13 @@ module RunnerDispatch
|
|
|
287
288
|
end
|
|
288
289
|
|
|
289
290
|
test_command = NodeInput.test_command_block(intent_dir: intent_dir, files: (nodes_decl[node] || {})[:files])
|
|
290
|
-
spawn = spawn_block(model: model, input: build_result[:path], test_command: test_command,
|
|
291
|
+
spawn = spawn_block(model: model, effort: effort, input: build_result[:path], test_command: test_command,
|
|
292
|
+
call_cap: calls_cap)
|
|
291
293
|
|
|
292
294
|
{
|
|
293
295
|
ok: true,
|
|
294
|
-
entry: { node: node, kind: kind.to_s, role: role_for(kind), model: model,
|
|
296
|
+
entry: { node: node, kind: kind.to_s, role: role_for(kind), model: model, effort: effort,
|
|
297
|
+
worktree: provisioned[:path],
|
|
295
298
|
input: build_result[:path], spawn: spawn },
|
|
296
299
|
}
|
|
297
300
|
end
|
|
@@ -507,6 +510,7 @@ module RunnerDispatch
|
|
|
507
510
|
"return_contract" => RETURN_CONTRACT,
|
|
508
511
|
"dispatch" => dispatched.map do |d|
|
|
509
512
|
{ "node" => d[:node], "kind" => d[:kind], "role" => d[:role], "model" => d[:model],
|
|
513
|
+
"effort" => d[:effort],
|
|
510
514
|
"worktree" => d[:worktree], "input" => d[:input] }
|
|
511
515
|
end,
|
|
512
516
|
"spawn" => dispatched.map { |d| d[:spawn] }
|
|
@@ -35,7 +35,7 @@ module RunnerPolicy
|
|
|
35
35
|
# resolves through AgentModels::TIER_DEFAULTS, the one place
|
|
36
36
|
# `plastic-executor`'s shipped tier is already declared (post-execution
|
|
37
37
|
# review minor 4), rather than a second, independently-drifting literal
|
|
38
|
-
# here. `plastic-advisor` carries no lifecycle-stage entry in
|
|
38
|
+
# here. `plastic-primary-advisor` carries no lifecycle-stage entry in
|
|
39
39
|
# TIER_DEFAULTS at all (it is a consultation agent, never auto-dispatched
|
|
40
40
|
# - see agent_models.rb's own docstring), so its default stays the one
|
|
41
41
|
# literal this table cannot source from anywhere else.
|
|
@@ -43,7 +43,7 @@ module RunnerPolicy
|
|
|
43
43
|
DEFAULT_ADVISOR_MODEL = "opus"
|
|
44
44
|
|
|
45
45
|
EXECUTOR_CONFIG_KEY = "plastic-executor"
|
|
46
|
-
ADVISOR_CONFIG_KEY = "plastic-advisor"
|
|
46
|
+
ADVISOR_CONFIG_KEY = "plastic-primary-advisor"
|
|
47
47
|
|
|
48
48
|
# {model_role:, worktree:, retry_cap:, diff_rule:, lease_minutes:} per kind
|
|
49
49
|
# (327 D12). `decision` carries no retry cap or lease: it is never
|
|
@@ -69,24 +69,42 @@ module RunnerPolicy
|
|
|
69
69
|
# verify resolves the advisor model, and a config with no override falls
|
|
70
70
|
# back to the shipped default rather than an empty string (`running`
|
|
71
71
|
# requires a non-blank `model=`).
|
|
72
|
-
def model_for(kind, config: {})
|
|
73
|
-
policy_for(kind)[:model_role] == :advisor ? advisor_model(config: config
|
|
72
|
+
def model_for(kind, config: {}, harness: "claude-code")
|
|
73
|
+
alias_or_id = policy_for(kind)[:model_role] == :advisor ? advisor_model(config: config, harness: harness) :
|
|
74
|
+
executor_model(config: config, harness: harness)
|
|
75
|
+
codex_harness?(harness) ? (AgentModels.codex_model_for(alias_or_id) || alias_or_id) : alias_or_id
|
|
74
76
|
end
|
|
75
77
|
|
|
76
|
-
def executor_model(config: {})
|
|
77
|
-
resolve_model(config, EXECUTOR_CONFIG_KEY, DEFAULT_EXECUTOR_MODEL)
|
|
78
|
+
def executor_model(config: {}, harness: "claude-code")
|
|
79
|
+
resolve_model(config, EXECUTOR_CONFIG_KEY, DEFAULT_EXECUTOR_MODEL, harness)
|
|
78
80
|
end
|
|
79
81
|
|
|
80
|
-
def advisor_model(config: {})
|
|
81
|
-
resolve_model(config, ADVISOR_CONFIG_KEY, DEFAULT_ADVISOR_MODEL)
|
|
82
|
+
def advisor_model(config: {}, harness: "claude-code")
|
|
83
|
+
resolve_model(config, ADVISOR_CONFIG_KEY, DEFAULT_ADVISOR_MODEL, harness)
|
|
82
84
|
end
|
|
83
85
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
+
def effort_for(kind, config: {}, harness: "claude-code")
|
|
87
|
+
key = policy_for(kind)[:model_role] == :advisor ? ADVISOR_CONFIG_KEY : EXECUTOR_CONFIG_KEY
|
|
88
|
+
value = AgentModels.efforts_section(config, harness_name(harness))[key]
|
|
89
|
+
present?(value) ? value : AgentModels::DEFAULT_EFFORT
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def resolve_model(config, key, shipped_default, harness)
|
|
93
|
+
value = AgentModels.models_section(config, harness: harness_name(harness))[key]
|
|
86
94
|
present?(value) ? value : shipped_default
|
|
87
95
|
end
|
|
88
96
|
private_class_method :resolve_model
|
|
89
97
|
|
|
98
|
+
def harness_name(harness)
|
|
99
|
+
codex_harness?(harness) ? "codex" : "claude"
|
|
100
|
+
end
|
|
101
|
+
private_class_method :harness_name
|
|
102
|
+
|
|
103
|
+
def codex_harness?(harness)
|
|
104
|
+
harness.to_s == "codex"
|
|
105
|
+
end
|
|
106
|
+
private_class_method :codex_harness?
|
|
107
|
+
|
|
90
108
|
def present?(value)
|
|
91
109
|
!(value.nil? || value.to_s.strip.empty?)
|
|
92
110
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "sqlite"
|
|
5
|
+
|
|
6
|
+
module Plastic
|
|
7
|
+
module SearchIndex
|
|
8
|
+
NAME = "knowledge_graph.db"
|
|
9
|
+
SCHEMA = <<~SQL
|
|
10
|
+
CREATE TABLE doc(id INTEGER PRIMARY KEY, path TEXT, sz INT, data BLOB, hash TEXT);
|
|
11
|
+
CREATE VIEW doc_v AS SELECT id AS rowid, CAST(sqlar_uncompress(data, sz) AS TEXT) AS body FROM doc;
|
|
12
|
+
CREATE VIRTUAL TABLE ft USING fts5(body, content='doc_v', content_rowid='rowid');
|
|
13
|
+
SQL
|
|
14
|
+
FINISH = "INSERT INTO ft(ft) VALUES('rebuild'); INSERT INTO ft(ft) VALUES('optimize'); VACUUM;"
|
|
15
|
+
|
|
16
|
+
def self.path(home)
|
|
17
|
+
File.join(home, NAME)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.files(home)
|
|
21
|
+
Dir.glob("{store,projects,stores}/**/*.md", File::FNM_DOTMATCH, base: home).reject { |file| file.include?("/.git/") }.sort
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.build(home)
|
|
25
|
+
files = files(home)
|
|
26
|
+
draft = "#{path(home)}.tmp"
|
|
27
|
+
FileUtils.rm_f(draft)
|
|
28
|
+
Sqlite.call(draft, [SCHEMA, "BEGIN;", *files.map { |file| insert(home, file) }, "COMMIT;", FINISH].join("\n"))
|
|
29
|
+
File.rename(draft, path(home))
|
|
30
|
+
files.size
|
|
31
|
+
ensure
|
|
32
|
+
FileUtils.rm_f(draft.to_s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.insert(home, file)
|
|
36
|
+
source = quote(File.join(home, file))
|
|
37
|
+
"INSERT INTO doc(path, sz, data, hash) VALUES(#{quote(file)}, length(readfile(#{source})), " \
|
|
38
|
+
"sqlar_compress(readfile(#{source})), lower(hex(sha3(readfile(#{source}), 256))));"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.search(home, terms, limit:, prefix: "")
|
|
42
|
+
phrases = terms.map { |term| %("#{term.delete('"')}") }.join(" ")
|
|
43
|
+
Sqlite.call(path(home), <<~SQL, readonly: true)
|
|
44
|
+
SELECT doc.id AS id, doc.path AS path, snippet(ft, 0, '[', ']', ' ... ', 12) AS excerpt
|
|
45
|
+
FROM ft JOIN doc ON doc.id = ft.rowid
|
|
46
|
+
WHERE ft MATCH #{quote(phrases)} AND doc.path LIKE #{quote("#{prefix}%")}
|
|
47
|
+
ORDER BY rank LIMIT #{Integer(limit)};
|
|
48
|
+
SQL
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.quote(text)
|
|
52
|
+
Sqlite.quote(text)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require "yaml"
|
|
5
6
|
require_relative "worktree"
|
|
6
7
|
require_relative "scaffold_intent"
|
|
@@ -160,7 +161,7 @@ module SessionGit
|
|
|
160
161
|
def read_project_flow(slug, plastic_home)
|
|
161
162
|
return nil if slug == "global"
|
|
162
163
|
|
|
163
|
-
project_yml = File.join(plastic_home,
|
|
164
|
+
project_yml = File.join(Plastic::StoreLayout.project_root(plastic_home, slug), "project.yml")
|
|
164
165
|
return nil unless File.exist?(project_yml)
|
|
165
166
|
|
|
166
167
|
data = begin
|
|
@@ -196,7 +197,7 @@ module SessionGit
|
|
|
196
197
|
resolver ||= lambda { |short_session|
|
|
197
198
|
ActiveDelivery.resolve_by_short_session(
|
|
198
199
|
global_store: store,
|
|
199
|
-
project_roots: ActiveDelivery.project_roots(
|
|
200
|
+
project_roots: ActiveDelivery.project_roots(Plastic::StoreLayout.locate(store).first),
|
|
200
201
|
short_session: short_session
|
|
201
202
|
)
|
|
202
203
|
}
|
|
@@ -333,7 +334,7 @@ module SessionGit
|
|
|
333
334
|
# a detached HEAD. Both modes now share exactly one check of each.
|
|
334
335
|
def commit!(cwd:, summary:, day:, session:, plastic_home:, store: nil,
|
|
335
336
|
runner: Worktree::ShellRunner.new, gh_runner: GhRunner.new)
|
|
336
|
-
effective_store = store ||
|
|
337
|
+
effective_store = store || Plastic::StoreLayout.global_store(plastic_home)
|
|
337
338
|
repo = resolve_repo(cwd, runner: runner)
|
|
338
339
|
return note("no repo") if repo.nil?
|
|
339
340
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Plastic
|
|
7
|
+
module Sqlite
|
|
8
|
+
Error = Class.new(StandardError)
|
|
9
|
+
|
|
10
|
+
def self.quote(text)
|
|
11
|
+
"'#{text.gsub("'", "''")}'"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.call(path, sql, readonly: false)
|
|
15
|
+
flags = readonly ? ["-readonly"] : []
|
|
16
|
+
out, err, status = Open3.capture3("sqlite3", "-json", *flags, path, stdin_data: sql)
|
|
17
|
+
raise Error, err.strip unless status.success?
|
|
18
|
+
|
|
19
|
+
out.strip.empty? ? [] : JSON.parse(out)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# encoding: UTF-8
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "store_layout"
|
|
4
5
|
require_relative "store_provisioning"
|
|
5
6
|
|
|
6
7
|
# StoreDiscovery: the single source of truth for "what stores exist" (intent 189).
|
|
@@ -39,20 +40,20 @@ module StoreDiscovery
|
|
|
39
40
|
stores = []
|
|
40
41
|
missing = []
|
|
41
42
|
|
|
42
|
-
global_store =
|
|
43
|
+
global_store = Plastic::StoreLayout.global_store(plastic_home)
|
|
43
44
|
if File.directory?(global_store)
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
global_root = Plastic::StoreLayout.global_root(plastic_home)
|
|
46
|
+
stores << { key: "global", slug: "global", root: global_root,
|
|
47
|
+
store: global_store, index: File.join(global_root, "INDEX.md") }
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
registered = StoreProvisioning.load_projects(plastic_home) # { slug => info }, {} on error/absence
|
|
49
|
-
|
|
50
|
-
on_disk = File.directory?(projects_root) ? Dir.children(projects_root).reject { |e| e.start_with?(".") } : []
|
|
51
|
+
on_disk = Plastic::StoreLayout.project_slugs(plastic_home)
|
|
51
52
|
|
|
52
53
|
all_slugs = (registered.keys + on_disk).uniq.sort
|
|
53
54
|
|
|
54
55
|
all_slugs.each do |slug|
|
|
55
|
-
root =
|
|
56
|
+
root = Plastic::StoreLayout.project_root(plastic_home, slug)
|
|
56
57
|
store_dir = File.join(root, "store")
|
|
57
58
|
if File.directory?(store_dir)
|
|
58
59
|
stores << { key: "project:#{slug}", slug: slug, root: root,
|