aienvmp 0.1.12 → 0.1.14

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.14
4
+
5
+ - Added `aienvmp plan` for read-only AI environment action plans.
6
+ - Added optional `aienvmp plan --write` artifacts at `.aienvmp/plan.json` and `.aienvmp/plan.md`.
7
+ - Kept plan output advisory-only with review gates instead of automatic fixes.
8
+
9
+ ## 0.1.13
10
+
11
+ - Added `recommendedActions` to AI handoff output.
12
+ - Added a Recommended Actions dashboard card for human review.
13
+ - Reused the same advisory action engine across context, doctor, handoff, and dashboard.
14
+
3
15
  ## 0.1.12
4
16
 
5
17
  - Added AI-readable `recommendedActions` to `context --json` and `doctor --json`.
package/README.md CHANGED
@@ -17,6 +17,7 @@ Core loop: scan the env, give AI a preflight context, and hand off safe next ste
17
17
  ```bash
18
18
  npx aienvmp sync
19
19
  npx aienvmp context
20
+ npx aienvmp plan
20
21
  npx aienvmp handoff
21
22
  npx aienvmp handoff --record --actor agent:codex
22
23
  ```
@@ -56,7 +57,8 @@ npx aienvmp snippet agents
56
57
  aienvmp sync # update env map, light SBOM, ledger, dashboard
57
58
  aienvmp context # AI preflight brief
58
59
  aienvmp context --json # machine-readable AI decision context + recommended actions
59
- aienvmp handoff # next-agent handoff summary
60
+ aienvmp plan # read-only AI action plan, no automatic fixes
61
+ aienvmp handoff # next-agent handoff summary + recommended actions
60
62
  aienvmp intent # record a planned env change
61
63
  aienvmp record # record what changed
62
64
  aienvmp doctor --ci # strict CI check
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -11,6 +11,7 @@ import { resolveWorkspace } from "./commands/resolve.js";
11
11
  import { syncWorkspace } from "./commands/sync.js";
12
12
  import { snippetWorkspace } from "./commands/snippet.js";
13
13
  import { handoffWorkspace } from "./commands/handoff.js";
14
+ import { planWorkspace } from "./commands/plan.js";
14
15
  import { readFileSync } from "node:fs";
15
16
 
