@mriekki/lean 0.0.0-stage → 0.1.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.
Files changed (82) hide show
  1. package/README.md +66 -2
  2. package/assets/components.json +33 -0
  3. package/assets/core/distribution.json +51 -0
  4. package/assets/deployment/distribution.json +34 -0
  5. package/assets/generated/ASSEMBLY.json +382 -0
  6. package/assets/generated/core/core/agents/coach.agent.md +69 -0
  7. package/assets/generated/core/core/agents/reviewer.agent.md +85 -0
  8. package/assets/generated/core/core/instructions/lean-docs.instructions.md +14 -0
  9. package/assets/generated/core/core/instructions/lean-workflow.instructions.md +35 -0
  10. package/assets/generated/core/core/learning/CODING_DRILLS.md +35 -0
  11. package/assets/generated/core/core/learning/DIAGRAM_GUIDE.md +19 -0
  12. package/assets/generated/core/core/learning/ENGINEERING_LEARNING_WORKBOOK.md +82 -0
  13. package/assets/generated/core/core/learning/LEARNING_MAP.md +51 -0
  14. package/assets/generated/core/core/learning/LEARNING_PROGRESS.md +31 -0
  15. package/assets/generated/core/core/learning/RESOURCE_CATALOG.md +25 -0
  16. package/assets/generated/core/core/learning/SQL_DRILLS.md +27 -0
  17. package/assets/generated/core/core/learning/SYSTEM_DESIGN_DRILLS.md +21 -0
  18. package/assets/generated/core/core/process/COACHING_GUIDE.md +57 -0
  19. package/assets/generated/core/core/process/GUIDED_NEXT_STEPS.md +48 -0
  20. package/assets/generated/core/core/process/HUMAN_GATES.md +81 -0
  21. package/assets/generated/core/core/process/INTERACTIVE_SESSION_GUIDE.md +62 -0
  22. package/assets/generated/core/core/process/PRODUCTION_ENABLEMENT.md +59 -0
  23. package/assets/generated/core/core/process/PR_WORKFLOW.md +53 -0
  24. package/assets/generated/core/core/process/SETUP_ACCEPTANCE_CHECKLIST.md +72 -0
  25. package/assets/generated/core/core/process/WORK_ITEM_MODEL.md +56 -0
  26. package/assets/generated/core/core/skills/cleanup-branches/SKILL.md +190 -0
  27. package/assets/generated/core/core/skills/coach/SKILL.md +136 -0
  28. package/assets/generated/core/core/skills/code-walkthrough/SKILL.md +197 -0
  29. package/assets/generated/core/core/skills/core-sync/SKILL.md +220 -0
  30. package/assets/generated/core/core/skills/define-feature/SKILL.md +144 -0
  31. package/assets/generated/core/core/skills/deliver/SKILL.md +315 -0
  32. package/assets/generated/core/core/skills/deploy/SKILL.md +193 -0
  33. package/assets/generated/core/core/skills/grill-me/SKILL.md +77 -0
  34. package/assets/generated/core/core/skills/grill-with-docs/SKILL.md +90 -0
  35. package/assets/generated/core/core/skills/learning-review/SKILL.md +90 -0
  36. package/assets/generated/core/core/skills/next-step/SKILL.md +166 -0
  37. package/assets/generated/core/core/skills/production-readiness/SKILL.md +357 -0
  38. package/assets/generated/core/core/skills/pull-latest/SKILL.md +166 -0
  39. package/assets/generated/core/core/skills/refine-requirements/SKILL.md +207 -0
  40. package/assets/generated/core/core/skills/start-work/SKILL.md +178 -0
  41. package/assets/generated/core/core/skills/tear-down/SKILL.md +138 -0
  42. package/assets/generated/core/core/skills/upstream-fix/SKILL.md +206 -0
  43. package/assets/generated/core/core/skills/wait-what/SKILL.md +86 -0
  44. package/assets/generated/core/core/templates/FEATURE_BRIEF_TEMPLATE.md +27 -0
  45. package/assets/generated/core/core/templates/PRODUCT_BRIEF_TEMPLATE.md +27 -0
  46. package/assets/generated/core/core/templates/PULL_REQUEST_TEMPLATE.md +37 -0
  47. package/assets/generated/core/core/templates/REQUIREMENTS_TEMPLATE.md +17 -0
  48. package/assets/generated/core/profiles/greenfield/lean-workspace.json +12 -0
  49. package/assets/generated/core/tools/package/check_baseline.py +102 -0
  50. package/assets/generated/core/tools/package/check_upstream.py +171 -0
  51. package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -0
  52. package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -0
  53. package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -0
  54. package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -0
  55. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -0
  56. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +416 -0
  57. package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -0
  58. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -0
  59. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -0
  60. package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -0
  61. package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -0
  62. package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -0
  63. package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -0
  64. package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -0
  65. package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -0
  66. package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -0
  67. package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -0
  68. package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -0
  69. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +335 -0
  70. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +340 -0
  71. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +86 -0
  72. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -0
  73. package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -0
  74. package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -0
  75. package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -0
  76. package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -0
  77. package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -0
  78. package/bin/lean.mjs +9 -0
  79. package/package.json +31 -4
  80. package/src/cli.mjs +131 -0
  81. package/src/engine.mjs +243 -0
  82. package/src/package-data.mjs +18 -0
