aienvmp 0.1.11 → 0.1.13

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.13
4
+
5
+ - Added `recommendedActions` to AI handoff output.
6
+ - Added a Recommended Actions dashboard card for human review.
7
+ - Reused the same advisory action engine across context, doctor, handoff, and dashboard.
8
+
9
+ ## 0.1.12
10
+
11
+ - Added AI-readable `recommendedActions` to `context --json` and `doctor --json`.
12
+ - Added concise recommended actions to text context and doctor output.
13
+ - Kept recommendations advisory-only; strict failure still requires `doctor --ci`.
14
+
3
15
  ## 0.1.11
4
16
 
5
17
  - Added bounded remediation details to security summaries, including fix versions and advisory references.
package/README.md CHANGED
@@ -55,8 +55,8 @@ npx aienvmp snippet agents
55
55
  ```bash
56
56
  aienvmp sync # update env map, light SBOM, ledger, dashboard
57
57
  aienvmp context # AI preflight brief
58
- aienvmp context --json # machine-readable AI decision context
59
- aienvmp handoff # next-agent handoff summary
58
+ aienvmp context --json # machine-readable AI decision context + recommended actions
59
+ aienvmp handoff # next-agent handoff summary + recommended actions
60
60
  aienvmp intent # record a planned env change
61
61
  aienvmp record # record what changed
62
62
  aienvmp doctor --ci # strict CI check
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/actions.js ADDED
@@ -0,0 +1,87 @@
1
+ export function recommendedActions(manifest = {}, context = {}) {
2
+ const warnings = context.warnings || [];
3
+ const intents = context.intents || [];
4
+ const actions = [];
5
+
6
+ if (isStaleWarning(warnings)) {
7
+ actions.push(action("sync-snapshot", "high", "sync", "Refresh the environment snapshot before changing runtimes or package managers.", "aienvmp sync"));
8
+ }
9
+
10
+ if (intents.length) {
11
+ actions.push(action("review-open-intents", "high", "coordination", "Review or resolve open environment intents before another agent changes the environment.", "aienvmp context --json"));
12
+ }
13
+
14
+ if (warnings.some((warning) => warning.code === "conflicting-open-intents")) {
15
+ actions.push(action("coordinate-agents", "high", "coordination", "Multiple agents are planning changes to the same environment target. Coordinate with the user before proceeding."));
16
+ }
17
+
18
+ actions.push(...securityActions(manifest.security));
19
+
20
+ if (hasRuntimePolicyWarning(warnings)) {
21
+ actions.push(action("review-version-policy", "medium", "runtime", "Detected runtime or package manager policy drift. Prefer project-local version files and ask before global changes."));
22
+ }
23
+
24
+ if (warnings.some((warning) => warning.code === "handoff-stale")) {
25
+ actions.push(action("record-handoff", "medium", "handoff", "Record an AI handoff after environment changes so the next agent starts with current context.", "aienvmp handoff --record --actor agent:id"));
26
+ }
27
+
28
+ if (!actions.length) {
29
+ actions.push(action("continue-project-local", "low", "workflow", "Continue with project-local work. Record intent before environment changes.", "aienvmp intent --actor agent:id --action planned-change"));
30
+ }
31
+
32
+ return dedupeActions(actions).slice(0, 8);
33
+ }
34
+
35
+ function securityActions(security = {}) {
36
+ if (!security.enabled) return [];
37
+
38
+ const summary = security.summary || {};
39
+ const highRisk = Number(summary.critical || 0) > 0 || Number(summary.high || 0) > 0;
40
+ const packages = (security.topPackages || []).slice(0, 5);
41
+ if (!highRisk && !packages.length) return [];
42
+
43
+ const packageHints = packages.map((pkg) => {
44
+ const fix = pkg.fixVersions?.length ? `fix ${pkg.fixVersions.slice(0, 2).join(", ")}` : pkg.fixAvailable ? "fix available" : "review required";
45
+ return `${pkg.name} (${pkg.severity}, ${fix})`;
46
+ });
47
+
48
+ return [action(
49
+ "review-security-remediation",
50
+ highRisk ? "high" : "medium",
51
+ "security",
52
+ packageHints.length
53
+ ? `Review vulnerable packages before dependency or deployment changes: ${packageHints.join("; ")}.`
54
+ : "Review vulnerability summary before dependency or deployment changes.",
55
+ "aienvmp context --json"
56
+ )];
57
+ }
58
+
59
+ function action(id, priority, category, summary, command = "") {
60
+ return { id, priority, category, summary, command };
61
+ }
62
+
63
+ function dedupeActions(actions) {
64
+ const seen = new Set();
65
+ return actions.filter((item) => {
66
+ if (seen.has(item.id)) return false;
67
+ seen.add(item.id);
68
+ return true;
69
+ });
70
+ }
71
+
72
+ function isStaleWarning(warnings) {
73
+ return warnings.some((warning) => warning.code === "manifest-stale" || warning.code === "stale-open-intent");
74
+ }
75
+
76
+ function hasRuntimePolicyWarning(warnings) {
77
+ return warnings.some((warning) => [
78
+ "node-version-mismatch",
79
+ "python-version-mismatch",
80
+ "mixed-node-lockfiles",
81
+ "python-missing",
82
+ "docker-missing",
83
+ "policy-node-mismatch",
84
+ "policy-python-mismatch",
85
+ "policy-package-manager-mismatch"
86
+ ].includes(warning.code));
87
+ }
@@ -4,6 +4,7 @@ import { openIntents, readJsonl, readTimeline } from "../timeline.js";
4
4
  import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
5
5
  import { renderContext } from "../render.js";
6
6
  import { loadPolicy, policyWarnings } from "../policy.js";
7
+ import { recommendedActions } from "../actions.js";
7
8
 
8
9
  export async function contextWorkspace(args) {
9
10
  const dir = workspaceDir(args);
@@ -14,10 +15,12 @@ export async function contextWorkspace(args) {
14
15
  const policy = await loadPolicy(dir);
15
16
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
16
17
  const decision = contextDecision(warnings, intents);
18
+ const actions = recommendedActions(manifest, { warnings, intents });
17
19
  if (args.json) {
18
20
  console.log(JSON.stringify({
19
21
  status: warnings.length ? "review-required" : "clear",
20
22
  decision,
23
+ recommendedActions: actions,
21
24
  trust: manifest.trust || {},
22
25
  guidance: decision,
23
26
  workspace: manifest.workspace,
@@ -35,7 +38,7 @@ export async function contextWorkspace(args) {
35
38
  }, null, 2));
36
39
  return;
37
40
  }
38
- console.log(renderContext(manifest, timeline, warnings, intents, policy));
41
+ console.log(renderContext(manifest, timeline, warnings, intents, policy, actions));
39
42
  }
40
43
 
41
44
  function securitySummary(security = {}) {
@@ -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");
@@ -3,6 +3,7 @@ import { readJson } from "../fsutil.js";
3
3
  import { openIntents, readJsonl, readTimeline } from "../timeline.js";
4
4
  import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
5
5
  import { loadPolicy, policyWarnings } from "../policy.js";
6
+ import { recommendedActions } from "../actions.js";
6
7
 
7
8
  export async function doctorWorkspace(args) {
8
9
  const dir = workspaceDir(args);
@@ -12,13 +13,15 @@ export async function doctorWorkspace(args) {
12
13
  const timeline = await readTimeline(timelinePath(dir));
13
14
  const intents = openIntents(await readJsonl(intentsPath(dir)));
14
15
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
16
+ const actions = recommendedActions(manifest, { warnings, intents });
15
17
  if (args.json) {
16
18
  console.log(JSON.stringify({
17
19
  status: warnings.length ? "warning" : "ok",
18
20
  trust: manifest.trust || {},
19
21
  policy,
20
22
  openIntentCount: intents.length,
21
- warnings
23
+ warnings,
24
+ recommendedActions: actions
22
25
  }, null, 2));
23
26
  if (args.ci && warnings.length) {
24
27
  process.exitCode = 1;
@@ -32,6 +35,10 @@ export async function doctorWorkspace(args) {
32
35
  for (const warning of warnings) {
33
36
  console.log(`[${warning.code}] ${warning.message}`);
34
37
  }
38
+ console.log("recommended actions:");
39
+ for (const item of actions) {
40
+ console.log(`- [${item.priority}] ${item.summary}${item.command ? ` (${item.command})` : ""}`);
41
+ }
35
42
  console.log("doctor: warnings are non-blocking by default; pass --ci to fail automation.");
36
43
  if (args.ci) {
37
44
  process.exitCode = 1;
@@ -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",
package/src/render.js CHANGED
@@ -84,7 +84,7 @@ Before changing runtimes, package managers, Docker settings, global packages, or
84
84
  \`aienvmp\` does not replace this instruction file. It provides the live env map, lightweight runtime SBOM, intent log, timeline, and dashboard.`;