16
17
  const commands = new Map([
@@ -26,7 +27,8 @@ const commands = new Map([
26
27
  ["resolve", resolveWorkspace],
27
28
  ["sync", syncWorkspace],
28
29
  ["snippet", snippetWorkspace],
29
- ["handoff", handoffWorkspace]
30
+ ["handoff", handoffWorkspace],
31
+ ["plan", planWorkspace]
30
32
  ]);
31
33
 
32
34
  const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
@@ -77,11 +79,13 @@ Usage:
77
79
  aienvmp sync [--dir .] [--json] [--quiet] [--deep] [--security]
78
80
  aienvmp context [--dir .] [--json]
79
81
  aienvmp handoff [--dir .] [--json] [--record --actor agent:id]
82
+ aienvmp plan [--dir .] [--json] [--write]
80
83
 
81
84
  Common:
82
85
  aienvmp sync update AIENV.md, manifest, ledger, intents, and dashboard
83
86
  aienvmp context print the AI preflight brief
84
87
  aienvmp handoff print the next-agent handoff summary
88
+ aienvmp plan print a read-only AI environment action plan
85
89
  aienvmp snippet print an AGENTS.md pointer snippet
86
90
  aienvmp dash regenerate/open the lightweight dashboard
87
91
 
@@ -7,6 +7,7 @@ import { openIntents, readJsonl, readTimeline } from "../timeline.js";
7
7
  import { dashboardPath, intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
8
8
  import { renderDashboard } from "../render.js";
9
9
  import { loadPolicy, policyWarnings } from "../policy.js";
10
+ import { recommendedActions } from "../actions.js";
10
11
 
11
12
  export async function dashWorkspace(args) {
12
13
  const dir = workspaceDir(args);
@@ -16,7 +17,7 @@ export async function dashWorkspace(args) {
16
17
  const intents = openIntents(await readJsonl(intentsPath(dir)));
17
18
  const policy = await loadPolicy(dir);
18
19
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
19
- const html = renderDashboard(manifest, timeline, warnings, intents, policy);
20
+ const html = renderDashboard({ ...manifest, recommendedActions: recommendedActions(manifest, { warnings, intents }) }, timeline, warnings, intents, policy);
20
21
  const out = dashboardPath(dir);
21
22
  await fs.mkdir(path.dirname(out), { recursive: true });
22
23
  await fs.writeFile(out, html, "utf8");
@@ -5,6 +5,7 @@ import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.
5
5
  import { renderHandoff } from "../render.js";
6
6
  import { openIntents, readJsonl, readTimeline } from "../timeline.js";
7
7
  import { changedTrust } from "../trust.js";
8
+ import { recommendedActions } from "../actions.js";
8
9
 
9
10
  export async function handoffWorkspace(args) {
10
11
  const dir = workspaceDir(args);
@@ -42,6 +43,7 @@ async function recordHandoff(file, handoff, actor) {
42
43
 
43
44
  export function buildHandoff(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
44
45
  const reviewRequired = warnings.length > 0 || intents.length > 0;
46
+ const actions = recommendedActions(manifest, { warnings, intents });
45
47
  return {
46
48
  status: reviewRequired ? "review-required" : "clear",
47
49
  trust: manifest.trust || {},
@@ -63,6 +65,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
63
65
  },
64
66
  openIntents: intents.slice(-5).reverse(),
65
67
  warnings,
68
+ recommendedActions: actions,
66
69
  recentChanges: timeline.slice(-5).reverse(),
67
70
  mustNotDo: [
68
71
  "do not change global runtimes without user approval",
@@ -0,0 +1,77 @@
1
+ import fs from "node:fs/promises";
2
+ import { diagnose } from "../doctor.js";
3
+ import { readJson, writeJson } from "../fsutil.js";
4
+ import { loadPolicy, policyWarnings } from "../policy.js";
5
+ import { intentsPath, manifestPath, planJsonPath, planMdPath, timelinePath, workspaceDir } from "../paths.js";
6
+ import { renderPlan } from "../render.js";
7
+ import { recommendedActions } from "../actions.js";
8
+ import { openIntents, readJsonl, readTimeline } from "../timeline.js";
9
+
10
+ export async function planWorkspace(args) {
11
+ const dir = workspaceDir(args);
12
+ const manifest = await readJson(manifestPath(dir));
13
+ if (!manifest) throw new Error("missing manifest; run `aienvmp sync` first");
14
+ const timeline = await readTimeline(timelinePath(dir));
15
+ const intents = openIntents(await readJsonl(intentsPath(dir)));
16
+ const policy = await loadPolicy(dir);
17
+ const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
18
+ const plan = buildPlan(manifest, warnings, intents, policy);
19
+
20
+ if (args.write) {
21
+ await writeJson(planJsonPath(dir), plan);
22
+ await fs.writeFile(planMdPath(dir), renderPlan(plan), "utf8");
23
+ }
24
+
25
+ if (args.json) {
26
+ console.log(JSON.stringify(plan, null, 2));
27
+ } else {
28
+ console.log(renderPlan(plan));
29
+ }
30
+ return plan;
31
+ }
32
+
33
+ export function buildPlan(manifest, warnings = [], intents = [], policy = {}) {
34
+ const actions = recommendedActions(manifest, { warnings, intents });
35
+ const status = warnings.length || intents.length ? "review-required" : "clear";
36
+ return {
37
+ schemaVersion: 1,
38
+ generatedAt: new Date().toISOString(),
39
+ status,
40
+ workspace: manifest.workspace || {},
41
+ trust: manifest.trust || {},
42
+ policy: {
43
+ node: policy.node || "not set",
44
+ python: policy.python || "not set",
45
+ packageManager: policy.packageManager || "not set",
46
+ runtimeChanges: policy.runtimeChanges || "ask-first",
47
+ globalInstalls: policy.globalInstalls || "ask-first"
48
+ },
49
+ recommendedActions: actions,
50
+ reviewGates: reviewGates(warnings, intents, manifest.security),
51
+ warnings,
52
+ openIntents: intents.slice(-5).reverse(),
53
+ security: {
54
+ mode: manifest.security?.mode || "basic",
55
+ enabled: manifest.security?.enabled === true,
56
+ summary: manifest.security?.summary || { total: 0, critical: 0, high: 0, moderate: 0, low: 0, info: 0 },
57
+ topPackages: manifest.security?.topPackages || []
58
+ },
59
+ notes: [
60
+ "This plan is read-only.",
61
+ "Ask the user before global runtime, package manager, Docker, or global package changes.",
62
+ "Prefer project-local version files and local environments."
63
+ ]
64
+ };
65
+ }
66
+
67
+ function reviewGates(warnings, intents, security = {}) {
68
+ const gates = [];
69
+ if (warnings.length) gates.push("Review warnings before changing the environment.");
70
+ if (intents.length) gates.push("Resolve or coordinate open intents before overlapping environment changes.");
71
+ const summary = security.summary || {};
72
+ if (security.enabled && (Number(summary.critical || 0) > 0 || Number(summary.high || 0) > 0)) {
73
+ gates.push("Review high or critical vulnerability remediation before dependency or deployment changes.");
74
+ }
75
+ if (!gates.length) gates.push("No blocking gates detected; continue project-local work and record intent before environment changes.");
76
+ return gates;
77
+ }
package/src/paths.js CHANGED
@@ -28,6 +28,14 @@ export function dashboardPath(dir) {
28
28
  return path.join(stateDir(dir), "dashboard.html");
29
29
  }
30
30
 
31
+ export function planJsonPath(dir) {
32
+ return path.join(stateDir(dir), "plan.json");
33
+ }
34
+
35
+ export function planMdPath(dir) {
36
+ return path.join(stateDir(dir), "plan.md");
37
+ }
38
+
31
39
  export function aiEnvPath(dir) {
32
40
  return path.join(dir, "AIENV.md");
33
41
  }
package/src/render.js CHANGED
@@ -149,6 +149,9 @@ export function renderHandoff(handoff) {
149
149
  "Warnings:",
150
150
  ...(handoff.warnings.length ? handoff.warnings.map((w) => `- ${w.message}`) : ["- none"]),
151
151
  "",
152
+ "Recommended actions:",
153
+ ...(handoff.recommendedActions?.length ? handoff.recommendedActions.map((item) => `- [${item.priority}] ${item.summary}${item.command ? ` (${item.command})` : ""}`) : ["- none"]),
154
+ "",
152
155
  "Recent changes:",
153
156
  ...(handoff.recentChanges.length ? handoff.recentChanges.map((t) => `- ${formatTimeline(t)}`) : ["- none"]),
154
157
  "",
@@ -161,6 +164,31 @@ export function renderHandoff(handoff) {
161
164
  return lines.join("\n");
162
165
  }
163
166
 
167
+ export function renderPlan(plan) {
168
+ const lines = [
169
+ "# AI Environment Plan",
170
+ "",
171
+ `Status: ${plan.status}`,
172
+ `Generated: ${plan.generatedAt}`,
173
+ `Workspace: ${plan.workspace?.path || "unknown"}`,
174
+ "",
175
+ "Purpose: read-only review plan for AI agents and humans. It does not install, remove, upgrade, downgrade, or lock anything.",
176
+ "",
177
+ "Recommended actions:",
178
+ ...(plan.recommendedActions.length
179
+ ? plan.recommendedActions.map((item) => `- [${item.priority}] ${item.category}: ${item.summary}${item.command ? ` (${item.command})` : ""}`)
180
+ : ["- none"]),
181
+ "",
182
+ "Review gates:",
183
+ ...plan.reviewGates.map((item) => `- ${item}`),
184
+ "",
185
+ "Warnings:",
186
+ ...(plan.warnings.length ? plan.warnings.map((warning) => `- [${warning.code}] ${warning.message}`) : ["- none"]),
187
+ ""
188
+ ];
189
+ return lines.join("\n");
190
+ }
191
+
164
192
  export function renderDashboard(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
165
193
  const data = JSON.stringify({ manifest, timeline, warnings, intents, policy });
166
194
  return `<!doctype html>
@@ -234,6 +262,8 @@ const warnHtml=warnings.length?'<div class="warnings">'+warnings.map(w=>\`<div c
234
262
  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>';
235
263
  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>';
236
264
  const policyHtml=entries(policy).length?\`<table>\${rows(policy)}</table>\`:'<div class="okline">No explicit version policy set.</div>';
265
+ const actions=manifest.recommendedActions||[];
266
+ const actionsHtml=actions.length?'<div class="timeline">'+actions.slice(0,6).map(a=>\`<div class="event"><time>\${esc(a.priority)}</time><div><b>\${esc(a.category)}</b> \${esc(a.summary)}\${a.command?\`<div class="path">\${esc(a.command)}</div>\`:''}</div></div>\`).join('')+'</div>':'<div class="okline">No recommended actions. Continue project-local work.</div>';
237
267
  const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
238
268
  const reviewRequired=warnings.length>0||intents.length>0;
239
269
  const recentChanges=timeline.slice(-8).length;
@@ -273,6 +303,8 @@ document.getElementById('app').innerHTML=\`
273
303
  \${card('Security Summary',sec.enabled?'<span class="pill warn">security</span>':'<span class="pill off">basic</span>',securityHtml)}
274
304
  </div>
275
305
  <aside>
306
+ \${card('Recommended Actions','<span class="pill">'+actions.length+' actions</span>',actionsHtml)}
307
+ <div style="height:14px"></div>
276
308
  \${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
277
309
  <div style="height:14px"></div>
278
310
  \${card('Version Policy','<span class="pill">'+entries(policy).length+' rules</span>',policyHtml)}