aienvmp 0.1.0 → 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.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: aienvmp
3
+ description: Use before changing runtimes, package managers, Docker settings, global packages, or environment policy in an AI coding workspace. Reads aienvmp context, prevents AI-driven version drift, records intent before environment changes, and records what changed afterward.
4
+ ---
5
+
6
+ # aienvmp
7
+
8
+ Use `aienvmp` as the shared environment source of truth for this workspace.
9
+
10
+ The goal is to help multiple AI agents avoid silently installing or using different versions of Node, Python, Docker, package managers, or global tools.
11
+
12
+ ## Preflight
13
+
14
+ Before environment-impacting work, run:
15
+
16
+ ```bash
17
+ npx aienvmp context
18
+ ```
19
+
20
+ If the output says `review-required`, do not change global runtimes, package managers, Docker settings, or global packages without asking the user.
21
+
22
+ For machine-readable context, use:
23
+
24
+ ```bash
25
+ npx aienvmp context --json
26
+ ```
27
+
28
+ ## Before Environment Changes
29
+
30
+ Record intent before changing shared environment state:
31
+
32
+ ```bash
33
+ npx aienvmp intent --actor agent:codex --action "<planned change>" --target "<tool-or-runtime>"
34
+ ```
35
+
36
+ Use this for changes such as:
37
+
38
+ - installing or upgrading Node, Python, Docker, package managers, or global CLIs
39
+ - changing `.nvmrc`, `.python-version`, `mise.toml`, `.tool-versions`, or `.aienvmp/policy.yml`
40
+ - switching package managers
41
+ - changing Docker daemon/context assumptions
42
+
43
+ ## After Environment Changes
44
+
45
+ Refresh the environment map:
46
+
47
+ ```bash
48
+ npx aienvmp sync
49
+ ```
50
+
51
+ Record what changed:
52
+
53
+ ```bash
54
+ npx aienvmp record --actor agent:codex --summary "<what changed>" --target "<tool-or-runtime>" --evidence "<command or file>"
55
+ ```
56
+
57
+ Resolve the original intent if complete:
58
+
59
+ ```bash
60
+ npx aienvmp resolve --actor agent:codex --id "<intent-id>"
61
+ ```
62
+
63
+ ## Safety Rules
64
+
65
+ - `aienvmp` warnings are non-blocking by default.
66
+ - Treat policy mismatches as review-required.
67
+ - Do not install, upgrade, downgrade, or remove global software unless the user explicitly asks.
68
+ - Prefer project-local version files and local environments.
69
+ - Do not use warnings as permission to interrupt production or shared workspace operations.
70
+ - Use `npx aienvmp doctor --ci` only in CI or explicit strict-mode automation.
71
+
72
+ ## Normal Coding Work
73
+
74
+ For ordinary source edits that do not affect runtime versions, package managers, Docker settings, global packages, or environment policy, you do not need to record an intent.
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,295 +4,72 @@
4
4
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
5
5
  [![Node](https://img.shields.io/badge/node-%3E%3D18-339933.svg)](package.json)
6
6
 
7
- **aienvmp = AI Environment Map**
7
+ **AI Environment Map.**
8
8
 
9
- `aienvmp` is an AI-first environment map and change ledger for shared AI coding workspaces.
9
+ `aienvmp` gives AI agents a shared view of the current dev environment:
10
10
 
11
- It helps multiple AI coding agents read the same runtime state, avoid installing or using conflicting software versions, declare environment-changing intent, record what changed, and keep humans informed through a lightweight dashboard.
11
+ - runtime versions
12
+ - package managers
13
+ - Docker state
14
+ - project version hints
15
+ - planned env changes
16
+ - recent env changes
12
17
 
13
- It is intentionally small: use it beside SBOM scanners, vulnerability scanners, version managers, and CI systems.
18
+ So multiple AI agents do not silently use or install different software versions.
14
19
 
15
- ## Why
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.
42
-
43
- ```bash
44
- aienvmp context
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:
20
+ ## Use
62
21
 
63
22
  ```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
- Try the included sample app:
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
- ## What AI Agents See
113
-
114
- `aienvmp context` returns a compact preflight brief:
27
+ ## Output
115
28
 
116
29
  ```text
117
- # AI Preflight Context
118
-
119
- Status: clear
120
- Node: 24.14.1
121
- Python: 3.11.0
122
- Docker: available
123
-
124
- Must follow:
125
- - Ask the user before global runtime, package manager, Docker, or global package changes.
126
- - Prefer project-local version files and local environments.
127
- - Before planned env changes, run `aienvmp intent --actor <agent:id> --action <planned-change>`.
128
- - After env changes, run `aienvmp scan && aienvmp compile`.
129
- - Then run `aienvmp record --actor <agent:id> --summary <what-changed>`.
130
- ```
131
-
132
- For tool integrations:
133
-
134
- ```bash
135
- aienvmp context --json
136
- aienvmp doctor --json
137
- aienvmp doctor --ci
138
- ```
139
-
140
- `doctor --ci` is the explicit strict path. Normal `doctor` output is advisory and exits successfully so it does not disrupt shared operations.
141
-
142
- ## Commands
143
-
144
- | Command | Purpose |
145
- | --- | --- |
146
- | `init` | Create `.aienvmp/` |
147
- | `scan` | Collect runtime/tooling versions and write `.aienvmp/manifest.json` |
148
- | `context` | Print short AI preflight context |
149
- | `intent` | Record a planned environment-impacting action |
150
- | `resolve` | Mark an intent as resolved or cancelled |
151
- | `record` | Append an agent/human environment change to the ledger |
152
- | `compile` | Write `AIENV.md` and optionally inject agent files |
153
- | `diff` | Compare current and previous manifests |
154
- | `doctor` | Report environment warnings |
155
- | `dash` | Generate `.aienvmp/dashboard.html` |
156
-
157
- Intent IDs are short and prefix-matchable, so `aienvmp resolve --id int_mabc` works when it matches one open intent.
158
-
159
- ## What It Scans Today
160
-
161
- - OS: platform, release, arch, hostname, shell
162
- - Runtimes: Node.js, Python, Go, Java, Rust
163
- - Package managers and version managers: npm, pnpm, yarn, uv, pip, pipx, mise, asdf, pyenv, nvm, fnm, volta
164
- - Containers: Docker and Docker Compose
165
- - Project hints: `.nvmrc`, `.python-version`, `mise.toml`, `.tool-versions`, lockfiles, `package.json`, `pyproject.toml`, `Dockerfile`
166
- - Agent integration files: Codex, Claude, Gemini, Cursor, Copilot targets
167
-
168
- ## Version Drift Prevention
169
-
170
- The highest-priority use case is preventing multiple AI agents from installing or using different software versions in the same workspace.
171
-
172
- `aienvmp` checks project hints and policy against the currently detected environment.
173
-
174
- Example `.aienvmp/policy.yml`:
175
-
176
- ```yaml
177
- node: 24
178
- python: 3.11
179
- packageManager: npm
180
- globalInstalls: ask-first
181
- runtimeChanges: ask-first
182
- ```
183
-
184
- 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.
185
-
186
- Example warning:
187
-
188
- ```text
189
- .aienvmp/policy.yml requires node 24, but detected 22.11.0.
190
- ```
191
-
192
- Normal commands remain non-blocking. Use CI/strict mode only when you explicitly want a warning to fail automation.
193
-
194
- ## Output Layout
195
-
196
- ```text
197
- .aienvmp/
198
- manifest.json
199
- timeline.jsonl
200
- intents.jsonl
201
- dashboard.html
202
30
  AIENV.md
203
- AGENTS.md
204
- CLAUDE.md
205
- GEMINI.md
31
+ .aienvmp/manifest.json
32
+ .aienvmp/intents.jsonl
33
+ .aienvmp/timeline.jsonl
34
+ .aienvmp/dashboard.html
206
35
  ```
207
36
 
208
- ## Dashboard
37
+ ## For AGENTS.md
209
38
 
210
- The dashboard is a static HTML file. It is derived from the manifest, warnings, intents, and ledger.
39
+ `aienvmp` does not replace AGENTS.md. It gives AGENTS.md a live environment source of truth.
211
40
 
212
41
  ```bash
213
- aienvmp dash
214
- ```
215
-
216
- Open:
217
-
218
- ```text
219
- .aienvmp/dashboard.html
42
+ npx aienvmp snippet agents
220
43
  ```
221
44
 
222
- ## GitHub Action
223
-
224
- Use `aienvmp` in CI without making it disruptive by default:
45
+ ## Commands
225
46
 
226
- ```yaml
227
- - uses: soovwv/aienvmp@main
228
- with:
229
- directory: "."
230
- agents: "codex,claude,gemini"
231
- strict: "false"
47
+ ```bash
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
232
54
  ```
233
55
 
234
- Set `strict: "true"` only when you want policy warnings to fail automation.
235
-
236
- See [examples/github-action.yml](examples/github-action.yml).
237
-
238
- ## How This Differs From SBOM Tools
239
-
240
- `aienvmp` is not trying to replace full SBOM or security scanners.
241
-
242
- Tools like Syft, Trivy, Dependency-Track, and osquery are excellent for component inventory, vulnerability scanning, and system instrumentation.
243
-
244
- `aienvmp` focuses on a narrower AI workflow:
245
-
246
- - What environment should this AI agent assume?
247
- - What must it avoid changing globally?
248
- - What changed recently?
249
- - Which agent planned or performed the change?
250
- - What should the next agent read before continuing?
251
-
252
- Think of it as an **AI coordination layer over a lightweight runtime SBOM**.
253
-
254
- ## Why It Is Different
255
-
256
- - **AI-first**: the primary consumer is an AI coding agent, not only a human operator.
257
- - **Version drift prevention**: policy warnings are designed to stop agents from silently using different Node, Python, Docker, or package manager assumptions.
258
- - **Change intent before change**: agents can declare intent before touching shared environment state.
259
- - **Append-only ledger**: environment-impacting changes are recorded for the next agent.
260
- - **Non-blocking operations**: normal warnings do not lock or break the machine; strict mode is opt-in.
261
- - **Small surface area**: no daemon, no database, no hosted service required.
56
+ ## Principles
262
57
 
263
- ## Project Links
264
-
265
- - [Roadmap](ROADMAP.md)
266
- - [Security policy](SECURITY.md)
267
- - [Contributing](CONTRIBUTING.md)
268
-
269
- ## Status
270
-
271
- This is an early prototype.
272
-
273
- Good fit today:
274
-
275
- - shared development servers
276
- - AI coding workspaces
277
- - local projects using Codex, Claude, Gemini, Cursor, or Copilot
278
- - self-hosted runner experiments
279
-
280
- Planned next:
281
-
282
- - deeper policy enforcement
283
- - deeper nvm/fnm/volta/pyenv/mise/asdf scans
284
- - global npm, pipx, and uv tool inventory
285
-
286
- 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
287
62
 
288
63
  ## Development
289
64
 
290
65
  ```bash
291
66
  node --test
292
- npm pack --dry-run
293
67
  npm run smoke
68
+ npm pack --dry-run
294
69
  ```
295
70
 
296
- ## License
71
+ ## Links
72
+
73
+ [Roadmap](ROADMAP.md) / [Security](SECURITY.md) / [Contributing](CONTRIBUTING.md)
297
74
 
298
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: Scan environment
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
@@ -13,5 +13,4 @@ jobs:
13
13
  - uses: soovwv/aienvmp@main
14
14
  with:
15
15
  directory: "."
16
- agents: "codex,claude,gemini"
17
16
  strict: "false"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,8 @@
24
24
  "SECURITY.md",
25
25
  "ROADMAP.md",
26
26
  "action.yml",
27
- "examples"
27
+ "examples",
28
+ ".agents"
28
29
  ],
29
30
  "scripts": {
30
31
  "test": "node --test",
package/src/cli.js CHANGED
@@ -8,6 +8,9 @@ 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";
13
+ import { readFileSync } from "node:fs";
11
14
 
12
15
  const commands = new Map([
13
16
  ["init", initWorkspace],
@@ -19,10 +22,12 @@ const commands = new Map([
19
22
  ["context", contextWorkspace],
20
23
  ["record", recordWorkspace],
21
24
  ["intent", intentWorkspace],
22
- ["resolve", resolveWorkspace]
25
+ ["resolve", resolveWorkspace],
26
+ ["sync", syncWorkspace],
27
+ ["snippet", snippetWorkspace]
23
28
  ]);
24
29
 
25
- const version = "0.1.0";
30
+ const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
26
31
 
27
32
  export async function main(argv) {
28
33
  const [command, ...rest] = argv;
@@ -67,13 +72,23 @@ function printUsage() {
67
72
  console.log(`aienvmp - AI-first env map + lightweight runtime SBOM
68
73
 
69
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:
70
85
  aienvmp init [--dir .]
71
86
  aienvmp scan [--dir .]
72
- aienvmp context [--dir .] [--json]
73
87
  aienvmp intent [--dir .] --actor agent:codex --action "install pnpm"
74
88
  aienvmp resolve [--dir .] --actor human:you --id <intent-id> [--status resolved|cancelled]
75
89
  aienvmp record [--dir .] --actor agent:codex --summary "updated .nvmrc" [--target node] [--before 20] [--after 24]
76
- aienvmp compile [--dir .] [--agents all|codex,claude,gemini]
90
+ aienvmp snippet [agents|claude|gemini] [--write AGENTS.md]
91
+ aienvmp compile [--dir .]
77
92
  aienvmp diff [--dir .]
78
93
  aienvmp doctor [--dir .] [--json] [--ci]
79
94
  aienvmp dash [--dir .] [--open]
@@ -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, replaceMarkerBlock } from "../fsutil.js";
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 { markerBegin, markerEnd, renderAgentBlock, renderAIEnv } from "../render.js";
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 scan` first");
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
- const agents = parseAgents(args.agents);
27
- const block = renderAgentBlock(manifest);
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
- console.log(`compiled ${aiEnvPath(dir)}`);
34
- if (agents.length) console.log(`injected: ${agents.join(", ")}`);
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
  }
@@ -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 scan` first");
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
+ }
@@ -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 scan` first");
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) {
@@ -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 scan` first");
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");
@@ -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 scan` first");
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) {
@@ -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
  }
@@ -22,7 +22,15 @@ export async function scanWorkspace(args) {
22
22
  change
23
23
  });
24
24
  }
25
- console.log(`scanned ${dir}`);
26
- console.log(`manifest: ${currentPath}`);
27
- console.log(`changes: ${changes.length}`);
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 scan", "aienvmp compile"],
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 scan && aienvmp compile`.");
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 renderAgentBlock(manifest) {
61
- const node = manifest.runtimes.node || "not detected";
62
- const python = manifest.runtimes.python || manifest.runtimes.python3 || "not detected";
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
- Before changing runtimes, package managers, Docker, or global packages:
67
- 1. Read AIENV.md.
68
- 2. Run \`aienvmp context\`.
69
- 3. Ask the user before global environment changes.
70
- 4. Record planned changes with \`aienvmp intent\`.
71
- 5. After changes, run \`aienvmp scan && aienvmp compile\`.
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
- Default detected tools:
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: ${warnings.length ? "review-required" : "clear"}`,
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 scan && aienvmp compile`.",
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]?'connected':'not connected'}</span></div>\`).join('');
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 Integration','<span class="pill">'+totalTools+' tools</span>','<div class="agents">'+agentCards+'</div>')}
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"}`,