aienvmp 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/aienvmp/SKILL.md +1 -2
- package/CHANGELOG.md +14 -0
- package/README.md +35 -266
- package/action.yml +2 -15
- package/examples/github-action.yml +0 -1
- package/package.json +1 -1
- package/src/cli.js +17 -3
- package/src/commands/compile.js +8 -24
- package/src/commands/context.js +26 -1
- package/src/commands/dash.js +3 -2
- package/src/commands/diff.js +1 -1
- package/src/commands/doctor.js +1 -1
- package/src/commands/init.js +2 -1
- package/src/commands/scan.js +11 -3
- package/src/commands/snippet.js +27 -0
- package/src/commands/sync.js +34 -0
- package/src/manifest.js +47 -1
- package/src/render.js +22 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
- Added `aienvmp sync` as the simple one-step command for init, scan, `AIENV.md`, manifest, ledger, and dashboard generation.
|
|
6
|
+
- Improved AI preflight context with an explicit next action.
|
|
7
|
+
- Repositioned AGENTS.md integration as an optional snippet instead of default file generation.
|
|
8
|
+
- Added machine-readable sync/context improvements for AI and CI integrations.
|
|
9
|
+
- Simplified README for faster first-time understanding.
|
|
10
|
+
- Kept AGENTS.md, CLAUDE.md, and GEMINI.md integration explicit through `aienvmp snippet`.
|
|
11
|
+
|
|
12
|
+
## 0.1.1
|
|
13
|
+
|
|
14
|
+
- Added repo-scoped Codex skill wrapper.
|
|
15
|
+
- Normalized npm package metadata and published the initial npm package.
|
|
16
|
+
|
|
3
17
|
## 0.1.0
|
|
4
18
|
|
|
5
19
|
- Initial AI-first env map prototype.
|
package/README.md
CHANGED
|
@@ -4,303 +4,72 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**AI Environment Map.**
|
|
8
8
|
|
|
9
|
-
`aienvmp`
|
|
9
|
+
`aienvmp` gives AI agents a shared view of the current dev environment:
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
- runtime versions
|
|
12
|
+
- package managers
|
|
13
|
+
- Docker state
|
|
14
|
+
- project version hints
|
|
15
|
+
- planned env changes
|
|
16
|
+
- recent env changes
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
So multiple AI agents do not silently use or install different software versions.
|
|
14
19
|
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
Shared development machines drift.
|
|
18
|
-
|
|
19
|
-
One person upgrades Node. Another AI agent installs a global Python tool. A different agent assumes Docker is available. A third agent uses the wrong package manager because it did not see the project lockfile.
|
|
20
|
-
|
|
21
|
-
After a few sessions, nobody is quite sure which runtime is safe to use.
|
|
22
|
-
|
|
23
|
-
`aienvmp` is designed to prevent that kind of AI-driven version drift.
|
|
24
|
-
|
|
25
|
-
It is also designed to stay out of the operator's way. By default, `aienvmp` reports warnings and review-required states instead of taking locks, blocking commands, or changing the machine.
|
|
26
|
-
|
|
27
|
-
`aienvmp` turns that machine state into:
|
|
28
|
-
|
|
29
|
-
- `AIENV.md`: an AI-readable environment protocol
|
|
30
|
-
- `.aienvmp/manifest.json`: a lightweight runtime SBOM
|
|
31
|
-
- `.aienvmp/timeline.jsonl`: an append-only environment change ledger
|
|
32
|
-
- `.aienvmp/intents.jsonl`: planned and resolved environment-changing actions
|
|
33
|
-
- `.aienvmp/dashboard.html`: a small human-readable dashboard
|
|
34
|
-
|
|
35
|
-
## Core Idea
|
|
36
|
-
|
|
37
|
-
Before an AI changes the environment, it should know the environment.
|
|
38
|
-
|
|
39
|
-
Before an AI installs or uses a runtime, it should check the shared policy.
|
|
40
|
-
|
|
41
|
-
The policy is advisory by default. It should guide AI agents and humans, not unexpectedly interrupt production or shared workspace operations.
|
|
20
|
+
## Use
|
|
42
21
|
|
|
43
22
|
```bash
|
|
44
|
-
aienvmp
|
|
45
|
-
aienvmp intent --actor agent:codex --action "install pnpm" --target pnpm
|
|
46
|
-
# make the change
|
|
47
|
-
aienvmp scan
|
|
48
|
-
aienvmp compile
|
|
49
|
-
aienvmp record --actor agent:codex --summary "installed pnpm" --target pnpm --evidence "pnpm --version"
|
|
50
|
-
aienvmp resolve --actor human:you --id int_abc123
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Quickstart
|
|
54
|
-
|
|
55
|
-
Install with npm:
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
npm install -g aienvmp
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Or run without installing:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
npx aienvmp init
|
|
65
|
-
npx aienvmp scan
|
|
23
|
+
npx aienvmp sync
|
|
66
24
|
npx aienvmp context
|
|
67
|
-
npx aienvmp compile --agents all
|
|
68
|
-
npx aienvmp dash
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Run locally from this repository while developing:
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
node bin/aienvmp.js init
|
|
75
|
-
node bin/aienvmp.js scan
|
|
76
|
-
node bin/aienvmp.js context
|
|
77
|
-
node bin/aienvmp.js compile --agents all
|
|
78
|
-
node bin/aienvmp.js dash
|
|
79
25
|
```
|
|
80
26
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
node bin/aienvmp.js scan --dir sample-app
|
|
85
|
-
node bin/aienvmp.js context --dir sample-app
|
|
86
|
-
node bin/aienvmp.js dash --dir sample-app
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## AI Agent Workflow
|
|
90
|
-
|
|
91
|
-
1. Read `AIENV.md` or run `aienvmp context`.
|
|
92
|
-
2. Prefer project-local version files such as `.nvmrc`, `.python-version`, `mise.toml`, and `.tool-versions`.
|
|
93
|
-
3. Do not install or switch to a different runtime version when it conflicts with `.aienvmp/policy.yml`.
|
|
94
|
-
4. Ask the user before changing global runtimes, global packages, Docker settings, or package managers.
|
|
95
|
-
5. Record planned environment changes with `aienvmp intent`.
|
|
96
|
-
6. After changes, run `aienvmp scan && aienvmp compile`.
|
|
97
|
-
7. Record what changed with `aienvmp record`.
|
|
98
|
-
8. Resolve the intent with `aienvmp resolve`.
|
|
99
|
-
|
|
100
|
-
Agent files can be injected automatically:
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
aienvmp compile --agents codex,claude,gemini
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Generated agent targets:
|
|
107
|
-
|
|
108
|
-
- `AGENTS.md`
|
|
109
|
-
- `CLAUDE.md`
|
|
110
|
-
- `GEMINI.md`
|
|
111
|
-
|
|
112
|
-
Codex can also use the repo-scoped skill:
|
|
27
|
+
## Output
|
|
113
28
|
|
|
114
29
|
```text
|
|
115
|
-
.
|
|
30
|
+
AIENV.md
|
|
31
|
+
.aienvmp/manifest.json
|
|
32
|
+
.aienvmp/intents.jsonl
|
|
33
|
+
.aienvmp/timeline.jsonl
|
|
34
|
+
.aienvmp/dashboard.html
|
|
116
35
|
```
|
|
117
36
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
## What AI Agents See
|
|
121
|
-
|
|
122
|
-
`aienvmp context` returns a compact preflight brief:
|
|
123
|
-
|
|
124
|
-
```text
|
|
125
|
-
# AI Preflight Context
|
|
126
|
-
|
|
127
|
-
Status: clear
|
|
128
|
-
Node: 24.14.1
|
|
129
|
-
Python: 3.11.0
|
|
130
|
-
Docker: available
|
|
37
|
+
## For AGENTS.md
|
|
131
38
|
|
|
132
|
-
|
|
133
|
-
- Ask the user before global runtime, package manager, Docker, or global package changes.
|
|
134
|
-
- Prefer project-local version files and local environments.
|
|
135
|
-
- Before planned env changes, run `aienvmp intent --actor <agent:id> --action <planned-change>`.
|
|
136
|
-
- After env changes, run `aienvmp scan && aienvmp compile`.
|
|
137
|
-
- Then run `aienvmp record --actor <agent:id> --summary <what-changed>`.
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
For tool integrations:
|
|
39
|
+
`aienvmp` does not replace AGENTS.md. It gives AGENTS.md a live environment source of truth.
|
|
141
40
|
|
|
142
41
|
```bash
|
|
143
|
-
aienvmp
|
|
144
|
-
aienvmp doctor --json
|
|
145
|
-
aienvmp doctor --ci
|
|
42
|
+
npx aienvmp snippet agents
|
|
146
43
|
```
|
|
147
44
|
|
|
148
|
-
`doctor --ci` is the explicit strict path. Normal `doctor` output is advisory and exits successfully so it does not disrupt shared operations.
|
|
149
|
-
|
|
150
45
|
## Commands
|
|
151
46
|
|
|
152
|
-
| Command | Purpose |
|
|
153
|
-
| --- | --- |
|
|
154
|
-
| `init` | Create `.aienvmp/` |
|
|
155
|
-
| `scan` | Collect runtime/tooling versions and write `.aienvmp/manifest.json` |
|
|
156
|
-
| `context` | Print short AI preflight context |
|
|
157
|
-
| `intent` | Record a planned environment-impacting action |
|
|
158
|
-
| `resolve` | Mark an intent as resolved or cancelled |
|
|
159
|
-
| `record` | Append an agent/human environment change to the ledger |
|
|
160
|
-
| `compile` | Write `AIENV.md` and optionally inject agent files |
|
|
161
|
-
| `diff` | Compare current and previous manifests |
|
|
162
|
-
| `doctor` | Report environment warnings |
|
|
163
|
-
| `dash` | Generate `.aienvmp/dashboard.html` |
|
|
164
|
-
|
|
165
|
-
Intent IDs are short and prefix-matchable, so `aienvmp resolve --id int_mabc` works when it matches one open intent.
|
|
166
|
-
|
|
167
|
-
## What It Scans Today
|
|
168
|
-
|
|
169
|
-
- OS: platform, release, arch, hostname, shell
|
|
170
|
-
- Runtimes: Node.js, Python, Go, Java, Rust
|
|
171
|
-
- Package managers and version managers: npm, pnpm, yarn, uv, pip, pipx, mise, asdf, pyenv, nvm, fnm, volta
|
|
172
|
-
- Containers: Docker and Docker Compose
|
|
173
|
-
- Project hints: `.nvmrc`, `.python-version`, `mise.toml`, `.tool-versions`, lockfiles, `package.json`, `pyproject.toml`, `Dockerfile`
|
|
174
|
-
- Agent integration files: Codex, Claude, Gemini, Cursor, Copilot targets
|
|
175
|
-
|
|
176
|
-
## Version Drift Prevention
|
|
177
|
-
|
|
178
|
-
The highest-priority use case is preventing multiple AI agents from installing or using different software versions in the same workspace.
|
|
179
|
-
|
|
180
|
-
`aienvmp` checks project hints and policy against the currently detected environment.
|
|
181
|
-
|
|
182
|
-
Example `.aienvmp/policy.yml`:
|
|
183
|
-
|
|
184
|
-
```yaml
|
|
185
|
-
node: 24
|
|
186
|
-
python: 3.11
|
|
187
|
-
packageManager: npm
|
|
188
|
-
globalInstalls: ask-first
|
|
189
|
-
runtimeChanges: ask-first
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
If an AI tries to use Node 22 when policy requires Node 24, `aienvmp doctor` and `aienvmp context` should surface that mismatch before work continues.
|
|
193
|
-
|
|
194
|
-
Example warning:
|
|
195
|
-
|
|
196
|
-
```text
|
|
197
|
-
.aienvmp/policy.yml requires node 24, but detected 22.11.0.
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
Normal commands remain non-blocking. Use CI/strict mode only when you explicitly want a warning to fail automation.
|
|
201
|
-
|
|
202
|
-
## Output Layout
|
|
203
|
-
|
|
204
|
-
```text
|
|
205
|
-
.aienvmp/
|
|
206
|
-
manifest.json
|
|
207
|
-
timeline.jsonl
|
|
208
|
-
intents.jsonl
|
|
209
|
-
dashboard.html
|
|
210
|
-
AIENV.md
|
|
211
|
-
AGENTS.md
|
|
212
|
-
CLAUDE.md
|
|
213
|
-
GEMINI.md
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
## Dashboard
|
|
217
|
-
|
|
218
|
-
The dashboard is a static HTML file. It is derived from the manifest, warnings, intents, and ledger.
|
|
219
|
-
|
|
220
47
|
```bash
|
|
221
|
-
aienvmp
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
.aienvmp/dashboard.html
|
|
48
|
+
aienvmp sync # update env map, light SBOM, ledger, dashboard
|
|
49
|
+
aienvmp context # AI preflight brief
|
|
50
|
+
aienvmp context --json # machine-readable AI decision context
|
|
51
|
+
aienvmp intent # record a planned env change
|
|
52
|
+
aienvmp record # record what changed
|
|
53
|
+
aienvmp doctor --ci # strict CI check
|
|
228
54
|
```
|
|
229
55
|
|
|
230
|
-
##
|
|
56
|
+
## Principles
|
|
231
57
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
-
|
|
236
|
-
with:
|
|
237
|
-
directory: "."
|
|
238
|
-
agents: "codex,claude,gemini"
|
|
239
|
-
strict: "false"
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
Set `strict: "true"` only when you want policy warnings to fail automation.
|
|
243
|
-
|
|
244
|
-
See [examples/github-action.yml](examples/github-action.yml).
|
|
245
|
-
|
|
246
|
-
## How This Differs From SBOM Tools
|
|
247
|
-
|
|
248
|
-
`aienvmp` is not trying to replace full SBOM or security scanners.
|
|
249
|
-
|
|
250
|
-
Tools like Syft, Trivy, Dependency-Track, and osquery are excellent for component inventory, vulnerability scanning, and system instrumentation.
|
|
251
|
-
|
|
252
|
-
`aienvmp` focuses on a narrower AI workflow:
|
|
253
|
-
|
|
254
|
-
- What environment should this AI agent assume?
|
|
255
|
-
- What must it avoid changing globally?
|
|
256
|
-
- What changed recently?
|
|
257
|
-
- Which agent planned or performed the change?
|
|
258
|
-
- What should the next agent read before continuing?
|
|
259
|
-
|
|
260
|
-
Think of it as an **AI coordination layer over a lightweight runtime SBOM**.
|
|
261
|
-
|
|
262
|
-
## Why It Is Different
|
|
263
|
-
|
|
264
|
-
- **AI-first**: the primary consumer is an AI coding agent, not only a human operator.
|
|
265
|
-
- **Version drift prevention**: policy warnings are designed to stop agents from silently using different Node, Python, Docker, or package manager assumptions.
|
|
266
|
-
- **Change intent before change**: agents can declare intent before touching shared environment state.
|
|
267
|
-
- **Append-only ledger**: environment-impacting changes are recorded for the next agent.
|
|
268
|
-
- **Non-blocking operations**: normal warnings do not lock or break the machine; strict mode is opt-in.
|
|
269
|
-
- **Small surface area**: no daemon, no database, no hosted service required.
|
|
270
|
-
|
|
271
|
-
## Project Links
|
|
272
|
-
|
|
273
|
-
- [Roadmap](ROADMAP.md)
|
|
274
|
-
- [Security policy](SECURITY.md)
|
|
275
|
-
- [Contributing](CONTRIBUTING.md)
|
|
276
|
-
|
|
277
|
-
## Status
|
|
278
|
-
|
|
279
|
-
This is an early prototype.
|
|
280
|
-
|
|
281
|
-
Good fit today:
|
|
282
|
-
|
|
283
|
-
- shared development servers
|
|
284
|
-
- AI coding workspaces
|
|
285
|
-
- local projects using Codex, Claude, Gemini, Cursor, or Copilot
|
|
286
|
-
- self-hosted runner experiments
|
|
287
|
-
|
|
288
|
-
Planned next:
|
|
289
|
-
|
|
290
|
-
- deeper policy enforcement
|
|
291
|
-
- deeper nvm/fnm/volta/pyenv/mise/asdf scans
|
|
292
|
-
- global npm, pipx, and uv tool inventory
|
|
293
|
-
|
|
294
|
-
See [ROADMAP.md](ROADMAP.md) for the longer plan.
|
|
58
|
+
- simple by default
|
|
59
|
+
- AI-first
|
|
60
|
+
- lightweight
|
|
61
|
+
- non-blocking unless strict mode is requested
|
|
295
62
|
|
|
296
63
|
## Development
|
|
297
64
|
|
|
298
65
|
```bash
|
|
299
66
|
node --test
|
|
300
|
-
npm pack --dry-run
|
|
301
67
|
npm run smoke
|
|
68
|
+
npm pack --dry-run
|
|
302
69
|
```
|
|
303
70
|
|
|
304
|
-
##
|
|
71
|
+
## Links
|
|
72
|
+
|
|
73
|
+
[Roadmap](ROADMAP.md) / [Security](SECURITY.md) / [Contributing](CONTRIBUTING.md)
|
|
305
74
|
|
|
306
75
|
Apache-2.0
|
package/action.yml
CHANGED
|
@@ -7,10 +7,6 @@ inputs:
|
|
|
7
7
|
description: Workspace directory to scan
|
|
8
8
|
required: false
|
|
9
9
|
default: "."
|
|
10
|
-
agents:
|
|
11
|
-
description: Agent files to inject during compile
|
|
12
|
-
required: false
|
|
13
|
-
default: ""
|
|
14
10
|
strict:
|
|
15
11
|
description: Fail when doctor reports warnings
|
|
16
12
|
required: false
|
|
@@ -19,18 +15,9 @@ inputs:
|
|
|
19
15
|
runs:
|
|
20
16
|
using: composite
|
|
21
17
|
steps:
|
|
22
|
-
- name:
|
|
23
|
-
shell: bash
|
|
24
|
-
run: node "$GITHUB_ACTION_PATH/bin/aienvmp.js" scan --dir "${{ inputs.directory }}"
|
|
25
|
-
|
|
26
|
-
- name: Compile agent context
|
|
18
|
+
- name: Sync environment map
|
|
27
19
|
shell: bash
|
|
28
|
-
run:
|
|
29
|
-
if [ -n "${{ inputs.agents }}" ]; then
|
|
30
|
-
node "$GITHUB_ACTION_PATH/bin/aienvmp.js" compile --dir "${{ inputs.directory }}" --agents "${{ inputs.agents }}"
|
|
31
|
-
else
|
|
32
|
-
node "$GITHUB_ACTION_PATH/bin/aienvmp.js" compile --dir "${{ inputs.directory }}"
|
|
33
|
-
fi
|
|
20
|
+
run: node "$GITHUB_ACTION_PATH/bin/aienvmp.js" sync --dir "${{ inputs.directory }}" --quiet
|
|
34
21
|
|
|
35
22
|
- name: Doctor
|
|
36
23
|
shell: bash
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -8,6 +8,8 @@ import { contextWorkspace } from "./commands/context.js";
|
|
|
8
8
|
import { recordWorkspace } from "./commands/record.js";
|
|
9
9
|
import { intentWorkspace } from "./commands/intent.js";
|
|
10
10
|
import { resolveWorkspace } from "./commands/resolve.js";
|
|
11
|
+
import { syncWorkspace } from "./commands/sync.js";
|
|
12
|
+
import { snippetWorkspace } from "./commands/snippet.js";
|
|
11
13
|
import { readFileSync } from "node:fs";
|
|
12
14
|
|
|
13
15
|
const commands = new Map([
|
|
@@ -20,7 +22,9 @@ const commands = new Map([
|
|
|
20
22
|
["context", contextWorkspace],
|
|
21
23
|
["record", recordWorkspace],
|
|
22
24
|
["intent", intentWorkspace],
|
|
23
|
-
["resolve", resolveWorkspace]
|
|
25
|
+
["resolve", resolveWorkspace],
|
|
26
|
+
["sync", syncWorkspace],
|
|
27
|
+
["snippet", snippetWorkspace]
|
|
24
28
|
]);
|
|
25
29
|
|
|
26
30
|
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
@@ -68,13 +72,23 @@ function printUsage() {
|
|
|
68
72
|
console.log(`aienvmp - AI-first env map + lightweight runtime SBOM
|
|
69
73
|
|
|
70
74
|
Usage:
|
|
75
|
+
aienvmp sync [--dir .] [--json] [--quiet]
|
|
76
|
+
aienvmp context [--dir .] [--json]
|
|
77
|
+
|
|
78
|
+
Common:
|
|
79
|
+
aienvmp sync update AIENV.md, manifest, ledger, intents, and dashboard
|
|
80
|
+
aienvmp context print the AI preflight brief
|
|
81
|
+
aienvmp snippet print an AGENTS.md pointer snippet
|
|
82
|
+
aienvmp dash regenerate/open the lightweight dashboard
|
|
83
|
+
|
|
84
|
+
Advanced:
|
|
71
85
|
aienvmp init [--dir .]
|
|
72
86
|
aienvmp scan [--dir .]
|
|
73
|
-
aienvmp context [--dir .] [--json]
|
|
74
87
|
aienvmp intent [--dir .] --actor agent:codex --action "install pnpm"
|
|
75
88
|
aienvmp resolve [--dir .] --actor human:you --id <intent-id> [--status resolved|cancelled]
|
|
76
89
|
aienvmp record [--dir .] --actor agent:codex --summary "updated .nvmrc" [--target node] [--before 20] [--after 24]
|
|
77
|
-
aienvmp
|
|
90
|
+
aienvmp snippet [agents|claude|gemini] [--write AGENTS.md]
|
|
91
|
+
aienvmp compile [--dir .]
|
|
78
92
|
aienvmp diff [--dir .]
|
|
79
93
|
aienvmp doctor [--dir .] [--json] [--ci]
|
|
80
94
|
aienvmp dash [--dir .] [--open]
|
package/src/commands/compile.js
CHANGED
|
@@ -1,41 +1,25 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import { diagnose } from "../doctor.js";
|
|
4
|
-
import { readJson
|
|
3
|
+
import { readJson } from "../fsutil.js";
|
|
5
4
|
import { openIntents, readJsonl, readTimeline } from "../timeline.js";
|
|
6
5
|
import { aiEnvPath, intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
|
|
7
|
-
import {
|
|
6
|
+
import { renderAIEnv } from "../render.js";
|
|
8
7
|
import { loadPolicy, policyWarnings } from "../policy.js";
|
|
9
8
|
|
|
10
|
-
const agentFiles = {
|
|
11
|
-
codex: "AGENTS.md",
|
|
12
|
-
claude: "CLAUDE.md",
|
|
13
|
-
gemini: "GEMINI.md"
|
|
14
|
-
};
|
|
15
|
-
|
|
16
9
|
export async function compileWorkspace(args) {
|
|
17
10
|
const dir = workspaceDir(args);
|
|
18
11
|
const manifest = await readJson(manifestPath(dir));
|
|
19
|
-
if (!manifest) throw new Error("missing manifest; run `aienvmp
|
|
12
|
+
if (!manifest) throw new Error("missing manifest; run `aienvmp sync` first");
|
|
20
13
|
const timeline = await readTimeline(timelinePath(dir));
|
|
21
14
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
22
15
|
const policy = await loadPolicy(dir);
|
|
23
16
|
const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
|
|
24
17
|
const rendered = renderAIEnv(manifest, timeline, warnings, intents, policy);
|
|
25
18
|
await fs.writeFile(aiEnvPath(dir), rendered, "utf8");
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
for (const agent of agents) {
|
|
29
|
-
const rel = agentFiles[agent];
|
|
30
|
-
if (!rel) continue;
|
|
31
|
-
await replaceMarkerBlock(path.join(dir, rel), markerBegin, markerEnd, block);
|
|
19
|
+
if (!args.quiet) {
|
|
20
|
+
console.log(`compiled ${aiEnvPath(dir)}`);
|
|
32
21
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function parseAgents(value) {
|
|
38
|
-
if (!value) return [];
|
|
39
|
-
if (value === true || value === "all") return Object.keys(agentFiles);
|
|
40
|
-
return String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
22
|
+
return {
|
|
23
|
+
aiEnv: aiEnvPath(dir)
|
|
24
|
+
};
|
|
41
25
|
}
|
package/src/commands/context.js
CHANGED
|
@@ -8,14 +8,17 @@ import { loadPolicy, policyWarnings } from "../policy.js";
|
|
|
8
8
|
export async function contextWorkspace(args) {
|
|
9
9
|
const dir = workspaceDir(args);
|
|
10
10
|
const manifest = await readJson(manifestPath(dir));
|
|
11
|
-
if (!manifest) throw new Error("missing manifest; run `aienvmp
|
|
11
|
+
if (!manifest) throw new Error("missing manifest; run `aienvmp sync` first");
|
|
12
12
|
const timeline = await readTimeline(timelinePath(dir));
|
|
13
13
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
14
14
|
const policy = await loadPolicy(dir);
|
|
15
15
|
const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
|
|
16
|
+
const decision = contextDecision(warnings, intents);
|
|
16
17
|
if (args.json) {
|
|
17
18
|
console.log(JSON.stringify({
|
|
18
19
|
status: warnings.length ? "review-required" : "clear",
|
|
20
|
+
decision,
|
|
21
|
+
guidance: decision,
|
|
19
22
|
workspace: manifest.workspace,
|
|
20
23
|
runtimes: manifest.runtimes,
|
|
21
24
|
packageManagers: manifest.packageManagers,
|
|
@@ -31,3 +34,25 @@ export async function contextWorkspace(args) {
|
|
|
31
34
|
}
|
|
32
35
|
console.log(renderContext(manifest, timeline, warnings, intents, policy));
|
|
33
36
|
}
|
|
37
|
+
|
|
38
|
+
function contextDecision(warnings, intents) {
|
|
39
|
+
const reviewRequired = warnings.length > 0 || intents.length > 0;
|
|
40
|
+
return {
|
|
41
|
+
canProceed: !reviewRequired,
|
|
42
|
+
safeForProjectLocalWork: warnings.length === 0,
|
|
43
|
+
reviewRequired,
|
|
44
|
+
environmentChangeRequiresIntent: true,
|
|
45
|
+
globalEnvironmentChangesRequireUserApproval: true,
|
|
46
|
+
pendingIntentCount: intents.length,
|
|
47
|
+
mustNotDo: [
|
|
48
|
+
"do not change global runtimes without user approval",
|
|
49
|
+
"do not install or remove global package managers without user approval",
|
|
50
|
+
"do not change Docker daemon/context assumptions without user approval",
|
|
51
|
+
"do not ignore open intents or review-required warnings"
|
|
52
|
+
],
|
|
53
|
+
recommendedNextActions: warnings.length
|
|
54
|
+
? ["review warnings", "ask the user before environment changes", "record intent before changes"]
|
|
55
|
+
: ["continue with project-local work", "run aienvmp intent before environment changes"],
|
|
56
|
+
nextCommand: warnings.length ? "review warnings before changing environment" : "continue with project-local work"
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/commands/dash.js
CHANGED
|
@@ -11,7 +11,7 @@ import { loadPolicy, policyWarnings } from "../policy.js";
|
|
|
11
11
|
export async function dashWorkspace(args) {
|
|
12
12
|
const dir = workspaceDir(args);
|
|
13
13
|
const manifest = await readJson(manifestPath(dir));
|
|
14
|
-
if (!manifest) throw new Error("missing manifest; run `aienvmp
|
|
14
|
+
if (!manifest) throw new Error("missing manifest; run `aienvmp sync` first");
|
|
15
15
|
const timeline = await readTimeline(timelinePath(dir));
|
|
16
16
|
const intents = openIntents(await readJsonl(intentsPath(dir)));
|
|
17
17
|
const policy = await loadPolicy(dir);
|
|
@@ -20,8 +20,9 @@ export async function dashWorkspace(args) {
|
|
|
20
20
|
const out = dashboardPath(dir);
|
|
21
21
|
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
22
22
|
await fs.writeFile(out, html, "utf8");
|
|
23
|
-
console.log(`dashboard: ${out}`);
|
|
23
|
+
if (!args.quiet) console.log(`dashboard: ${out}`);
|
|
24
24
|
if (args.open) openFile(out);
|
|
25
|
+
return { dashboard: out };
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function openFile(file) {
|
package/src/commands/diff.js
CHANGED
|
@@ -6,7 +6,7 @@ export async function diffWorkspace(args) {
|
|
|
6
6
|
const dir = workspaceDir(args);
|
|
7
7
|
const previous = await readJson(previousManifestPath(dir));
|
|
8
8
|
const current = await readJson(manifestPath(dir));
|
|
9
|
-
if (!current) throw new Error("missing manifest; run `aienvmp
|
|
9
|
+
if (!current) throw new Error("missing manifest; run `aienvmp sync` first");
|
|
10
10
|
const changes = diffManifests(previous, current);
|
|
11
11
|
if (!changes.length) {
|
|
12
12
|
console.log("no environment changes detected");
|
package/src/commands/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { loadPolicy, policyWarnings } from "../policy.js";
|
|
|
6
6
|
export async function doctorWorkspace(args) {
|
|
7
7
|
const dir = workspaceDir(args);
|
|
8
8
|
const manifest = await readJson(manifestPath(dir));
|
|
9
|
-
if (!manifest) throw new Error("missing manifest; run `aienvmp
|
|
9
|
+
if (!manifest) throw new Error("missing manifest; run `aienvmp sync` first");
|
|
10
10
|
const policy = await loadPolicy(dir);
|
|
11
11
|
const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
|
|
12
12
|
if (args.json) {
|
package/src/commands/init.js
CHANGED
|
@@ -15,5 +15,6 @@ export async function initWorkspace(args) {
|
|
|
15
15
|
"runtimeChanges: ask-first",
|
|
16
16
|
""
|
|
17
17
|
].join("\n"), { flag: "wx" }).catch(() => {});
|
|
18
|
-
console.log(`initialized ${stateDir(dir)}`);
|
|
18
|
+
if (!args.quiet) console.log(`initialized ${stateDir(dir)}`);
|
|
19
|
+
return { stateDir: stateDir(dir) };
|
|
19
20
|
}
|
package/src/commands/scan.js
CHANGED
|
@@ -22,7 +22,15 @@ export async function scanWorkspace(args) {
|
|
|
22
22
|
change
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
if (!args.quiet) {
|
|
26
|
+
console.log(`scanned ${dir}`);
|
|
27
|
+
console.log(`manifest: ${currentPath}`);
|
|
28
|
+
console.log(`changes: ${changes.length}`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
dir,
|
|
32
|
+
manifest: currentPath,
|
|
33
|
+
timeline: timelinePath(dir),
|
|
34
|
+
changes: changes.length
|
|
35
|
+
};
|
|
28
36
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { markerBegin, markerEnd, renderAgentPointer } from "../render.js";
|
|
3
|
+
import { replaceMarkerBlock } from "../fsutil.js";
|
|
4
|
+
import { workspaceDir } from "../paths.js";
|
|
5
|
+
|
|
6
|
+
const defaultTargets = {
|
|
7
|
+
agents: "AGENTS.md",
|
|
8
|
+
codex: "AGENTS.md",
|
|
9
|
+
claude: "CLAUDE.md",
|
|
10
|
+
gemini: "GEMINI.md"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function snippetWorkspace(args) {
|
|
14
|
+
const target = String(args._?.[0] || args.agent || "agents").toLowerCase();
|
|
15
|
+
const block = renderAgentPointer(target);
|
|
16
|
+
|
|
17
|
+
if (args.write) {
|
|
18
|
+
const dir = workspaceDir(args);
|
|
19
|
+
const rel = args.write === true ? defaultTargets[target] || "AGENTS.md" : String(args.write);
|
|
20
|
+
await replaceMarkerBlock(path.join(dir, rel), markerBegin, markerEnd, block);
|
|
21
|
+
if (!args.quiet) console.log(`snippet written: ${rel}`);
|
|
22
|
+
return { file: rel, target };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(block);
|
|
26
|
+
return { target };
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { initWorkspace } from "./init.js";
|
|
2
|
+
import { scanWorkspace } from "./scan.js";
|
|
3
|
+
import { compileWorkspace } from "./compile.js";
|
|
4
|
+
import { dashWorkspace } from "./dash.js";
|
|
5
|
+
|
|
6
|
+
export async function syncWorkspace(args) {
|
|
7
|
+
const quiet = args.quiet || args.json;
|
|
8
|
+
const next = { ...args, quiet };
|
|
9
|
+
|
|
10
|
+
const initialized = await initWorkspace(next);
|
|
11
|
+
const scanned = await scanWorkspace(next);
|
|
12
|
+
const compiled = await compileWorkspace(next);
|
|
13
|
+
const dashboard = await dashWorkspace(next);
|
|
14
|
+
|
|
15
|
+
const result = {
|
|
16
|
+
status: "ok",
|
|
17
|
+
outputs: {
|
|
18
|
+
aiEnv: compiled.aiEnv,
|
|
19
|
+
manifest: scanned.manifest,
|
|
20
|
+
timeline: scanned.timeline,
|
|
21
|
+
dashboard: dashboard.dashboard
|
|
22
|
+
},
|
|
23
|
+
changes: scanned.changes,
|
|
24
|
+
initialized: initialized.stateDir
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (args.json) {
|
|
28
|
+
console.log(JSON.stringify(result, null, 2));
|
|
29
|
+
} else if (!quiet) {
|
|
30
|
+
console.log("sync complete: AIENV.md, manifest, ledger, intents, and dashboard are up to date");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return result;
|
|
34
|
+
}
|
package/src/manifest.js
CHANGED
|
@@ -7,8 +7,13 @@ import { exists } from "./fsutil.js";
|
|
|
7
7
|
export async function buildManifest(dir) {
|
|
8
8
|
const now = new Date().toISOString();
|
|
9
9
|
const manifest = {
|
|
10
|
+
schemaName: "aienvmp.runtime-sbom",
|
|
10
11
|
schemaVersion: 1,
|
|
11
12
|
generatedAt: now,
|
|
13
|
+
generatedBy: {
|
|
14
|
+
name: "aienvmp",
|
|
15
|
+
command: "aienvmp sync"
|
|
16
|
+
},
|
|
12
17
|
workspace: {
|
|
13
18
|
path: dir,
|
|
14
19
|
name: path.basename(dir)
|
|
@@ -24,10 +29,51 @@ export async function buildManifest(dir) {
|
|
|
24
29
|
preflightCommand: "aienvmp context",
|
|
25
30
|
intentCommand: "aienvmp intent --actor <agent:id> --action <planned-change>",
|
|
26
31
|
recordCommand: "aienvmp record --actor <agent:id> --summary <what-changed>",
|
|
27
|
-
afterEnvironmentChange: ["aienvmp
|
|
32
|
+
afterEnvironmentChange: ["aienvmp sync"],
|
|
28
33
|
globalRuntimeChangeRequiresUserApproval: true,
|
|
29
34
|
globalInstallPolicy: "ask-first",
|
|
30
35
|
projectLocalChanges: "allowed-when-task-requires"
|
|
36
|
+
},
|
|
37
|
+
evidence: {
|
|
38
|
+
runtimes: {
|
|
39
|
+
node: "node --version",
|
|
40
|
+
python: "python --version",
|
|
41
|
+
python3: "python3 --version",
|
|
42
|
+
go: "go version",
|
|
43
|
+
java: "java -version",
|
|
44
|
+
rustc: "rustc --version"
|
|
45
|
+
},
|
|
46
|
+
packageManagers: {
|
|
47
|
+
npm: "npm --version",
|
|
48
|
+
pnpm: "pnpm --version",
|
|
49
|
+
yarn: "yarn --version",
|
|
50
|
+
uv: "uv --version",
|
|
51
|
+
pip: "pip --version",
|
|
52
|
+
pipx: "pipx --version",
|
|
53
|
+
mise: "mise --version",
|
|
54
|
+
asdf: "asdf --version",
|
|
55
|
+
pyenv: "pyenv --version",
|
|
56
|
+
nvm: "nvm --version",
|
|
57
|
+
fnm: "fnm --version",
|
|
58
|
+
volta: "volta --version"
|
|
59
|
+
},
|
|
60
|
+
containers: {
|
|
61
|
+
docker: "docker --version",
|
|
62
|
+
compose: "docker compose version or docker-compose --version"
|
|
63
|
+
},
|
|
64
|
+
projectHints: [
|
|
65
|
+
".nvmrc",
|
|
66
|
+
".python-version",
|
|
67
|
+
"mise.toml",
|
|
68
|
+
".tool-versions",
|
|
69
|
+
"package.json",
|
|
70
|
+
"pyproject.toml",
|
|
71
|
+
"requirements.txt",
|
|
72
|
+
"Dockerfile",
|
|
73
|
+
"package-lock.json",
|
|
74
|
+
"pnpm-lock.yaml",
|
|
75
|
+
"yarn.lock"
|
|
76
|
+
]
|
|
31
77
|
}
|
|
32
78
|
};
|
|
33
79
|
return manifest;
|
package/src/render.js
CHANGED
|
@@ -13,7 +13,7 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
13
13
|
lines.push("2. Prefer project-local version files such as `.nvmrc`, `.python-version`, `mise.toml`, and `.tool-versions`.");
|
|
14
14
|
lines.push("3. Ask the user before changing global environment state.");
|
|
15
15
|
lines.push("4. Record planned environment changes with `aienvmp intent --actor <agent:id> --action <planned-change>`.");
|
|
16
|
-
lines.push("5. After environment changes, run `aienvmp
|
|
16
|
+
lines.push("5. After environment changes, run `aienvmp sync`.");
|
|
17
17
|
lines.push("6. Record what changed with `aienvmp record --actor <agent:id> --summary <what-changed>`.", "");
|
|
18
18
|
lines.push("## Current Policy", "");
|
|
19
19
|
lines.push(...policyLines(policy));
|
|
@@ -57,31 +57,32 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
57
57
|
return lines.join("\n");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export function
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const docker = manifest.containers.docker ? "available" : "not detected";
|
|
64
|
-
return `AI environment map is maintained by aienvmp.
|
|
60
|
+
export function renderAgentPointer(target = "agents") {
|
|
61
|
+
const label = target === "claude" ? "Claude" : target === "gemini" ? "Gemini" : "AI agents";
|
|
62
|
+
return `## aienvmp Environment Map
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
${label} should use \`aienvmp\` as the workspace environment source of truth.
|
|
65
|
+
|
|
66
|
+
Before changing runtimes, package managers, Docker settings, global packages, or environment policy:
|
|
67
|
+
|
|
68
|
+
1. Run \`aienvmp context\`.
|
|
69
|
+
2. Read \`AIENV.md\`.
|
|
70
|
+
3. If the context says \`review-required\`, ask the user before changing the environment.
|
|
71
|
+
4. Record planned environment changes with \`aienvmp intent\`.
|
|
72
|
+
5. After environment changes, run \`aienvmp sync\`.
|
|
72
73
|
6. Record what changed with \`aienvmp record\`.
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
- Node.js: ${node}
|
|
76
|
-
- Python: ${python}
|
|
77
|
-
- Docker: ${docker}`;
|
|
75
|
+
\`aienvmp\` does not replace this instruction file. It provides the live env map, lightweight runtime SBOM, intent log, timeline, and dashboard.`;
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
export function renderContext(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
|
|
79
|
+
const status = warnings.length ? "review-required" : "clear";
|
|
80
|
+
const next = warnings.length ? "Review warnings before changing the environment." : "Continue with project-local work. Record intent before environment changes.";
|
|
81
81
|
return [
|
|
82
82
|
"# AI Preflight Context",
|
|
83
83
|
"",
|
|
84
|
-
`Status: ${
|
|
84
|
+
`Status: ${status}`,
|
|
85
|
+
`Next: ${next}`,
|
|
85
86
|
`Workspace: ${manifest.workspace.path}`,
|
|
86
87
|
`Node: ${manifest.runtimes.node || "not detected"}`,
|
|
87
88
|
`Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
@@ -95,7 +96,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
95
96
|
"- Treat policy mismatches as review-required, not as permission to break ongoing operations.",
|
|
96
97
|
"- Prefer project-local version files and local environments.",
|
|
97
98
|
"- Before planned env changes, run `aienvmp intent --actor <agent:id> --action <planned-change>`.",
|
|
98
|
-
"- After env changes, run `aienvmp
|
|
99
|
+
"- After env changes, run `aienvmp sync`.",
|
|
99
100
|
"- Then run `aienvmp record --actor <agent:id> --summary <what-changed>`.",
|
|
100
101
|
"",
|
|
101
102
|
"Warnings:",
|
|
@@ -159,9 +160,8 @@ const entries=o=>Object.entries(o||{});
|
|
|
159
160
|
const rows=o=>entries(o).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${esc(String(v))}</code></td></tr>\`).join('')||'<tr><td colspan="2">None detected</td></tr>';
|
|
160
161
|
const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
|
|
161
162
|
const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
|
|
162
|
-
const totalTools=entries(manifest.runtimes).length+entries(manifest.packageManagers).length+entries(manifest.containers).length;
|
|
163
163
|
const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
|
|
164
|
-
const agentCards=Object.entries(agentNames).map(([key,label])=>\`<div class="agent"><strong>\${label}</strong><span>\${manifest.agentFiles?.[key]?'
|
|
164
|
+
const agentCards=Object.entries(agentNames).map(([key,label])=>\`<div class="agent"><strong>\${label}</strong><span>\${manifest.agentFiles?.[key]?'instruction file detected':'not detected'}</span></div>\`).join('');
|
|
165
165
|
const warnHtml=warnings.length?'<div class="warnings">'+warnings.map(w=>\`<div class="warning">\${esc(w.message)}</div>\`).join('')+'</div>':'<div class="okline">No blocking environment warnings detected.</div>';
|
|
166
166
|
const timelineHtml=timeline.length?'<div class="timeline">'+timeline.slice(-8).reverse().map(t=>\`<div class="event"><time>\${esc(t.at.replace('T',' ').slice(0,16))}</time><div><b>\${esc(t.actor||'system')}</b> \${esc(timelineLabel(t))}</div></div>\`).join('')+'</div>':'<div class="okline">No previous environment changes recorded.</div>';
|
|
167
167
|
const intentsHtml=intents.length?'<div class="timeline">'+intents.slice(-6).reverse().map(i=>\`<div class="event"><time>\${esc(i.at.replace('T',' ').slice(0,16))}</time><div><b>\${esc(i.actor)}</b> plans \${esc(i.action)}</div></div>\`).join('')+'</div>':'<div class="okline">No pending agent intents recorded.</div>';
|
|
@@ -196,7 +196,7 @@ document.getElementById('app').innerHTML=\`
|
|
|
196
196
|
<div style="height:14px"></div>
|
|
197
197
|
\${card('Agent Intents','<span class="pill">'+intents.length+' open</span>',intentsHtml)}
|
|
198
198
|
<div style="height:14px"></div>
|
|
199
|
-
\${card('Agent
|
|
199
|
+
\${card('Agent Pointers','<span class="pill">'+entries(manifest.agentFiles).filter(([,v])=>v).length+' detected</span>','<div class="agents">'+agentCards+'</div>')}
|
|
200
200
|
<div style="height:14px"></div>
|
|
201
201
|
\${card('Snapshot','',\`<table><tr><th>OS</th><td>\${esc(manifest.os.platform)} \${esc(manifest.os.release)} \${esc(manifest.os.arch)}</td></tr><tr><th>Shell</th><td>\${esc(manifest.os.shell||'unknown')}</td></tr><tr><th>Workspace</th><td><div class="path">\${esc(manifest.workspace.path)}</div></td></tr></table>\`)}
|
|
202
202
|
</aside>
|
|
@@ -236,6 +236,7 @@ function formatTimeline(item) {
|
|
|
236
236
|
function contextLines(manifest, warnings, intents) {
|
|
237
237
|
return [
|
|
238
238
|
`- Status: ${warnings.length ? "review-required" : "clear"}`,
|
|
239
|
+
`- Next: ${warnings.length ? "review warnings before environment changes" : "continue with project-local work"}`,
|
|
239
240
|
`- Node: ${manifest.runtimes.node || "not detected"}`,
|
|
240
241
|
`- Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
241
242
|
`- Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
|