lightcone-cli 0.3.0__tar.gz → 0.3.1__tar.gz
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.
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/PKG-INFO +1 -1
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/agents/lc-extractor.md +1 -3
- lightcone_cli-0.3.1/claude/lightcone/guides/astra-reference.md +389 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/guides/lightcone-cli-reference.md +22 -8
- lightcone_cli-0.3.1/claude/lightcone/scripts/check-lc-run.sh +107 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/scripts/session-start.sh +24 -44
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/skills/lc-migrate/SKILL.md +11 -8
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/skills/lc-new/SKILL.md +9 -14
- lightcone_cli-0.3.1/claude/lightcone/templates/CLAUDE.md +25 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/cli/commands.py +81 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/status.py +8 -1
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/harness.py +24 -4
- lightcone_cli-0.3.0/claude/lightcone/guides/astra-reference.md +0 -338
- lightcone_cli-0.3.0/claude/lightcone/guides/ui-brand.md +0 -86
- lightcone_cli-0.3.0/claude/lightcone/scripts/check-lc-run.sh +0 -140
- lightcone_cli-0.3.0/claude/lightcone/skills/lc-build/SKILL.md +0 -92
- lightcone_cli-0.3.0/claude/lightcone/skills/lc-build/assets/loop-prompt.md +0 -91
- lightcone_cli-0.3.0/claude/lightcone/skills/lc-build/scripts/setup-lc-build.sh +0 -240
- lightcone_cli-0.3.0/claude/lightcone/skills/lc-verify/SKILL.md +0 -58
- lightcone_cli-0.3.0/claude/lightcone/templates/CLAUDE.md +0 -30
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/.gitignore +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/LICENSE +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/hooks/langfuse_git_commit_hook.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/hooks/langfuse_hook.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/hooks/langfuse_prepare_commit_msg.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/hooks/langfuse_session_init_hook.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/hooks/langfuse_utils.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/scripts/activate-venv.sh +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/scripts/validate-on-save.sh +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/skills/lc-feedback/SKILL.md +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/pyproject.toml +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/cli/__init__.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/cli/plugin.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/__init__.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/container.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/dask_cluster.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/manifest.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/runner.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/scratch.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/site_registry.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/snakefile.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/tree.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/validation.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/engine/verify.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/__init__.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/build.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/cli.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/graders.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/models.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/report.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/lightcone/eval/sandbox.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/snakemake_executor_plugin_dask/__init__.py +0 -0
- {lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/src/snakemake_executor_plugin_dask/executor.py +0 -0
|
@@ -69,12 +69,10 @@ prior_insights:
|
|
|
69
69
|
doi: "[DOI]"
|
|
70
70
|
version: <version if arXiv, omit otherwise>
|
|
71
71
|
quote:
|
|
72
|
-
type: TextQuoteSelector
|
|
73
72
|
exact: "<VERIFIED exact quote from paper>"
|
|
74
73
|
prefix: "<~20-100 chars BEFORE the quote>"
|
|
75
74
|
suffix: "<~20-100 chars AFTER the quote>"
|
|
76
75
|
location:
|
|
77
|
-
type: FragmentSelector
|
|
78
76
|
page: <page number hint>
|
|
79
77
|
scope: "<when this applies -- optional, include only if the claim has limited applicability>"
|
|
80
78
|
|
|
@@ -111,4 +109,4 @@ verification_summary:
|
|
|
111
109
|
| `prefix/suffix mismatch` | Context text does not match surrounding text | Re-read the area around the quote, copy exact surrounding text |
|
|
112
110
|
| Persistent `not_found` | OCR artifacts, ligatures, or Unicode differences | Try shorter quote avoiding problem characters; increase prefix/suffix |
|
|
113
111
|
|
|
114
|
-
**Recovery**: Re-read the failing page, copy the exact text, update prefix/suffix, verify with `astra paper verify-
|
|
112
|
+
**Recovery**: Re-read the failing page, copy the exact text, update prefix/suffix, verify with `astra paper verify-quotes`, then run `astra validate astra.yaml --verify-evidence`.
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
# ASTRA Reference
|
|
2
|
+
|
|
3
|
+
## What an ASTRA Analysis Is
|
|
4
|
+
|
|
5
|
+
An ASTRA analysis is a structured layer between the code and the paper. It surfaces the inputs a computation depends on, the outputs it produces, and -- critically -- every methodological decision that could plausibly affect the results. The goal is to make the full decision space explicit and machine-readable, so that alternative defensible choices can be systematically explored rather than silently baked in.
|
|
6
|
+
|
|
7
|
+
An `astra.yaml` spec captures this for a single unit of work. The structure is **self-similar**: a top-level analysis and a nested sub-analysis have exactly the same shape. Everything in this reference applies equally to both.
|
|
8
|
+
|
|
9
|
+
## astra.yaml Structure
|
|
10
|
+
|
|
11
|
+
Fields: `id`, `version`, `name`, `narrative`, `authors`, `tags`, `inputs`, `outputs`, `decisions`, `prior_insights`, `findings`, `analyses`, `container`. `narrative` is the analysis-level prose field -- see [Narrative](#narrative) (typically filled in later, once the structural pieces have settled).
|
|
12
|
+
|
|
13
|
+
**Reserved IDs.** No analysis entity (input, output, decision, option, finding, prior insight, evidence, sub-analysis) may use any of these names as its `id` -- they collide with the narrative anchor grammar:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
inputs outputs decisions findings prior_insights
|
|
17
|
+
analyses options content narrative
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**`label` field.** Inputs, Outputs, Decisions, Options, and Insights all accept an optional `label:` -- a short human-readable name for compact rendering (margin glyphs, breadcrumbs, card titles). Tooling falls back to `id` when absent. `label` is required only on Options.
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
# Simple analysis -- everything at top level
|
|
24
|
+
version: "1.0"
|
|
25
|
+
name: "My Analysis"
|
|
26
|
+
# narrative: { ... } # see Narrative section; typically added later
|
|
27
|
+
inputs:
|
|
28
|
+
- id: training_data
|
|
29
|
+
type: data
|
|
30
|
+
source: "data/train.csv"
|
|
31
|
+
decisions:
|
|
32
|
+
scaling:
|
|
33
|
+
label: "Feature Scaling"
|
|
34
|
+
tags: [preprocessing] # optional freeform tags for grouping
|
|
35
|
+
rationale: "Affects convergence"
|
|
36
|
+
default: standard
|
|
37
|
+
options:
|
|
38
|
+
standard: { label: "StandardScaler" }
|
|
39
|
+
minmax: { label: "MinMaxScaler" }
|
|
40
|
+
use_pca:
|
|
41
|
+
label: "Use PCA"
|
|
42
|
+
default: "no"
|
|
43
|
+
options:
|
|
44
|
+
"yes": { label: "Yes" }
|
|
45
|
+
"no": { label: "No" }
|
|
46
|
+
n_components:
|
|
47
|
+
label: "PCA Components"
|
|
48
|
+
default: "50"
|
|
49
|
+
options:
|
|
50
|
+
"50": { label: "50 components" }
|
|
51
|
+
"100": { label: "100 components" }
|
|
52
|
+
outputs:
|
|
53
|
+
- id: accuracy
|
|
54
|
+
type: metric
|
|
55
|
+
inputs: [training_data] # provenance lives on the Output
|
|
56
|
+
decisions: [scaling, use_pca, n_components] # not on the recipe
|
|
57
|
+
recipe:
|
|
58
|
+
command: >-
|
|
59
|
+
python src/evaluate.py
|
|
60
|
+
--train {inputs.training_data}
|
|
61
|
+
--scaling {decisions.scaling}
|
|
62
|
+
--out {output}
|
|
63
|
+
container: Containerfile
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Cross-Analysis Inputs
|
|
67
|
+
|
|
68
|
+
To consume the outputs of a separate ASTRA analysis as a whole-cloth dependency, declare an Input of `type: analysis` with `ref:` (and optionally `ref_version:` and `use_outputs:`):
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
inputs:
|
|
72
|
+
- id: prior_study
|
|
73
|
+
type: analysis
|
|
74
|
+
ref: analyses/preprocessing_comparison
|
|
75
|
+
ref_version: "v1.2"
|
|
76
|
+
use_outputs: [best_method, performance_table]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This is distinct from `from:` -- `ref` points to an external analysis by reference; `from:` aliases an element within the current analysis tree (see [Composition Mechanics](#composition-mechanics)).
|
|
80
|
+
|
|
81
|
+
## Decisions
|
|
82
|
+
|
|
83
|
+
A decision is a methodological choice where a different defensible option could plausibly produce a different numerical result. Include it if changing the choice could shift a quantitative outcome -- even modestly. Many small decisions can compound. When in doubt, include it.
|
|
84
|
+
|
|
85
|
+
**Not decisions -- skip these:**
|
|
86
|
+
|
|
87
|
+
- **Tooling choices** that produce identical numerical results: programming language, library/framework (PyTorch vs TensorFlow), file format, parallelization strategy, plotting style.
|
|
88
|
+
- **Fixed constraints** with no degrees of freedom: "use the data that exists," "satisfy the grant requirements."
|
|
89
|
+
- **What to produce** -- decisions control *how* something is computed, not *what* outputs exist. Outputs are fixed by the analysis structure.
|
|
90
|
+
|
|
91
|
+
**These ARE decisions -- do not skip:**
|
|
92
|
+
|
|
93
|
+
- Algorithmic choices (MCMC vs optimization, KDE vs histogram, smoothing method)
|
|
94
|
+
- Numerical parameters and thresholds (sigma clipping level, bin width, convergence criterion, iteration count)
|
|
95
|
+
- Statistical method choices (bootstrap vs analytic errors, Bayesian vs frequentist)
|
|
96
|
+
- Data selection criteria (quality cuts, magnitude limits, spatial boundaries)
|
|
97
|
+
- Correction and calibration choices (which reddening law, which zero-point, which prior)
|
|
98
|
+
|
|
99
|
+
### Tags
|
|
100
|
+
|
|
101
|
+
Decisions may carry an optional `tags:` list for grouping (e.g. `[preprocessing]`, `[physics]`, `[stats]`). Keep the tag vocabulary **small and consolidated** -- reuse existing tags rather than minting new ones, since tags are mostly useful for cross-cutting views over a shared decision space, and that view fragments quickly when every decision invents its own label.
|
|
102
|
+
|
|
103
|
+
### Parameterization
|
|
104
|
+
|
|
105
|
+
**Every decision must be parameterized in code** -- never hardcode a decision value. The recipe's `command:` template references it via `{decisions.<id>}` (see [Command Template Substitution](#command-template-substitution)).
|
|
106
|
+
|
|
107
|
+
### Constraints
|
|
108
|
+
|
|
109
|
+
- `when: "decision.option"` -- decision only exists given an upstream choice (e.g., `svm_kernel` only exists `when: model.svm`)
|
|
110
|
+
- `incompatible_with: ["decision.option"]` -- cannot coexist in a universe
|
|
111
|
+
- `requires: ["decision.option"]` -- must be selected together
|
|
112
|
+
- `excluded: true` + `excluded_reason: "..."` -- option considered but rejected (cannot be default or selected)
|
|
113
|
+
|
|
114
|
+
## Recipe Format
|
|
115
|
+
|
|
116
|
+
ASTRA is asset-centric: the **Output** declares its provenance (`inputs`, `decisions`) and when it's active (`when`); the recipe is pure *how*. Recipe fields: `command` (required), `container`, `resources`.
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
outputs:
|
|
120
|
+
- id: accuracy
|
|
121
|
+
type: metric
|
|
122
|
+
inputs: [trained_model] # Dependencies live on the Output
|
|
123
|
+
decisions: [scaling, classifier] # Decisions that parameterize this output
|
|
124
|
+
recipe:
|
|
125
|
+
command: >-
|
|
126
|
+
python src/evaluate.py
|
|
127
|
+
--model {inputs.trained_model}
|
|
128
|
+
--scaling {decisions.scaling}
|
|
129
|
+
--out {output}
|
|
130
|
+
container: ghcr.io/proj/ml:latest # Overrides analysis-level default
|
|
131
|
+
resources: { cpus: 4, memory: "32GB", gpus: 1, time_limit: "2h" }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Set `container:` at analysis level (all recipes inherit); per-recipe `container:` overrides. Pass either a container image name (e.g., `python:3.12-slim`, `ghcr.io/org/img:latest`) or a path to a Containerfile (e.g., `Containerfile`, `containers/Dockerfile`). The runtime figures out whether to pull or build.
|
|
135
|
+
|
|
136
|
+
### Command Template Substitution
|
|
137
|
+
|
|
138
|
+
Runners expand `{...}` placeholders in `command:` before invoking it: `{inputs.<id>}` (input path), `{inputs}` (all input paths, declared order), `{decisions.<id>}` (active option ID), `{output}` (artifact path), `{{`/`}}` (literal braces). Every `{inputs.<id>}` and `{decisions.<id>}` must name something declared in the parent Output's `inputs:`/`decisions:` lists -- always **local IDs** (no `../`; bridging is declared once at the Input/Decision via `from:`).
|
|
139
|
+
|
|
140
|
+
### Conditional Outputs
|
|
141
|
+
|
|
142
|
+
Outputs can have `when` conditions -- the output only exists when the condition is met for a given universe. Uses the same syntax as decision `when` (negation with `~`, lists AND'd).
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
outputs:
|
|
146
|
+
- id: faint_metrics
|
|
147
|
+
type: metric
|
|
148
|
+
when: "~training_sample.bright_only" # Only when NOT bright_only
|
|
149
|
+
recipe: { command: python src/evaluate.py }
|
|
150
|
+
- id: combined_report
|
|
151
|
+
type: report
|
|
152
|
+
when: ["~training_sample.bright_only", model.svm] # AND: both must be true
|
|
153
|
+
recipe: { command: python src/combo.py }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Universe Management
|
|
157
|
+
|
|
158
|
+
A universe selects one option per decision -- a defensible alternative analysis path. Bug fixes and refactors are normal commits, not universes. Universe IDs use the pattern `^[a-z][a-z0-9_-]*$` (hyphens allowed, unlike other ASTRA IDs).
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
astra universe generate -n experiment1 -d "Testing hypothesis X"
|
|
162
|
+
# Edit universes/experiment1.yaml, then run with the runner of your choice.
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Adding a new decision:** (1) add to `astra.yaml` with options/default/rationale, (2) add parameter to code, (3) add to all existing universe files with default, (4) create new universe, (5) `astra validate astra.yaml`.
|
|
166
|
+
|
|
167
|
+
## Prior Insights and Findings
|
|
168
|
+
|
|
169
|
+
Two kinds of insight, distinguished by direction:
|
|
170
|
+
|
|
171
|
+
- **Prior insights** (`prior_insights:`) — knowledge from outside the analysis that informs decisions. From literature (by DOI) or artifacts from a prior/parent analysis.
|
|
172
|
+
- **Findings** (`findings:`) — conclusions from the analysis itself, backed by its own output artifacts.
|
|
173
|
+
|
|
174
|
+
Both use the same Insight model: `id`, `label` (optional), `claim`, `created_at`, `evidence`, plus optional `derived` (true if synthesized/inferred from multiple sources), `scope` (applicability conditions), `tags`, `notes`. Placement determines direction.
|
|
175
|
+
|
|
176
|
+
Each evidence item has its own fields: `id`, exactly one of `doi` (literature) or `artifact` (output ID), and either a `quote` (TextQuoteSelector with required `exact`, optional `prefix`/`suffix`) or `location` (FragmentSelector with `value` like `"page=6"` and/or 1-indexed `page`). DOI evidence may add `version` (arXiv version). Artifact evidence may add `snapshot` (path to an immutable artifact copy) and `source_commit` (git commit that produced it).
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
prior_insights:
|
|
180
|
+
layer_norm_stability:
|
|
181
|
+
id: layer_norm_stability
|
|
182
|
+
label: "LN stability"
|
|
183
|
+
claim: "Layer normalization improves training stability"
|
|
184
|
+
created_at: "2025-01-15T10:30:00"
|
|
185
|
+
derived: false
|
|
186
|
+
scope: "Transformer training with batch sizes < 64"
|
|
187
|
+
tags: [optimization]
|
|
188
|
+
evidence:
|
|
189
|
+
- id: e1
|
|
190
|
+
doi: "10.48550/arXiv.1607.06450"
|
|
191
|
+
version: 1
|
|
192
|
+
quote: { exact: "Exact text", prefix: "~20-100 chars before", suffix: "~20-100 chars after" }
|
|
193
|
+
location: { value: "page=5", page: 5 }
|
|
194
|
+
|
|
195
|
+
findings:
|
|
196
|
+
scaling_result:
|
|
197
|
+
id: scaling_result
|
|
198
|
+
claim: "StandardScaler achieves 97% accuracy vs 91% for MinMaxScaler"
|
|
199
|
+
created_at: "2025-02-01T14:00:00"
|
|
200
|
+
derived: true
|
|
201
|
+
evidence:
|
|
202
|
+
- id: e1
|
|
203
|
+
artifact: accuracy # references a declared output ID
|
|
204
|
+
snapshot: "snapshots/run_2025-02-01.json"
|
|
205
|
+
source_commit: "a3f9c12"
|
|
206
|
+
- id: e2
|
|
207
|
+
artifact: model_comparison
|
|
208
|
+
quote: { exact: "StandardScaler achieved 97% accuracy vs 91% for MinMaxScaler" }
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Link prior insights to decisions: `options: { layer_norm: { insights: [layer_norm_stability] } }`
|
|
212
|
+
|
|
213
|
+
Artifact references are validated against declared outputs — `astra validate` flags any `artifact:` that doesn't match an output ID. Literature evidence (DOI) requires a `quote` (a `TextQuoteSelector` with required `exact` plus optional `prefix`/`suffix`); artifact evidence does not. Each evidence item must set exactly one of `doi` or `artifact`.
|
|
214
|
+
|
|
215
|
+
**Sub-analysis findings as prior insights:** When a sub-analysis explores a specific question (calibration study, simulation validation, sensitivity test), its findings can inform decisions elsewhere. The parent or sibling references the sub-analysis output as artifact evidence in its own `prior_insights`, e.g. `artifact: "build_mocks.noise_diagnostics"`. This creates a traceable chain from sub-analysis conclusion to downstream decision.
|
|
216
|
+
|
|
217
|
+
### Adding a Paper as Prior Insight
|
|
218
|
+
|
|
219
|
+
Found a paper through literature search? Three steps to wire it into the analysis:
|
|
220
|
+
|
|
221
|
+
1. **Cache the PDF** — `astra paper add <doi>` downloads it to the project's paper cache. Pass `--pdf PATH` if you already have a local copy, or `--version N` for a specific arXiv version.
|
|
222
|
+
2. **Add a `prior_insights:` entry** that cites the DOI (and optionally `version`) under `evidence:`. The `quote.exact` text must match the PDF verbatim; optional `prefix`/`suffix` (~20–100 chars on either side) disambiguate when the exact string occurs more than once.
|
|
223
|
+
3. **Verify** — `astra paper verify-quotes <doi>` for one paper, or `astra validate astra.yaml --verify-evidence` to check every quote in the spec. A wrong `exact` string fails validation.
|
|
224
|
+
|
|
225
|
+
`astra paper list` shows what's cached; `astra paper path <doi>` prints the PDF path so you can open it for review.
|
|
226
|
+
|
|
227
|
+
## Sub-Analyses
|
|
228
|
+
|
|
229
|
+
### What a Sub-Analysis Is
|
|
230
|
+
|
|
231
|
+
Each `astra.yaml` -- root or nested -- represents a **unit of work**: meaningful inputs, methodological decisions, meaningful outputs. A sub-analysis is one of these units nested inside a larger analysis. It can be understood, executed, and evaluated on its own terms.
|
|
232
|
+
|
|
233
|
+
### When to Split
|
|
234
|
+
|
|
235
|
+
Default to a **single analysis**. Split into sub-analyses only when:
|
|
236
|
+
|
|
237
|
+
- **Decision ownership** -- the stage has its own decisions that could meaningfully vary, clearly scoped to that stage rather than the broader analysis. Shared decisions live at the parent (`from: ../`); stage-specific decisions live in the sub-analysis. If you can't cleanly assign decisions to levels, the split is probably wrong.
|
|
238
|
+
- **Reusability** -- someone working on a different paper could use this stage's output as-is (a cleaned catalog, a trained emulator, a set of mocks).
|
|
239
|
+
- **Side quests** -- independent investigations (diagnostics, calibrations, simulation studies) that have different inputs/outputs/code from the main analysis are sub-analyses, not universes. Universes are different parameter choices on the same pipeline.
|
|
240
|
+
- **If boundaries are unclear**, start flat and split later when they become explicit: separate stage outputs, explicit `from` links, clear decision ownership per level.
|
|
241
|
+
|
|
242
|
+
### Worked Examples
|
|
243
|
+
|
|
244
|
+
#### Two-Stage Pipeline (DAG Split)
|
|
245
|
+
|
|
246
|
+
A paper builds mock galaxy catalogs, then trains a neural network on them for photometric redshift estimation. Natural split:
|
|
247
|
+
- **`build_mocks`**: simulation inputs + survey properties, decisions about noise model and selection function. Produces mock catalogs.
|
|
248
|
+
- **`photo_z`**: mocks (from sibling) + real survey data, decisions about network architecture and training. Produces redshift estimates.
|
|
249
|
+
|
|
250
|
+
The mock-building decisions are independent from training decisions. Someone could reuse the mocks for a different estimator.
|
|
251
|
+
|
|
252
|
+
#### When NOT to Split
|
|
253
|
+
|
|
254
|
+
A paper downloads galaxies, applies quality cuts, corrects for extinction, computes luminosity functions, fits a Schechter function. Five steps -- but one objective, shared decisions, one end product.
|
|
255
|
+
|
|
256
|
+
### Anti-Patterns
|
|
257
|
+
|
|
258
|
+
- **Splitting by script** rather than by analytical unit.
|
|
259
|
+
- **Zero-decision sub-analyses** that just pass data through -- make these output recipes in the parent.
|
|
260
|
+
- **Premature splitting.** Start flat, split when boundaries become explicit. Easier to split a working flat analysis than merge a broken hierarchical one.
|
|
261
|
+
- **Forcing a linear DAG.** Independent stages don't need to be wired in sequence just because the paper presents them that way.
|
|
262
|
+
|
|
263
|
+
### Composition Mechanics
|
|
264
|
+
|
|
265
|
+
Sub-analyses can be **inline** (their content lives directly under the parent's `analyses:` map) or **external** (`path:` points to a directory with its own `astra.yaml`). `path:` is mutually exclusive with inline content -- a sub-analysis entry sets either `path:` or fields like `inputs`/`outputs`/`decisions`, not both. The parent below uses external sub-analyses:
|
|
266
|
+
|
|
267
|
+
```yaml
|
|
268
|
+
# Root astra.yaml
|
|
269
|
+
inputs:
|
|
270
|
+
- id: survey_catalog
|
|
271
|
+
type: data
|
|
272
|
+
source: "data/survey.parquet"
|
|
273
|
+
decisions:
|
|
274
|
+
cosmology_model: # Shared across stages
|
|
275
|
+
label: "Cosmological Model"
|
|
276
|
+
tags: [physics]
|
|
277
|
+
default: flat_lcdm
|
|
278
|
+
options:
|
|
279
|
+
flat_lcdm: { label: "Flat LCDM" }
|
|
280
|
+
wcdm: { label: "wCDM" }
|
|
281
|
+
outputs:
|
|
282
|
+
- id: trained_model
|
|
283
|
+
from: train_network.trained_model # Re-export from sub-analysis (pure alias)
|
|
284
|
+
analyses:
|
|
285
|
+
build_mocks:
|
|
286
|
+
path: ./analyses/build_mocks
|
|
287
|
+
train_network:
|
|
288
|
+
path: ./analyses/train_network
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Inside each sub-analysis's own `astra.yaml`, `from:` wires inputs and decisions to the parent or siblings:
|
|
292
|
+
|
|
293
|
+
```yaml
|
|
294
|
+
# analyses/train_network/astra.yaml
|
|
295
|
+
inputs:
|
|
296
|
+
- id: training_data
|
|
297
|
+
from: ../build_mocks.mock_catalog # Sibling output (escape upward, then descend)
|
|
298
|
+
outputs:
|
|
299
|
+
- id: trained_model
|
|
300
|
+
type: data
|
|
301
|
+
inputs: [training_data]
|
|
302
|
+
decisions: [cosmology_model, noise_model]
|
|
303
|
+
recipe: { command: python src/train.py --train {inputs.training_data} --out {output}, resources: { gpus: 1, memory: "32GB" } }
|
|
304
|
+
decisions:
|
|
305
|
+
cosmology_model:
|
|
306
|
+
from: ../cosmology_model # Inherit parent decision
|
|
307
|
+
noise_model:
|
|
308
|
+
label: "Noise Model"
|
|
309
|
+
default: heteroscedastic
|
|
310
|
+
options:
|
|
311
|
+
homoscedastic: { label: "Homoscedastic" }
|
|
312
|
+
heteroscedastic: { label: "Heteroscedastic" }
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Path grammar.** `from:` paths use a uniform tree-path syntax: `../` escapes one scope upward (stack as needed), and `name.subname` descends into a named child scope. Multiple levels work in either direction. Per-slot direction:
|
|
316
|
+
|
|
317
|
+
| Slot | Legal forms | Meaning |
|
|
318
|
+
|---|---|---|
|
|
319
|
+
| `Input.from` | `../id`, `../../id`, `../scope.out_id` | parent/ancestor Input, or a sibling sub's Output (escape up, then descend) |
|
|
320
|
+
| `Decision.from` | `../id`, `../../id` | parent/ancestor Decision (downward-only flow; share via common ancestor) |
|
|
321
|
+
| `Output.from` | `child.out_id`, `child.sub.out_id` | own child sub's Output (re-export; descend through nested children) |
|
|
322
|
+
|
|
323
|
+
`from:` makes the node a pure pointer -- only `id` and `from` (plus `when` on Outputs) are allowed; everything else (`type`, `description`, `source`, `options`, `default`, `recipe`, …) is inherited from the source.
|
|
324
|
+
|
|
325
|
+
The **`universe:` field** in universe files selects which sub-analysis universe to load: `build_mocks: { universe: baseline }` loads `./analyses/build_mocks/universes/baseline.yaml`.
|
|
326
|
+
|
|
327
|
+
## Narrative
|
|
328
|
+
|
|
329
|
+
`narrative` is the analysis-level prose field on any Analysis (root or sub). It's structured as five Markdown sections: `summary`, `findings`, `methods`, `inputs`, `outputs`. The schema is closed (`additionalProperties: false`) -- no other keys are allowed.
|
|
330
|
+
|
|
331
|
+
**Recommendation:** fill `narrative` in *later*, once the structural pieces of the analysis (decisions, outputs, sub-analyses) have settled. Prose written too early goes stale fast and tends to describe what no longer exists. Per-element prose (what each Input, Output, Decision, Option, or Insight is and why) belongs on the elements themselves via `description`/`rationale`/`notes` -- those can be written from day one.
|
|
332
|
+
|
|
333
|
+
**Conditional coverage.** All five sections are schema-optional, but `astra validate` enforces:
|
|
334
|
+
|
|
335
|
+
| Section | Required when |
|
|
336
|
+
|---|---|
|
|
337
|
+
| `findings` | the analysis has entries under `findings:` |
|
|
338
|
+
| `methods` | the analysis has entries under `decisions:` or `analyses:` |
|
|
339
|
+
| `inputs` | the analysis has entries under `inputs:` |
|
|
340
|
+
| `outputs` | the analysis has entries under `outputs:` |
|
|
341
|
+
| `summary` | always optional |
|
|
342
|
+
|
|
343
|
+
Authors narrate what they declare; stub analyses with only a summary stay clean.
|
|
344
|
+
|
|
345
|
+
**Anchor references.** Inside any section, link to other elements with Markdown anchor links (`[text](#path.to.element)`) using the same tree-path grammar as `from:` -- `#decisions.scaling`, `#decisions.scaling.options.standard`, `#findings.best_model`, `#analyses.preprocessing` (whole sub-analysis), `#analyses.preprocessing.outputs.features` (element inside a sub-analysis), `#../decisions.method` to escape to a parent scope.
|
|
346
|
+
|
|
347
|
+
**Inline images.** Standard Markdown image syntax inside any section -- `` for repo-relative paths or `` for URLs. Renderers like lightcone-ui pick them up the same way they pick up text.
|
|
348
|
+
|
|
349
|
+
```yaml
|
|
350
|
+
narrative:
|
|
351
|
+
summary: |
|
|
352
|
+
A two-stage pipeline for Iris classification that demonstrates
|
|
353
|
+
sub-analyses.
|
|
354
|
+
methods: |
|
|
355
|
+
The [feature_extraction sub-analysis](#analyses.feature_extraction)
|
|
356
|
+
produces encoded features, which feed
|
|
357
|
+
[classification](#analyses.classification). A
|
|
358
|
+
[test_split](#decisions.test_split) decision controls the holdout.
|
|
359
|
+
inputs: |
|
|
360
|
+
[iris_data](#inputs.iris_data) is Fisher's 150-sample, 4-feature,
|
|
361
|
+
3-class dataset.
|
|
362
|
+
outputs: |
|
|
363
|
+
The top level exposes [accuracy](#outputs.accuracy) and a
|
|
364
|
+
[pipeline_summary](#outputs.pipeline_summary) report.
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## CLI Reference (astra)
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
astra init [DIRECTORY] # Scaffold a new analysis
|
|
371
|
+
astra validate astra.yaml # Validate (run after every change)
|
|
372
|
+
astra validate astra.yaml --verify-evidence # + verify insight quotes against PDFs
|
|
373
|
+
astra info [--decisions|--inputs|--outputs] # Analysis summary / element details
|
|
374
|
+
astra universe generate -n NAME [-d "desc"] # Generate universe from defaults
|
|
375
|
+
astra universe check universes/x.yaml # Check universe constraints
|
|
376
|
+
astra viz [--fmt ascii|mermaid] # Visualize decision space
|
|
377
|
+
astra schema show analysis|universe|insights # Show JSON schema
|
|
378
|
+
astra paper add DOI [--version N] [--pdf PATH] # Cache a paper for evidence checks
|
|
379
|
+
astra paper list # List cached papers
|
|
380
|
+
astra paper show DOI # Show metadata for a cached paper
|
|
381
|
+
astra paper path DOI [--version N] # Print the cached PDF's path
|
|
382
|
+
astra paper verify-quotes DOI # Batch-verify quotes; reads {"quotes":[...]} JSON from stdin
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Validation
|
|
386
|
+
|
|
387
|
+
Run `astra validate astra.yaml` after **every** spec change. Additional checks:
|
|
388
|
+
- Universe files: `astra universe check universes/<name>.yaml`
|
|
389
|
+
- Evidence quotes: `astra validate astra.yaml --verify-evidence`
|
{lightcone_cli-0.3.0 → lightcone_cli-0.3.1}/claude/lightcone/guides/lightcone-cli-reference.md
RENAMED
|
@@ -5,14 +5,23 @@ Reference for lightcone-cli execution: CLI commands, development workflow, statu
|
|
|
5
5
|
## CLI Reference
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
lc init [DIR]
|
|
9
|
-
lc run [
|
|
10
|
-
lc build [--force] [--runtime docker]
|
|
11
|
-
lc status [--universe NAME] [--json]
|
|
12
|
-
lc verify [--universe NAME]
|
|
8
|
+
lc init [DIR] [--permissions yolo|recommended|minimal] [--scratch PATH] # Scaffold a new ASTRA project
|
|
9
|
+
lc run [OUTPUTS...] [--universe NAME] [--force] [--verbose] [--rerun-triggers TRIGGERS] # Materialize outputs
|
|
10
|
+
lc build [--force] [--runtime docker] # Build container images from specs
|
|
11
|
+
lc status [--universe NAME] [--json] # Materialization status (text or JSON)
|
|
12
|
+
lc verify [--universe NAME] # Recompute hashes and walk the provenance chain
|
|
13
|
+
lc eval {run,report,compare} # Run/inspect eval suites (requires the 'eval' extra)
|
|
13
14
|
```
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
`lc run` is quiet by default — pass `--verbose` to see worker output. `--scratch` is only relevant on HPC sites where `$HOME` doesn't honor `flock` (NERSC etc.); it redirects Snakemake state and Dask spill onto the named filesystem.
|
|
17
|
+
|
|
18
|
+
The first `lc` invocation auto-creates `~/.lightcone/config.yaml`:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
container:
|
|
22
|
+
runtime: auto # or: docker | podman | podman-hpc | none
|
|
23
|
+
# extraction_model: sonnet # optional; controls the lc-extractor agent
|
|
24
|
+
```
|
|
16
25
|
|
|
17
26
|
**Always run via `lc`.** Recipes must execute through `lc run` so that container builds, option resolution, resource limits, and result paths are applied. Treat the underlying execution engine as a black box — never invoke schedulers or container runtimes directly, that will bypass reproducibility guarantees.
|
|
18
27
|
|
|
@@ -20,7 +29,7 @@ The first `lc` invocation auto-creates `~/.lightcone/config.yaml` with defaults;
|
|
|
20
29
|
|
|
21
30
|
Sub-analyses are scaffolded by hand, since each one is just another `astra.yaml` nested in a directory. To add one:
|
|
22
31
|
|
|
23
|
-
1. Create `analyses/<name>/` with its own `astra.yaml` (and optionally `
|
|
32
|
+
1. Create `analyses/<name>/` with its own `astra.yaml` (and optionally `src/`, `universes/baseline.yaml`, `results/`).
|
|
24
33
|
2. Add a `path:` entry to the parent `astra.yaml` under `analyses:` (e.g. `analyses: { my_sub: { path: ./analyses/my_sub } }`).
|
|
25
34
|
3. Add a `<name>: { universe: baseline }` entry to each existing parent universe file.
|
|
26
35
|
|
|
@@ -30,10 +39,14 @@ Populate the sub-analysis's `astra.yaml` with inputs, outputs, and decisions. Us
|
|
|
30
39
|
|
|
31
40
|
Three overlapping phases:
|
|
32
41
|
|
|
33
|
-
1. **Write & Debug** — Run scripts directly (`python
|
|
42
|
+
1. **Write & Debug** — Run scripts directly (`python src/compute.py`) to iterate. Write them recipe-ready from the start: parameterize decisions, write to convention paths, one script per output.
|
|
34
43
|
2. **Integrate** — Add `recipe:` blocks to outputs in `astra.yaml`. Track with `lc status` (`alias` / `missing` / `stale` / `ok`). Set `container:` at analysis level or per-recipe — pass an image name (e.g., `python:3.12-slim`) or a path to a Containerfile (e.g., `Containerfile`).
|
|
35
44
|
3. **Materialize** — `lc run` executes recipes inside their declared containers and writes a content-addressed manifest next to each output. Done when `lc status` shows all `ok`.
|
|
36
45
|
|
|
46
|
+
Bare `lc run` materializes every output across every universe in `universes/*.yaml`; pass `OUTPUT_ID...` to scope to specific outputs and `--universe NAME` to scope to one universe. **Build iteratively** — name one upstream output at a time (`lc run <output_id> --universe <name>`) so you can inspect each intermediate before chaining further downstream, rather than running the whole DAG and debugging from the bottom of a long failure trace. `lc run` auto-builds container images on demand, so `lc build` is only needed for pre-warming or forcing a rebuild with `--force`.
|
|
47
|
+
|
|
48
|
+
Outputs land at `results/<universe>/<output_id>/`, with the per-output manifest at `<output_dir>/.lightcone-manifest.json`. Path-rooted sub-analyses prefix the sub's path: `<sub_path>/results/<universe>/<output_id>/`.
|
|
49
|
+
|
|
37
50
|
**An output is not done until `lc run` produces it.** Running scripts directly is for debugging only — final results must always come from `lc run` so they are reproducible.
|
|
38
51
|
|
|
39
52
|
### Spec-Code Invariant
|
|
@@ -57,5 +70,6 @@ Three overlapping phases:
|
|
|
57
70
|
- **Script arg not recognized** — The recipe's `command` template controls how decisions reach the script. Make sure each `{decisions.<id>}` is paired with a flag the script's argparse defines (e.g. `--<id> {decisions.<id>}` ↔ `parser.add_argument('--<id>')`).
|
|
58
71
|
- **Recipe input not found** — Materialize upstream outputs first.
|
|
59
72
|
- **Undeclared placeholder error** — A `{decisions.<id>}` or `{inputs.<id>}` in the recipe references something not listed in `Output.decisions` / `Output.inputs`. Add it to the Output's declaration, or remove the placeholder.
|
|
73
|
+
- **`lc verify` failure** — `missing_manifest` (output dir exists with no `.lightcone-manifest.json`), `tampered_data` (bytes on disk no longer hash to the recorded `data_version`), or `broken_chain` (an upstream's `data_version` drifted from what this output's manifest recorded). Re-run the affected output with `lc run` to repair.
|
|
60
74
|
|
|
61
75
|
After failure: fix, then `lc run <output_id> --universe <name>`.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PostToolUse hook: warn when the agent runs a Python script that has an
|
|
3
|
+
# integrated recipe — they should use `lc run` so a manifest gets written.
|
|
4
|
+
#
|
|
5
|
+
# Pure bash + jq + the `lc` binary on PATH. No Python imports — the hook
|
|
6
|
+
# previously imported `lightcone.engine.status` which silently failed in
|
|
7
|
+
# the empty venv `lc init` creates, so the hook was a no-op for nearly
|
|
8
|
+
# every project. Status data now comes from `lc status --json`, which
|
|
9
|
+
# carries the recipe template per output as of the same change.
|
|
10
|
+
|
|
11
|
+
input=$(cat)
|
|
12
|
+
command=$(echo "$input" | jq -r '.tool_input.command // empty')
|
|
13
|
+
|
|
14
|
+
[ -z "$command" ] && exit 0
|
|
15
|
+
|
|
16
|
+
# Only inspect commands that contain "python" / "python3"
|
|
17
|
+
echo "$command" | grep -qE 'python[23]?[[:space:]]' || exit 0
|
|
18
|
+
|
|
19
|
+
# Skip lc/astra invocations (they're already going through the right path)
|
|
20
|
+
echo "$command" | grep -qE '(^|[[:space:]/])(lc|astra)[[:space:]]' && exit 0
|
|
21
|
+
|
|
22
|
+
# Must be in an ASTRA project
|
|
23
|
+
[ -f "astra.yaml" ] || exit 0
|
|
24
|
+
|
|
25
|
+
# Must have lc on PATH; otherwise we can't get status
|
|
26
|
+
command -v lc &> /dev/null || exit 0
|
|
27
|
+
|
|
28
|
+
# Parse --universe NAME or --universe=NAME from the agent's command;
|
|
29
|
+
# default to baseline.
|
|
30
|
+
universe=$(echo "$command" \
|
|
31
|
+
| grep -oE -- '--universe[= ][[:space:]]*[^[:space:]]+' \
|
|
32
|
+
| head -1 \
|
|
33
|
+
| sed -E 's/--universe[= ][[:space:]]*//')
|
|
34
|
+
[ -z "$universe" ] && universe="baseline"
|
|
35
|
+
|
|
36
|
+
status_json=$(lc status --json --universe "$universe" 2>/dev/null) || exit 0
|
|
37
|
+
|
|
38
|
+
# Collect script paths the agent invoked. Split on && || ; first so each
|
|
39
|
+
# chained command is considered separately, then look for tokens ending
|
|
40
|
+
# in .py and `python -m module.path`.
|
|
41
|
+
agent_scripts=$(echo "$command" | awk '
|
|
42
|
+
{
|
|
43
|
+
# Split on shell separators
|
|
44
|
+
gsub(/&&|\|\||;/, "\n")
|
|
45
|
+
print
|
|
46
|
+
}' | awk '
|
|
47
|
+
{
|
|
48
|
+
n = split($0, tok, /[[:space:]]+/)
|
|
49
|
+
for (i = 1; i <= n; i++) {
|
|
50
|
+
t = tok[i]
|
|
51
|
+
if (t ~ /\.py$/) {
|
|
52
|
+
sub(/^\.\//, "", t)
|
|
53
|
+
print t
|
|
54
|
+
n2 = split(t, pp, "/")
|
|
55
|
+
print pp[n2]
|
|
56
|
+
}
|
|
57
|
+
if (t == "-m" && i + 1 <= n) {
|
|
58
|
+
mod = tok[i+1]
|
|
59
|
+
gsub(/\./, "/", mod)
|
|
60
|
+
print mod ".py"
|
|
61
|
+
n2 = split(mod, pp, "/")
|
|
62
|
+
print pp[n2] ".py"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}' | sort -u)
|
|
66
|
+
|
|
67
|
+
[ -z "$agent_scripts" ] && exit 0
|
|
68
|
+
|
|
69
|
+
# For each output that has a recipe, extract the script path from the
|
|
70
|
+
# recipe template and check whether the agent's command invoked it.
|
|
71
|
+
matched=$(echo "$status_json" | jq -r --arg uni "$universe" '
|
|
72
|
+
.universes[]
|
|
73
|
+
| select(.universe_id == $uni)
|
|
74
|
+
| .outputs[]
|
|
75
|
+
| select(.recipe_command != null and .recipe_command != "")
|
|
76
|
+
| "\(.output_id)\t\(.status)\t\(.recipe_command)"
|
|
77
|
+
' | while IFS=$'\t' read -r out_id out_status recipe_cmd; do
|
|
78
|
+
recipe_scripts=$(echo "$recipe_cmd" | tr ' \t\n' '\n' | grep '\.py$' | sed 's|^\./||')
|
|
79
|
+
for r in $recipe_scripts; do
|
|
80
|
+
rb=$(basename "$r")
|
|
81
|
+
if echo "$agent_scripts" | grep -qFx "$r" || echo "$agent_scripts" | grep -qFx "$rb"; then
|
|
82
|
+
printf '%s\t%s\n' "$out_id" "$out_status"
|
|
83
|
+
break
|
|
84
|
+
fi
|
|
85
|
+
done
|
|
86
|
+
done | head -1)
|
|
87
|
+
|
|
88
|
+
[ -z "$matched" ] && exit 0
|
|
89
|
+
|
|
90
|
+
matched_id=$(echo "$matched" | cut -f1)
|
|
91
|
+
matched_status=$(echo "$matched" | cut -f2)
|
|
92
|
+
|
|
93
|
+
case "$matched_status" in
|
|
94
|
+
missing|stale)
|
|
95
|
+
msg="WARNING: You ran the script for output \`$matched_id\` directly (status: $matched_status in $universe), which has an integrated recipe. Use \`lc run $matched_id --universe $universe\` instead so a manifest is written and the result is reproducible."
|
|
96
|
+
;;
|
|
97
|
+
ok)
|
|
98
|
+
msg="NOTE: Output \`$matched_id\` already has current results in $universe from \`lc run\`. Running the script directly bypasses the manifest — use \`lc run $matched_id --universe $universe\` to regenerate reproducibly."
|
|
99
|
+
;;
|
|
100
|
+
*)
|
|
101
|
+
exit 0
|
|
102
|
+
;;
|
|
103
|
+
esac
|
|
104
|
+
|
|
105
|
+
escaped_msg=$(echo "$msg" | jq -Rs .)
|
|
106
|
+
echo "{\"hookSpecificOutput\": {\"hookEventName\": \"PostToolUse\", \"additionalContext\": $escaped_msg}}"
|
|
107
|
+
exit 0
|