85
85
  }
86
86
 
87
- export function renderContext(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
87
+ export function renderContext(manifest, timeline = [], warnings = [], intents = [], policy = {}, recommendedActions = []) {
88
88
  const status = warnings.length ? "review-required" : "clear";
89
89
  const next = warnings.length ? "Review warnings before changing the environment." : "Continue with project-local work. Record intent before environment changes.";
90
90
  return [
@@ -115,6 +115,9 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
115
115
  "Warnings:",
116
116
  ...(warnings.length ? warnings.map((w) => `- ${w.message}`) : ["- none"]),
117
117
  "",
118
+ "Recommended actions:",
119
+ ...(recommendedActions.length ? recommendedActions.map((item) => `- [${item.priority}] ${item.summary}${item.command ? ` (${item.command})` : ""}`) : ["- none"]),
120
+ "",
118
121
  "Open intents:",
119
122
  ...(intents.length ? intents.slice(-5).reverse().map((i) => `- ${i.actor}: ${i.action}`) : ["- none"]),
120
123
  "",
@@ -146,6 +149,9 @@ export function renderHandoff(handoff) {
146
149
  "Warnings:",
147
150
  ...(handoff.warnings.length ? handoff.warnings.map((w) => `- ${w.message}`) : ["- none"]),
148
151
  "",
152
+ "Recommended actions:",
153
+ ...(handoff.recommendedActions?.length ? handoff.recommendedActions.map((item) => `- [${item.priority}] ${item.summary}${item.command ? ` (${item.command})` : ""}`) : ["- none"]),
154
+ "",
149
155
  "Recent changes:",
150
156
  ...(handoff.recentChanges.length ? handoff.recentChanges.map((t) => `- ${formatTimeline(t)}`) : ["- none"]),
151
157
  "",
@@ -231,6 +237,8 @@ const warnHtml=warnings.length?'<div class="warnings">'+warnings.map(w=>\`<div c
231
237
  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>';
232
238
  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>';
233
239
  const policyHtml=entries(policy).length?\`<table>\${rows(policy)}</table>\`:'<div class="okline">No explicit version policy set.</div>';
240
+ const actions=manifest.recommendedActions||[];
241
+ 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>';
234
242
  const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
235
243
  const reviewRequired=warnings.length>0||intents.length>0;
236
244
  const recentChanges=timeline.slice(-8).length;
@@ -270,6 +278,8 @@ document.getElementById('app').innerHTML=\`
270
278
  \${card('Security Summary',sec.enabled?'<span class="pill warn">security</span>':'<span class="pill off">basic</span>',securityHtml)}
271
279
  </div>
272
280
  <aside>
281
+ \${card('Recommended Actions','<span class="pill">'+actions.length+' actions</span>',actionsHtml)}
282
+ <div style="height:14px"></div>
273
283
  \${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
274
284
  <div style="height:14px"></div>
275
285
  \${card('Version Policy','<span class="pill">'+entries(policy).length+' rules</span>',policyHtml)}