@@ -0,0 +1,206 @@
1
+ ---
2
+ name: upstream-fix
3
+ description: Route reusable bugs discovered in a consumer to the canonical shared repository, allow bounded upstream implementation, and return adoption to exact baseline sync.
4
+ argument-hint: "[optional problem summary]"
5
+ user-invocable: true
6
+ disable-model-invocation: true
7
+ ---
8
+
9
+ # Upstream Fix
10
+
11
+ `/upstream-fix` is for a problem discovered while working in a consumer repository when the likely owner is shared Lean code rather than consumer product code.
12
+
13
+ The governing rule is:
14
+
15
+ ```text
16
+ discover in the consumer
17
+ → diagnose/prove in the consumer
18
+ → fix in the owning canonical repository
19
+ → deliver there
20
+ → sync the accepted shared commit back into the consumer
21
+ → verify again in the consumer
22
+ ```
23
+
24
+ A temporary edit to a shared-managed file inside the consumer may be useful as diagnosis/proof, but it is never the canonical delivered fix.
25
+
26
+ ## 1. Invocation authorization
27
+
28
+ Invoking `/upstream-fix` authorizes:
29
+
30
+ - read-only ownership/provenance inspection in the consumer;
31
+ - classification of the problem to the owning layer;
32
+ - inspection of already-available local canonical repository candidates;
33
+ - when ownership and the intended patch are clear and a canonical checkout is safe, routine creation/use of one short-lived canonical work branch and application of only the bounded equivalent fix plus its focused tests/docs.
34
+
35
+ Invocation does **not** authorize:
36
+
37
+ - commit, push, PR creation, merge, release publication, or deployment;
38
+ - rewriting/discarding consumer work;
39
+ - changing more than one canonical repository in the same bounded fix;
40
+ - broad filesystem scans or cloning shared repositories merely to satisfy the helper;
41
+ - treating a temporary consumer patch as accepted shared implementation.
42
+
43
+ Normal review and `/deliver` occur later while focused on the owning canonical repository.
44
+
45
+ ## 2. Establish consumer provenance
46
+
47
+ From the consumer root inspect, when present:
48
+
49
+ - `CORE_BASELINE.json`;
50
+ - `DEPLOYMENT_BASELINE.json`;
51
+ - `.github/lean-workspace.json`;
52
+ - current Git branch/status/diff;
53
+ - the files involved in the reported problem or temporary proof.
54
+
55
+ Run repository-local managed-baseline checks when available:
56
+
57
+ ```text
58
+ python tools/lean-core/check-core-baseline.py --root .
59
+ python tools/lean-deployment/check-deployment-baseline.py
60
+ ```
61
+
62
+ Do not assume every generated-looking file is shared-owned. Use baseline managed-path records as the primary ownership evidence.
63
+
64
+ ## 3. Classify the owner
65
+
66
+ Use the smallest owning layer that would need the same fix for another consumer.
67
+
68
+ ### Consumer-owned
69
+
70
+ Examples: application domain logic, UI behavior, app-specific configuration, product tests, consumer architecture.
71
+
72
+ Result:
73
+
74
+ ```text
75
+ CONSUMER FIX
76
+ Owner: current repository
77
+ ```
78
+
79
+ Stop upstream routing and continue through the consumer's normal `/start-work` / implementation / review / `/deliver` flow.
80
+
81
+ ### Core-owned
82
+
83
+ Evidence includes a changed/problematic path managed by `CORE_BASELINE.json`, or behavior defined by canonical Lean workflow skills/agents/instructions.
84
+
85
+ Result:
86
+
87
+ ```text
88
+ CORE BUG
89
+ Owner: <repository recorded by CORE_BASELINE.json>
90
+ ```
91
+
92
+ ### Deployment-Kit-owned
93
+
94
+ Evidence includes a changed/problematic path managed by `DEPLOYMENT_BASELINE.json`, or reusable deployment behavior owned by the pinned Deployment Kit.
95
+
96
+ Result:
97
+
98
+ ```text
99
+ DEPLOYMENT BUG
100
+ Owner: <repository recorded by DEPLOYMENT_BASELINE.json>
101
+ ```
102
+
103
+ ### Generator/control-plane owned
104
+
105
+ If generated output is wrong but the managed payload itself is correct, classify the generator/orchestrator rather than patching the generated consumer. Resolve ownership from repository/project evidence; do not guess between Bootstrap and a control plane when evidence is insufficient.
106
+
107
+ If ownership is genuinely ambiguous, stop at `UPSTREAM OWNER UNCLEAR` and present the competing evidence. Do not write to multiple repositories speculatively.
108
+
109
+ ## 4. Temporary consumer proof
110
+
111
+ A temporary consumer edit to a shared-managed path is allowed for diagnosis when it helps prove the fix in the real application context.
112
+
113
+ Clearly report it as:
114
+
115
+ ```text
116
+ TEMPORARY SHARED-MANAGED PROOF
117
+ Consumer path: <path>
118
+ Canonical owner: <repo>
119
+ Do not deliver this consumer copy.
120
+ ```
121
+
122
+ Do not silently revert/discard the proof, because it may contain useful evidence. Do not stage/commit it as ordinary consumer implementation. Adoption later replaces the private proof with the accepted shared baseline.
123
+
124
+ ## 5. Resolve a canonical local checkout
125
+
126
+ For the classified owner, consider local candidates in this order:
127
+
128
+ 1. explicit path supplied by the human;
129
+ 2. an already-open workspace folder whose Git remote unambiguously matches the recorded canonical repository;
130
+ 3. a normal sibling checkout derived from the canonical repository name.
131
+
132
+ Do not broadly scan the filesystem. A multi-root maintainer workspace is a convenience, not a requirement.
133
+
134
+ A candidate is safe for bounded upstream implementation only when:
135
+
136
+ - it is the expected Git repository;
137
+ - Git state can be inspected;
138
+ - no merge/rebase/cherry-pick is in progress;
139
+ - unrelated local work is absent;
140
+ - branch state does not require reset/rebase/force/stash/discard operations.
141
+
142
+ If no safe local canonical checkout exists, report the exact canonical repository and the proven consumer evidence. Do not clone automatically just to continue.
143
+
144
+ ## 6. Port the bounded fix upstream
145
+
146
+ When the safe local canonical checkout exists and the equivalent fix is clear:
147
+
148
+ 1. inspect the canonical implementation and tests before editing;
149
+ 2. create/use a repository-appropriate short-lived branch tied to the upstream issue/work item when needed;
150
+ 3. reproduce the root cause against canonical code where practical;
151
+ 4. apply the reusable fix in the canonical source, not by copying consumer-specific names/configuration;
152
+ 5. add/update focused canonical tests that would have caught the defect;
153
+ 6. update canonical docs only when the contract changed;
154
+ 7. run relevant local checks when available;
155
+ 8. show the canonical meaningful diff and observed validation.
156
+
157
+ Preserve consumer specificity. A shared fix must not hard-code the consumer name, target, data, environment, route, or product requirement merely because that consumer exposed the bug.
158
+
159
+ If the consumer proof cannot be generalized safely, stop and reclassify or ask for a narrower canonical design rather than forcing the patch upstream.
160
+
161
+ ## 7. Handoff to canonical delivery
162
+
163
+ After the upstream implementation is ready, report:
164
+
165
+ ```text
166
+ UPSTREAM FIX READY
167
+
168
+ Owner: <canonical repo>
169
+ Branch: <branch>
170
+ Consumer proof: <path/status>
171
+ Validation: <observed checks>
172
+ Next: focus the canonical repository and run /deliver
173
+ ```
174
+
175
+ `/upstream-fix` does not commit/push/open/merge the canonical PR itself. `/deliver` retains the normal reviewed-diff and merge/finalize gates in exactly one repository.
176
+
177
+ ## 8. Adopt the accepted fix back into the consumer
178
+
179
+ After the canonical PR is merged and its accepted default-branch CI is green:
180
+
181
+ - Core-owned fixes return through `/core-sync` using the exact accepted Core commit/distribution;
182
+ - Deployment-Kit-owned fixes return through the canonical Deployment Kit consumer-sync capability and refresh `DEPLOYMENT_BASELINE.json`;
183
+ - generator/control-plane fixes are validated by regenerating or using that platform's canonical update path, not by manually editing generated managed files.
184
+
185
+ Then rerun the original consumer reproduction and baseline checks.
186
+
187
+ A successful consumer retest is evidence that the shared fix works in context; it does not turn the consumer copy into the canonical source.
188
+
189
+ ## 9. Consumer delivery guard
190
+
191
+ If `/deliver` is invoked in a consumer with unexplained Core- or Deployment-Kit-managed file drift, stop before staging/commit/push. Route reusable managed-file changes through `/upstream-fix` and accepted baseline sync first.
192
+
193
+ Consumer-owned changes may be delivered normally when managed baselines are clean.
194
+
195
+ ## 10. Boundaries
196
+
197
+ `/upstream-fix` never:
198
+
199
+ - merges changes;
200
+ - publishes releases/tags;
201
+ - deploys production;
202
+ - commits one change across multiple repositories;
203
+ - auto-stashes, resets, rebases, force-pushes, or discards work;
204
+ - turns a maintainer multi-root workspace into generated repository content;
205
+ - requires ordinary consumer users to clone shared repositories;
206
+ - treats a temporary shared-managed consumer edit as a legitimate permanent fork.
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: wait-what
3
+ description: "The last explanation did not land. Re-explain it with enough context to understand where we are and what happens next."
4
+ argument-hint: "[optional note about what was unclear]"
5
+ user-invocable: true
6
+ disable-model-invocation: true
7
+ ---
8
+
9
+ # Wait, What?
10
+
11
+ Purpose: repair communication when the immediately preceding explanation did not land.
12
+
13
+ This skill re-explains. It does not perform new engineering work.
14
+
15
+ ## 1. Use existing context first
16
+
17
+ Re-pitch the immediately preceding explanation, or the specific unclear point named by the human.
18
+
19
+ Prefer already-available conversation/repository context. Do not run commands, inspect new Git/GitHub state, modify files, or take the next workflow action merely to explain the prior response.
20
+
21
+ If a safe explanation depends on information that is genuinely missing, say what is missing instead of inventing it.
22
+
23
+ ## 2. Use plain technical English
24
+
25
+ Prefer:
26
+ - short, direct sentences;
27
+ - concrete nouns instead of vague pronouns;
28
+ - one idea per sentence when possible;
29
+ - familiar repository/work-item terminology;
30
+ - explicit cause-and-effect language;
31
+ - examples only when they make the explanation clearer.
32
+
33
+ Use a Simplified-Technical-English style, but do not claim formal ASD-STE100 compliance.
34
+
35
+ When repository glossary material exists, prefer its terminology. A glossary is helpful, not required.
36
+
37
+ ## 3. Re-establish orientation
38
+
39
+ For a substantive explanation, cover only what helps:
40
+
41
+ ```text
42
+ CURRENT CONTEXT
43
+ Where we are and what led here.
44
+
45
+ WHAT I MEANT
46
+ The important point in simpler language.
47
+
48
+ WHY IT MATTERS
49
+ Why the point affects the current work or decision.
50
+
51
+ WHAT HAPPENS NEXT
52
+ The immediate next step, if there is one.
53
+ ```
54
+
55
+ Do not force all headings for a tiny clarification. Keep the response proportional to the confusion.
56
+
57
+ ## 4. Respect human gates
58
+
59
+ If the unclear explanation immediately preceded a human approval or consequential-action gate, complete the re-explanation before asking for that approval again.
60
+
61
+ Do not treat `/wait-what` as approval to continue, write files, commit, push, merge, deploy, or call external systems.
62
+
63
+ ## 5. Route deeper needs without changing modes silently
64
+
65
+ If the human actually needs:
66
+ - current workflow-state orientation, suggest `/next-step`;
67
+ - deeper concept teaching, suggest `/coach`;
68
+ - a structured explanation of implementation changes, suggest `/code-walkthrough`;
69
+ - an understanding check, suggest `/learning-review`.
70
+
71
+ Do not silently switch into those modes unless the human asks.
72
+
73
+ ## 6. Boundaries
74
+
75
+ `/wait-what` is:
76
+ - read-only,
77
+ - user-invoked,
78
+ - non-blocking,
79
+ - communication-focused,
80
+ - usable during requirements, planning, implementation, review, delivery, setup, learning, or operations discussions.
81
+
82
+ It does not create a new engineering workflow state.
83
+
84
+ ## Provenance
85
+
86
+ Inspired by Matt Pocock's `wait-what` productivity skill in `mattpocock/skills` (MIT licensed). This Lean Core version is independently rewritten for repository-agnostic Human-in-the-Loop engineering workflows.
@@ -0,0 +1,27 @@
1
+ # Feature Brief — <Name>
2
+
3
+ > Authority: <AUTHORITATIVE | SUPPLEMENTAL / EXPERIMENTAL>
4
+ >
5
+ > Jira relationship: <Epic / Story set / other>
6
+ >
7
+ > A Jira Epic is not automatically equivalent to this Feature Brief.
8
+
9
+ ## Problem / User Outcome
10
+
11
+ ## Scope
12
+
13
+ ## Non-Scope
14
+
15
+ ## Workflows
16
+
17
+ ## Cross-Story / Domain Rules
18
+
19
+ ## Dependencies
20
+
21
+ ## Related Jira Epics / Stories
22
+
23
+ ## Acceptance Summary
24
+
25
+ ## Open Decisions
26
+
27
+ ## Approval / Status
@@ -0,0 +1,27 @@
1
+ # Product Brief — <Name>
2
+
3
+ > Authority: <AUTHORITATIVE | SUPPLEMENTAL / EXPERIMENTAL>
4
+ >
5
+ > External system relationship: <for example Jira Epics/Stories remain primary>
6
+
7
+ ## Problem / Opportunity
8
+
9
+ ## Users / Stakeholders
10
+
11
+ ## Desired Outcomes
12
+
13
+ ## Scope
14
+
15
+ ## Non-Scope
16
+
17
+ ## Major Workflows / Capabilities
18
+
19
+ ## Domain / Policy Constraints
20
+
21
+ ## Success Measures
22
+
23
+ ## Related Jira Epics / Work Items
24
+
25
+ ## Open Decisions
26
+
27
+ ## Approval / Status
@@ -0,0 +1,37 @@
1
+ <!--
2
+ Internal PR-body scaffolding for the Lean delivery workflow.
3
+ Populate from actual work-item, diff, test, review, and CI evidence.
4
+ Do not mark checks complete unless the evidence was observed.
5
+ -->
6
+
7
+ ## Work item
8
+
9
+ <!-- ID and concise title -->
10
+
11
+ ## What changed
12
+
13
+ -
14
+ -
15
+ -
16
+
17
+ ## Acceptance evidence
18
+
19
+ - [ ] Relevant local formatting/build/test/checks pass
20
+ - [ ] Relevant runtime/browser/container checks pass when applicable
21
+ - [ ] GitHub Actions passes
22
+
23
+ Mark items N/A in notes when they do not apply.
24
+
25
+ ## Review
26
+
27
+ - [ ] Lean Reviewer completed when required by the work classification
28
+ - [ ] Blocking findings resolved
29
+ - [ ] Human final PR diff review completed
30
+
31
+ ## Known limitations / follow-up
32
+
33
+ -
34
+
35
+ ## Notes
36
+
37
+ <!-- Keep concise. Link to authoritative work-item docs instead of duplicating requirements. -->
@@ -0,0 +1,17 @@
1
+ # Detailed Requirements — <Work Item>
2
+
3
+ > Use only when local detailed requirements are permitted and useful.
4
+ >
5
+ > Do not duplicate Jira/Issue authority without an explicit reason.
6
+
7
+ ## User / Engineering Outcomes
8
+
9
+ ## Domain / Engineering Requirements
10
+
11
+ ## Acceptance / Validation Scenarios
12
+
13
+ ## Open Decisions
14
+
15
+ ## Traceability
16
+
17
+ ## Authority / Approval
@@ -0,0 +1,12 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "profile": "greenfield",
4
+ "workItemSource": "local-docs",
5
+ "productBriefs": {"mode": "authoritative"},
6
+ "featureBriefs": {"mode": "authoritative"},
7
+ "engineeringEnablers": {"mode": "authoritative"},
8
+ "localDetailedRequirements": {"mode": "authoritative-combined"},
9
+ "jira": {"accessMode": "none", "epics": "not-used"},
10
+ "delivery": {"pr": true, "explicitFinalPrAcceptance": true, "explicitMerge": true, "postMergeCi": "observe-if-configured"},
11
+ "productionReadiness": {"enabled": "when-production-in-scope"}
12
+ }
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+
9
+ BASELINE_FILENAME = "CORE_BASELINE.json"
10
+
11
+
12
+ def sha256_file(path: Path) -> str:
13
+ digest = hashlib.sha256()
14
+ with path.open("rb") as handle:
15
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
16
+ digest.update(chunk)
17
+ return digest.hexdigest()
18
+
19
+
20
+ def resolve_managed_path(root: Path, rel: str) -> Path:
21
+ candidate = Path(rel)
22
+ if candidate.is_absolute() or ".." in candidate.parts:
23
+ raise ValueError(f"unsafe managed path: {rel}")
24
+ resolved = (root / candidate).resolve()
25
+ try:
26
+ resolved.relative_to(root)
27
+ except ValueError as exc:
28
+ raise ValueError(f"managed path escapes repository root: {rel}") from exc
29
+ return resolved
30
+
31
+
32
+ def verify(root: Path, baseline_path: Path | None = None) -> int:
33
+ root = root.resolve()
34
+ baseline_path = baseline_path.resolve() if baseline_path else root / BASELINE_FILENAME
35
+ if not baseline_path.is_file():
36
+ print(f"MISSING baseline: {baseline_path}")
37
+ return 2
38
+
39
+ try:
40
+ baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
41
+ except json.JSONDecodeError as exc:
42
+ print(f"INVALID baseline JSON: {exc}")
43
+ return 2
44
+
45
+ if baseline.get("schemaVersion") != 1:
46
+ print(f"UNSUPPORTED baseline schemaVersion: {baseline.get('schemaVersion')}")
47
+ return 2
48
+
49
+ managed = baseline.get("managedPaths")
50
+ if not isinstance(managed, list) or not managed:
51
+ print("INVALID baseline: managedPaths is missing or empty")
52
+ return 2
53
+
54
+ failures = 0
55
+ seen: set[str] = set()
56
+ for item in managed:
57
+ rel = item.get("path")
58
+ expected = item.get("sha256")
59
+ if not rel or not expected:
60
+ print(f"INVALID managed path entry: {item!r}")
61
+ failures += 1
62
+ continue
63
+ if rel in seen:
64
+ print(f"INVALID duplicate managed path: {rel}")
65
+ failures += 1
66
+ continue
67
+ seen.add(rel)
68
+ try:
69
+ path = resolve_managed_path(root, rel)
70
+ except ValueError as exc:
71
+ print(f"INVALID {exc}")
72
+ failures += 1
73
+ continue
74
+ if not path.is_file():
75
+ print(f"MISSING {rel}")
76
+ failures += 1
77
+ continue
78
+ actual = sha256_file(path)
79
+ if actual != expected:
80
+ print(f"DRIFT {rel}")
81
+ failures += 1
82
+ else:
83
+ print(f"SAME {rel}")
84
+
85
+ print()
86
+ if failures:
87
+ print(f"Core baseline integrity: FAIL ({failures} path(s))")
88
+ return 1
89
+ print(f"Core baseline integrity: PASS ({len(managed)} managed path(s))")
90
+ return 0
91
+
92
+
93
+ def main() -> int:
94
+ parser = argparse.ArgumentParser(description="Verify a consumer's Core-managed paths against CORE_BASELINE.json.")
95
+ parser.add_argument("--root", type=Path, default=Path.cwd(), help="Consumer repository root")
96
+ parser.add_argument("--baseline", type=Path, help="Optional explicit baseline file")
97
+ args = parser.parse_args()
98
+ return verify(args.root, args.baseline)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ raise SystemExit(main())
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ from pathlib import Path
12
+ from typing import Callable
13
+
14
+ BASELINE_FILENAME = "CORE_BASELINE.json"
15
+
16
+
17
+ class UpstreamUnavailable(RuntimeError):
18
+ pass
19
+
20
+
21
+ def load_baseline(root: Path) -> dict:
22
+ path = root / BASELINE_FILENAME
23
+ if not path.is_file():
24
+ raise SystemExit(f"Missing {BASELINE_FILENAME}: {path}")
25
+ try:
26
+ data = json.loads(path.read_text(encoding="utf-8"))
27
+ except json.JSONDecodeError as exc:
28
+ raise SystemExit(f"Invalid {BASELINE_FILENAME}: {exc}") from exc
29
+ required = ["coreRepository", "coreCommit", "coreVersion", "distribution"]
30
+ missing = [key for key in required if not data.get(key)]
31
+ if missing:
32
+ raise SystemExit(f"{BASELINE_FILENAME} missing required field(s): {', '.join(missing)}")
33
+ return data
34
+
35
+
36
+ def github_get_json(url: str, token: str | None) -> dict:
37
+ headers = {
38
+ "Accept": "application/vnd.github+json",
39
+ "X-GitHub-Api-Version": "2022-11-28",
40
+ "User-Agent": "lean-agentic-core-upstream-check",
41
+ }
42
+ if token:
43
+ headers["Authorization"] = f"Bearer {token}"
44
+ request = urllib.request.Request(url, headers=headers)
45
+ try:
46
+ with urllib.request.urlopen(request, timeout=20) as response:
47
+ return json.load(response)
48
+ except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError) as exc:
49
+ raise UpstreamUnavailable(str(exc)) from exc
50
+
51
+
52
+ def resolve_remote_head(repository: str, ref: str, token: str | None, fetch: Callable[[str, str | None], dict]) -> str:
53
+ quoted_ref = urllib.parse.quote(ref, safe="")
54
+ data = fetch(f"https://api.github.com/repos/{repository}/commits/{quoted_ref}", token)
55
+ sha = str(data.get("sha") or "").strip()
56
+ if not sha:
57
+ raise UpstreamUnavailable("GitHub response did not include a commit SHA")
58
+ return sha
59
+
60
+
61
+ def compare_remote(
62
+ repository: str,
63
+ baseline_sha: str,
64
+ upstream_sha: str,
65
+ token: str | None,
66
+ fetch: Callable[[str, str | None], dict],
67
+ ) -> dict:
68
+ return fetch(
69
+ f"https://api.github.com/repos/{repository}/compare/{baseline_sha}...{upstream_sha}",
70
+ token,
71
+ )
72
+
73
+
74
+ def resolve_local_head(core_path: Path, ref: str) -> str:
75
+ try:
76
+ result = subprocess.run(
77
+ ["git", "-C", str(core_path), "rev-parse", ref],
78
+ check=True,
79
+ capture_output=True,
80
+ text=True,
81
+ )
82
+ except (OSError, subprocess.CalledProcessError) as exc:
83
+ raise UpstreamUnavailable(f"unable to resolve {ref!r} in {core_path}") from exc
84
+ return result.stdout.strip()
85
+
86
+
87
+ def check_upstream(
88
+ root: Path,
89
+ *,
90
+ ref: str = "main",
91
+ token: str | None = None,
92
+ core_path: Path | None = None,
93
+ skip_if_unavailable: bool = False,
94
+ fetch: Callable[[str, str | None], dict] = github_get_json,
95
+ ) -> int:
96
+ root = root.resolve()
97
+ baseline = load_baseline(root)
98
+ repository = baseline["coreRepository"]
99
+ baseline_sha = baseline["coreCommit"]
100
+
101
+ try:
102
+ if core_path is not None:
103
+ upstream_sha = resolve_local_head(core_path.resolve(), ref)
104
+ comparison = None
105
+ else:
106
+ upstream_sha = resolve_remote_head(repository, ref, token, fetch)
107
+ comparison = None if upstream_sha == baseline_sha else compare_remote(
108
+ repository, baseline_sha, upstream_sha, token, fetch
109
+ )
110
+ except UpstreamUnavailable as exc:
111
+ if skip_if_unavailable:
112
+ print(f"Core upstream freshness: SKIP ({exc})")
113
+ return 0
114
+ print(f"Core upstream freshness: UNAVAILABLE ({exc})")
115
+ return 2
116
+
117
+ print(f"Consumer baseline: {baseline['coreVersion']} @ {baseline_sha}")
118
+ print(f"Upstream {ref}: {upstream_sha}")
119
+ if upstream_sha == baseline_sha:
120
+ print("Core upstream freshness: PASS")
121
+ return 0
122
+
123
+ if comparison:
124
+ status = comparison.get("status", "unknown")
125
+ ahead = comparison.get("ahead_by")
126
+ behind = comparison.get("behind_by")
127
+ if status in {"ahead", "identical"} and isinstance(ahead, int):
128
+ print(f"Core upstream freshness: STALE ({ahead} upstream commit(s) ahead)")
129
+ else:
130
+ details = []
131
+ if status:
132
+ details.append(f"status={status}")
133
+ if isinstance(ahead, int):
134
+ details.append(f"ahead_by={ahead}")
135
+ if isinstance(behind, int):
136
+ details.append(f"behind_by={behind}")
137
+ print("Core upstream freshness: DIVERGED" + (f" ({', '.join(details)})" if details else ""))
138
+ else:
139
+ print("Core upstream freshness: STALE")
140
+ return 1
141
+
142
+
143
+ def main() -> int:
144
+ parser = argparse.ArgumentParser(description="Check whether a consumer's recorded Lean Core commit is current upstream.")
145
+ parser.add_argument("--root", type=Path, default=Path.cwd(), help="Consumer repository root")
146
+ parser.add_argument("--ref", default="main", help="Core ref to compare against (default: main)")
147
+ parser.add_argument(
148
+ "--token-env",
149
+ default="LEAN_CORE_READ_TOKEN",
150
+ help="Environment variable containing a GitHub token for private Core access",
151
+ )
152
+ parser.add_argument("--core-path", type=Path, help="Use a local Core checkout instead of GitHub API")
153
+ parser.add_argument(
154
+ "--skip-if-unavailable",
155
+ action="store_true",
156
+ help="Exit successfully if upstream cannot be queried (useful when CI token is not configured)",
157
+ )
158
+ args = parser.parse_args()
159
+
160
+ token = os.getenv(args.token_env) or os.getenv("GITHUB_TOKEN")
161
+ return check_upstream(
162
+ args.root,
163
+ ref=args.ref,
164
+ token=token,
165
+ core_path=args.core_path,
166
+ skip_if_unavailable=args.skip_if_unavailable,
167
+ )
168
+
169
+
170
+ if __name__ == "__main__":
171
+ raise SystemExit(main())