aienvmp 0.1.36 → 0.1.38

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.38
4
+
5
+ - Added a 10-second AI quickstart flow to the shared preflight contract and status output.
6
+ - Added preflight intent target recommendations so agents can record runtime, package manager, dependency, Docker, or coordination changes consistently.
7
+ - Surfaced AI intent target recommendations in the dashboard so humans can review the same target guidance.
8
+ - Added the same quickstart and intent target guidance to `AIENV.md` so Markdown-first agents receive the current preflight.
9
+ - Aligned AGENTS/Claude/Gemini pointer snippets with the same status-first, target-aware AI flow.
10
+
11
+ ## 0.1.37
12
+
13
+ - Added `lightSbom` to the manifest as an AI-ready package and vulnerability summary.
14
+ - Linked dependency manifests, ecosystem/group counts, vulnerable direct dependencies, and top risk packages into one compact SBOM view.
15
+ - Surfaced dependency change hints in AI-facing outputs and the dashboard so agents and humans can identify relevant manifests before edits.
16
+ - Added read-only lockfile awareness to dependency snapshots and light SBOM hints.
17
+ - Added package manager policy hints from lockfiles to reduce accidental npm/pnpm/yarn drift.
18
+
3
19
  ## 0.1.36
4
20
 
5
21
  - Added an explicit enforcement profile to the shared AI preflight contract.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  It helps Codex, Claude, Gemini, and humans avoid silent Node, Python, package manager, dependency, Docker, and security drift.
12
12
 
13
- Core loop: scan once, link runtime/dependency/security context, give AI a shared decision contract with advisory priorities, and hand off safe next steps.
13
+ Core loop: scan once, link runtime/dependency/security context, give AI a shared decision contract with a light SBOM summary, and hand off safe next steps.
14
14
 
15
15
  ## Quick Start
16
16
 
@@ -22,6 +22,8 @@ npx aienvmp plan
22
22
  npx aienvmp handoff
23
23
  ```
24
24
 
25
+ 10-second AI flow: `aienvmp status --write` -> `aienvmp context --json` -> intent before environment changes.
26
+
25
27
  Optional deeper read-only checks:
26
28
 
27
29
  ```bash
@@ -53,9 +55,13 @@ AIENV.md
53
55
  .aienvmp/dashboard.html # includes dependencies, plan, remediation, and environment cards
54
56
  ```
55
57
 
58
+ `AIENV.md` includes the 10-second AI flow and recommended intent targets for Markdown-first agents.
59
+
56
60
  Trust states are machine-readable: `observed`, `planned`, `changed`, `review`, `verified`, `stale`.
57
61
  `status.json` also lists AI read order, artifact paths, and safe commands.
58
62
  `status`, `context`, `plan`, and `handoff` share the same AI preflight contract.
63
+ Preflight also recommends the intent target, so agents do not guess between runtime, package manager, dependency, Docker, or coordination changes.
64
+ The dashboard shows the same intent target guidance for human review.
59
65
 
60
66
  AI agents can observe, plan, and record. Only a human or CI should mark environment facts as verified.
61
67
 
@@ -108,6 +114,11 @@ aienvmp doctor --strict security # fail only scoped warnings
108
114
  - dashboard and preflight explain advisory default vs optional strict mode
109
115
  - non-blocking unless strict mode is explicitly requested
110
116
  - security checks are opt-in and read-only
117
+ - light SBOM is generated from project files and optional scanner summaries
118
+ - dependency change hints point AI agents to the relevant manifest before edits
119
+ - lockfiles are detected read-only and shown before dependency edits
120
+ - package manager policy is inferred from lockfiles to avoid accidental npm/pnpm/yarn drift
121
+ - dashboard mirrors AI-facing SBOM hints so humans can review the same dependency context
111
122
 
112
123
  ## Development
113
124
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,6 +5,7 @@ import { openIntents, readJsonl, readTimeline } from "../timeline.js";
5
5
  import { aiEnvPath, intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
6
6
  import { renderAIEnv } from "../render.js";
7
7
  import { loadPolicy, policyWarnings } from "../policy.js";
8
+ import { buildPreflight } from "../preflight.js";
8
9
 
9
10
  export async function compileWorkspace(args) {
10
11
  const dir = workspaceDir(args);
@@ -14,7 +15,10 @@ export async function compileWorkspace(args) {
14
15
  const intents = openIntents(await readJsonl(intentsPath(dir)));
15
16
  const policy = await loadPolicy(dir);
16
17
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
17
- const rendered = renderAIEnv(manifest, timeline, warnings, intents, policy);
18
+ const rendered = renderAIEnv({
19
+ ...manifest,
20
+ preflight: buildPreflight(manifest, warnings, intents)
21
+ }, timeline, warnings, intents, policy);
18
22
  await fs.writeFile(aiEnvPath(dir), rendered, "utf8");
19
23
  if (!args.quiet) {
20
24
  console.log(`compiled ${aiEnvPath(dir)}`);
@@ -38,6 +38,7 @@ export async function contextWorkspace(args) {
38
38
  containers: manifest.containers,
39
39
  inventory: inventorySummary(manifest.inventory),
40
40
  dependencySnapshot: dependencySummary(manifest.dependencySnapshot),
41
+ lightSbom: lightSbomSummary(manifest.lightSbom),
41
42
  security: securitySummary(manifest.security),
42
43
  projectHints: manifest.projectHints,
43
44
  warnings,
@@ -51,6 +52,31 @@ export async function contextWorkspace(args) {
51
52
  console.log(renderContext(manifest, timeline, warnings, intents, policy, actions));
52
53
  }
53
54
 
55
+ function lightSbomSummary(lightSbom = {}) {
56
+ return {
57
+ mode: lightSbom.mode || "light-sbom",
58
+ summary: lightSbom.summary || {
59
+ ecosystems: {},
60
+ managers: {},
61
+ groups: {},
62
+ manifests: [],
63
+ lockfiles: [],
64
+ packages: 0,
65
+ vulnerabilities: 0,
66
+ directVulnerablePackages: 0,
67
+ transitiveOrUnmatchedVulnerablePackages: 0
68
+ },
69
+ topRisk: (lightSbom.topRisk || []).slice(0, 8),
70
+ packageManagerPolicy: lightSbom.packageManagerPolicy || {
71
+ status: "no-lockfile",
72
+ ecosystems: {},
73
+ guidance: "No lockfile policy detected."
74
+ },
75
+ dependencyChangeHints: (lightSbom.dependencyChangeHints || []).slice(0, 8),
76
+ aiUse: lightSbom.aiUse || {}
77
+ };
78
+ }
79
+
54
80
  function dependencySummary(snapshot = {}) {
55
81
  return {
56
82
  mode: snapshot.mode || "snapshot",
@@ -23,6 +23,8 @@ export async function statusWorkspace(args) {
23
23
  } else if (!args.quiet) {
24
24
  console.log(`${output.state}: ${output.summary}`);
25
25
  console.log(`next: ${output.nextCommand}`);
26
+ console.log(`ai: ${output.quickstart.readFirst} -> ${output.quickstart.detailCommand}`);
27
+ console.log(`intent: ${output.intentTargets[0]?.command || output.commands.recordIntent}`);
26
28
  console.log(`strict: ${output.enforcement.recommendedCommand}`);
27
29
  if (artifact) console.log(`status: ${artifact}`);
28
30
  }
@@ -3,20 +3,33 @@ import path from "node:path";
3
3
  import { exists, readJson } from "./fsutil.js";
4
4
 
5
5
  const NODE_GROUPS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
6
+ const LOCKFILE_CANDIDATES = [
7
+ { file: "package-lock.json", ecosystem: "npm", manager: "npm" },
8
+ { file: "npm-shrinkwrap.json", ecosystem: "npm", manager: "npm" },
9
+ { file: "pnpm-lock.yaml", ecosystem: "npm", manager: "pnpm" },
10
+ { file: "yarn.lock", ecosystem: "npm", manager: "yarn" },
11
+ { file: "bun.lockb", ecosystem: "npm", manager: "bun" },
12
+ { file: "uv.lock", ecosystem: "python", manager: "uv" },
13
+ { file: "poetry.lock", ecosystem: "python", manager: "poetry" },
14
+ { file: "Pipfile.lock", ecosystem: "python", manager: "pipenv" }
15
+ ];
6
16
 
7
17
  export async function scanDependencySnapshot(dir) {
8
18
  const node = await scanNodeDependencies(dir);
9
19
  const python = await scanPythonDependencies(dir);
10
20
  const packages = [...node.packages, ...python.packages];
11
21
  const manifests = [...node.manifests, ...python.manifests];
22
+ const lockfiles = await scanDependencyLockfiles(dir);
12
23
  return {
13
24
  mode: "snapshot",
14
25
  enabled: true,
15
26
  note: "Read-only dependency snapshot from project files. It does not install, update, or resolve packages.",
16
27
  manifests,
28
+ lockfiles,
17
29
  summary: {
18
30
  ecosystems: [...new Set(packages.map((pkg) => pkg.ecosystem))],
19
31
  manifests: manifests.length,
32
+ lockfiles: lockfiles.length,
20
33
  packages: packages.length
21
34
  },
22
35
  packages: packages.slice(0, 80)
@@ -45,6 +58,144 @@ export function linkVulnerableDependencies(security = {}, snapshot = {}) {
45
58
  };
46
59
  }
47
60
 
61
+ export function buildLightSbom(snapshot = {}, security = {}) {
62
+ const packages = snapshot.packages || [];
63
+ const vulnerable = security.topPackages || [];
64
+ const directVulnerable = vulnerable.filter((pkg) => pkg.directDependency === true);
65
+ const transitiveOrUnmatched = vulnerable.filter((pkg) => pkg.directDependency !== true);
66
+ const topRisk = vulnerable.slice(0, 8).map((pkg) => ({
67
+ name: pkg.name,
68
+ ecosystem: scannerEcosystem(pkg.scanner),
69
+ severity: pkg.severity || "unknown",
70
+ directDependency: pkg.directDependency === true,
71
+ manifest: pkg.dependency?.manifest || "",
72
+ version: pkg.dependency?.version || pkg.version || "",
73
+ priority: pkg.remediationPriority?.level || "low",
74
+ score: pkg.remediationPriority?.score || 0,
75
+ fixAvailable: pkg.fixAvailable === true || Boolean(pkg.fixVersions?.length),
76
+ fixVersions: (pkg.fixVersions || []).slice(0, 3)
77
+ }));
78
+ return {
79
+ schemaVersion: 1,
80
+ mode: "light-sbom",
81
+ note: "AI-ready package and vulnerability summary from read-only project files and optional scanners.",
82
+ summary: {
83
+ ecosystems: countBy(packages, "ecosystem"),
84
+ managers: countBy(packages, "manager"),
85
+ groups: countBy(packages, "group"),
86
+ manifests: snapshot.manifests || [],
87
+ lockfiles: snapshot.lockfiles || [],
88
+ packages: packages.length,
89
+ vulnerabilities: Number(security.summary?.total || 0),
90
+ directVulnerablePackages: directVulnerable.length,
91
+ transitiveOrUnmatchedVulnerablePackages: transitiveOrUnmatched.length
92
+ },
93
+ topRisk,
94
+ packageManagerPolicy: packageManagerPolicy(snapshot.lockfiles || []),
95
+ dependencyChangeHints: dependencyChangeHints(packages, topRisk, snapshot.lockfiles || []),
96
+ aiUse: {
97
+ beforeDependencyChanges: "Read lightSbom.summary and lightSbom.topRisk before changing dependencies.",
98
+ securityMode: security.enabled ? "scanner-summary" : "scanner-off",
99
+ dependencySource: "project manifests only; no install or resolver is run"
100
+ }
101
+ };
102
+ }
103
+
104
+ function packageManagerPolicy(lockfiles = []) {
105
+ const byEcosystem = {};
106
+ for (const lockfile of lockfiles) {
107
+ const ecosystem = lockfile.ecosystem || "unknown";
108
+ const managers = byEcosystem[ecosystem]?.managers || new Set();
109
+ managers.add(lockfile.manager || "unknown");
110
+ byEcosystem[ecosystem] = {
111
+ ecosystem,
112
+ managers,
113
+ lockfiles: [...(byEcosystem[ecosystem]?.lockfiles || []), lockfile.file]
114
+ };
115
+ }
116
+ const ecosystems = Object.fromEntries(Object.entries(byEcosystem).map(([name, value]) => {
117
+ const managers = [...value.managers].sort();
118
+ return [name, {
119
+ managers,
120
+ lockfiles: value.lockfiles.sort(),
121
+ status: managers.length > 1 ? "mixed-lockfiles" : "single-manager",
122
+ recommendedManager: managers[0] || "not-detected",
123
+ guidance: managers.length > 1
124
+ ? "Review with the user before dependency changes; multiple package manager lockfiles are present."
125
+ : "Use the detected package manager for dependency changes unless the user says otherwise."
126
+ }];
127
+ }));
128
+ return {
129
+ status: Object.keys(ecosystems).length === 0
130
+ ? "no-lockfile"
131
+ : Object.values(ecosystems).some((item) => item.status === "mixed-lockfiles") ? "review-required" : "clear",
132
+ ecosystems,
133
+ guidance: Object.keys(ecosystems).length === 0
134
+ ? "No lockfile detected; avoid creating one with an unexpected package manager without user approval."
135
+ : "Preserve existing lockfile and package manager choices during dependency changes."
136
+ };
137
+ }
138
+
139
+ function dependencyChangeHints(packages = [], topRisk = [], lockfiles = []) {
140
+ const byManifest = new Map();
141
+ for (const pkg of packages) {
142
+ const key = pkg.manifest || "unknown";
143
+ const entry = byManifest.get(key) || {
144
+ manifest: key,
145
+ ecosystem: pkg.ecosystem || "unknown",
146
+ manager: pkg.manager || "unknown",
147
+ groups: new Set(),
148
+ packages: 0,
149
+ riskPackages: []
150
+ };
151
+ entry.groups.add(pkg.group || "unknown");
152
+ entry.packages += 1;
153
+ byManifest.set(key, entry);
154
+ }
155
+ for (const risk of topRisk) {
156
+ if (!risk.manifest || !byManifest.has(risk.manifest)) continue;
157
+ byManifest.get(risk.manifest).riskPackages.push({
158
+ name: risk.name,
159
+ severity: risk.severity,
160
+ priority: risk.priority,
161
+ fixAvailable: risk.fixAvailable
162
+ });
163
+ }
164
+ return [...byManifest.values()].map((entry) => ({
165
+ manifest: entry.manifest,
166
+ ecosystem: entry.ecosystem,
167
+ manager: entry.manager,
168
+ groups: [...entry.groups].sort(),
169
+ packages: entry.packages,
170
+ riskPackages: entry.riskPackages.slice(0, 5),
171
+ lockfiles: lockfilesForEntry(entry, lockfiles),
172
+ beforeChange: [
173
+ `Read ${entry.manifest} and the active package manager policy before editing dependencies.`,
174
+ lockfilesForEntry(entry, lockfiles).length
175
+ ? `Preserve related lockfiles: ${lockfilesForEntry(entry, lockfiles).map((item) => item.file).join(", ")}.`
176
+ : "No related lockfile was detected; do not create one with a different package manager without approval.",
177
+ "Record an intent before dependency or lockfile changes when another AI may be working."
178
+ ],
179
+ afterChange: [
180
+ "Run project tests or the narrowest relevant validation.",
181
+ "Run aienvmp sync.",
182
+ "Record the dependency change with aienvmp record."
183
+ ]
184
+ }));
185
+ }
186
+
187
+ async function scanDependencyLockfiles(dir) {
188
+ const found = [];
189
+ for (const item of LOCKFILE_CANDIDATES) {
190
+ if (await exists(path.join(dir, item.file))) found.push(item);
191
+ }
192
+ return found;
193
+ }
194
+
195
+ function lockfilesForEntry(entry, lockfiles = []) {
196
+ return lockfiles.filter((lockfile) => lockfile.ecosystem === entry.ecosystem);
197
+ }
198
+
48
199
  export function remediationPriority(pkg = {}, context = {}) {
49
200
  const severityScore = { critical: 90, high: 70, moderate: 45, low: 20, info: 5, unknown: 30 };
50
201
  const severity = String(pkg.severity || "unknown").toLowerCase();
@@ -66,6 +217,14 @@ export function remediationPriority(pkg = {}, context = {}) {
66
217
  return { level, score, reasons };
67
218
  }
68
219
 
220
+ function countBy(items = [], key) {
221
+ return Object.fromEntries(Object.entries(items.reduce((acc, item) => {
222
+ const value = item[key] || "unknown";
223
+ acc[value] = (acc[value] || 0) + 1;
224
+ return acc;
225
+ }, {})).sort(([a], [b]) => a.localeCompare(b)));
226
+ }
227
+
69
228
  async function scanNodeDependencies(dir) {
70
229
  const file = path.join(dir, "package.json");
71
230
  if (!(await exists(file))) return { manifests: [], packages: [] };
package/src/manifest.js CHANGED
@@ -6,12 +6,13 @@ import { exists } from "./fsutil.js";
6
6
  import { observedTrust } from "./trust.js";
7
7
  import { scanGlobalInventory } from "./inventory.js";
8
8
  import { scanSecurity } from "./security.js";
9
- import { linkVulnerableDependencies, scanDependencySnapshot } from "./dependencies.js";
9
+ import { buildLightSbom, linkVulnerableDependencies, scanDependencySnapshot } from "./dependencies.js";
10
10
 
11
11
  export async function buildManifest(dir, options = {}) {
12
12
  const now = new Date().toISOString();
13
13
  const dependencySnapshot = await scanDependencySnapshot(dir);
14
14
  const security = linkVulnerableDependencies(await scanSecurity(dir, { security: options.security }), dependencySnapshot);
15
+ const lightSbom = buildLightSbom(dependencySnapshot, security);
15
16
  const manifest = {
16
17
  schemaName: "aienvmp.runtime-sbom",
17
18
  schemaVersion: 1,
@@ -31,6 +32,7 @@ export async function buildManifest(dir, options = {}) {
31
32
  containers: await scanContainers(),
32
33
  projectHints: await scanProjectHints(dir),
33
34
  dependencySnapshot,
35
+ lightSbom,
34
36
  inventory: await scanGlobalInventory({ deep: options.deep }),
35
37
  security,
36
38
  agentFiles: await scanAgentFiles(dir),
package/src/preflight.js CHANGED
@@ -8,6 +8,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
8
8
  const actions = recommendedActions(manifest, { warnings, intents });
9
9
  const state = decision.reviewRequired ? "review-required" : "clear";
10
10
  const topAction = actions[0] || null;
11
+ const intentTargets = recommendedIntentTargets(manifest, warnings, intents);
11
12
  return {
12
13
  schemaVersion: 1,
13
14
  state,
@@ -43,6 +44,8 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
43
44
  projectLocalWork: decision.canContinueProjectLocalWork ? "allowed" : "review-first",
44
45
  environmentChanges: decision.canChangeEnvironmentWithoutReview ? "allowed" : "intent-and-review-first"
45
46
  },
47
+ quickstart: agentQuickstart(decision.reviewRequired),
48
+ intentTargets,
46
49
  artifacts: preflightArtifacts(),
47
50
  readOrder: [
48
51
  ".aienvmp/status.json",
@@ -58,13 +61,94 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
58
61
  context: "aienvmp context --json",
59
62
  plan: "aienvmp plan --write",
60
63
  handoff: "aienvmp handoff --record --actor agent:id",
61
- recordIntent: "aienvmp intent --actor agent:id --action planned-change"
64
+ recordIntent: intentTargets[0]?.command || "aienvmp intent --actor agent:id --action planned-change"
62
65
  },
63
66
  topAction,
64
67
  nextCommand: topAction?.command || decision.nextCommand
65
68
  };
66
69
  }
67
70
 
71
+ function recommendedIntentTargets(manifest = {}, warnings = [], intents = []) {
72
+ const targets = [];
73
+ for (const warning of warnings) {
74
+ addTarget(targets, targetFromWarning(warning), warning.message || warning.code, warning.code);
75
+ }
76
+ for (const intent of intents) {
77
+ addTarget(targets, intent.target || targetFromText(intent.action), `Open intent from ${intent.actor || "unknown"}.`, "open-intent");
78
+ }
79
+ const pmPolicy = manifest.lightSbom?.packageManagerPolicy;
80
+ if (pmPolicy?.status === "review-required") {
81
+ addTarget(targets, "package-manager", pmPolicy.guidance, "package-manager-policy");
82
+ }
83
+ if (Number(manifest.security?.summary?.total || 0) > 0) {
84
+ addTarget(targets, "dependency", "Security findings are dependency-related; record dependency intent before remediation.", "security");
85
+ }
86
+ if (Number(manifest.dependencySnapshot?.summary?.packages || 0) > 0) {
87
+ addTarget(targets, "dependency", "Dependency manifests detected; use this target before package changes.", "dependency-snapshot");
88
+ }
89
+ if (!targets.length) {
90
+ addTarget(targets, "environment", "Default target when the change affects runtime, dependency, container, or global tool state.", "default");
91
+ }
92
+ return targets.slice(0, 5).map((item) => ({
93
+ ...item,
94
+ command: `aienvmp intent --actor agent:id --action planned-change --target ${item.target}`
95
+ }));
96
+ }
97
+
98
+ function addTarget(targets, target, reason, source) {
99
+ const normalized = normalizeTarget(target);
100
+ if (!normalized) return;
101
+ const existing = targets.find((item) => item.target === normalized);
102
+ if (existing) {
103
+ if (source && !existing.sources.includes(source)) existing.sources.push(source);
104
+ return;
105
+ }
106
+ targets.push({ target: normalized, reason: reason || "Environment change target.", sources: source ? [source] : [] });
107
+ }
108
+
109
+ function targetFromWarning(warning = {}) {
110
+ if (warning.target) return warning.target;
111
+ const code = warning.code || "";
112
+ if (code.includes("node")) return "node";
113
+ if (code.includes("python")) return "python";
114
+ if (code.includes("docker")) return "docker";
115
+ if (code.includes("lockfile") || code.includes("package-manager")) return "package-manager";
116
+ if (code.includes("security")) return "dependency";
117
+ if (code.includes("intent") || code.includes("handoff")) return "coordination";
118
+ return targetFromText(`${warning.message || ""} ${code}`);
119
+ }
120
+
121
+ function targetFromText(text = "") {
122
+ const normalized = String(text).toLowerCase();
123
+ for (const target of ["node", "python", "docker", "package-manager", "dependency", "npm", "pnpm", "yarn", "uv", "pip", "pipx"]) {
124
+ if (normalized.includes(target)) return target;
125
+ }
126
+ if (normalized.includes("package manager") || normalized.includes("lockfile")) return "package-manager";
127
+ if (normalized.includes("vulnerab") || normalized.includes("package")) return "dependency";
128
+ return "";
129
+ }
130
+
131
+ function normalizeTarget(target = "") {
132
+ const normalized = String(target).trim().toLowerCase();
133
+ if (["npm", "pnpm", "yarn"].includes(normalized)) return "package-manager";
134
+ if (["pip", "pipx", "uv"].includes(normalized)) return "python";
135
+ return normalized;
136
+ }
137
+
138
+ function agentQuickstart(reviewRequired) {
139
+ return {
140
+ label: "10-second AI flow",
141
+ readFirst: "aienvmp status --write",
142
+ detailCommand: "aienvmp context --json",
143
+ beforeEnvironmentChange: "aienvmp intent --actor agent:id --action planned-change --target <runtime|package-manager|docker|dependency>",
144
+ afterEnvironmentChange: "aienvmp sync && aienvmp record --actor agent:id --summary what-changed",
145
+ handoff: "aienvmp handoff --record --actor agent:id",
146
+ rule: reviewRequired
147
+ ? "Review warnings or open intents before environment changes; project-local work may continue."
148
+ : "Continue project-local work; record intent before environment changes."
149
+ };
150
+ }
151
+
68
152
  export function preflightArtifacts() {
69
153
  return {
70
154
  status: ".aienvmp/status.json",
package/src/render.js CHANGED
@@ -16,6 +16,7 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
16
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("7. At handoff, run `aienvmp handoff --record --actor agent:id`.", "");
19
+ lines.push(...preflightLines(manifest.preflight), "");
19
20
  lines.push("## Current Policy", "");
20
21
  lines.push(...policyLines(policy));
21
22
  lines.push("- Enforcement: non-blocking by default; warnings require review but do not lock the machine.");
@@ -33,6 +34,8 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
33
34
  lines.push(...inventoryLines(manifest.inventory), "");
34
35
  lines.push("## Dependency Snapshot", "");
35
36
  lines.push(...dependencyLines(manifest.dependencySnapshot), "");
37
+ lines.push("## Light SBOM", "");
38
+ lines.push(...lightSbomLines(manifest.lightSbom), "");
36
39
  lines.push("## Security Summary", "");
37
40
  lines.push(...securityLines(manifest.security), "");
38
41
  lines.push("## Project Requirements And Hints", "");
@@ -67,6 +70,31 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
67
70
  return lines.join("\n");
68
71
  }
69
72
 
73
+ function preflightLines(preflight = {}) {
74
+ const quickstart = preflight.quickstart;
75
+ const targets = preflight.intentTargets || [];
76
+ const lines = ["## 10-Second AI Flow", ""];
77
+ if (quickstart) {
78
+ lines.push(`- Read first: \`${quickstart.readFirst}\``);
79
+ lines.push(`- Detail: \`${quickstart.detailCommand}\``);
80
+ lines.push(`- Before env change: \`${quickstart.beforeEnvironmentChange}\``);
81
+ lines.push(`- After env change: \`${quickstart.afterEnvironmentChange}\``);
82
+ lines.push(`- Handoff: \`${quickstart.handoff}\``);
83
+ lines.push(`- Rule: ${quickstart.rule}`);
84
+ } else {
85
+ lines.push("- Run `aienvmp status --write`, then `aienvmp context --json` before environment changes.");
86
+ }
87
+ lines.push("", "## Recommended Intent Targets", "");
88
+ if (targets.length) {
89
+ for (const target of targets.slice(0, 5)) {
90
+ lines.push(`- ${target.target}: \`${target.command}\` - ${target.reason}`);
91
+ }
92
+ } else {
93
+ lines.push("- environment: `aienvmp intent --actor agent:id --action planned-change --target environment`");
94
+ }
95
+ return lines;
96
+ }
97
+
70
98
  export function renderAgentPointer(target = "agents") {
71
99
  const label = target === "claude" ? "Claude" : target === "gemini" ? "Gemini" : "AI agents";
72
100
  return `## aienvmp Environment Map
@@ -75,12 +103,12 @@ ${label} should use \`aienvmp\` as the workspace environment source of truth.
75
103
 
76
104
  Before changing runtimes, package managers, Docker settings, global packages, or environment policy:
77
105
 
78
- 1. Run \`aienvmp context\`.
79
- 2. Read \`AIENV.md\`.
80
- 3. If the context says \`review-required\`, ask the user before changing the environment.
81
- 4. Record planned environment changes with \`aienvmp intent\`.
82
- 5. After environment changes, run \`aienvmp sync\`.
83
- 6. Record what changed with \`aienvmp record\`.
106
+ 1. Run \`aienvmp status --write\`.
107
+ 2. Run \`aienvmp context --json\` for details.
108
+ 3. Read \`AIENV.md\`.
109
+ 4. If status or context says \`review-required\`, ask the user before changing the environment.
110
+ 5. Record planned environment changes with the recommended target, for example \`aienvmp intent --actor agent:id --action planned-change --target dependency\`.
111
+ 6. After environment changes, run \`aienvmp sync\` and \`aienvmp record --actor agent:id --summary what-changed\`.
84
112
  7. At handoff, run \`aienvmp handoff --record --actor agent:id\`.
85
113
 
86
114
  \`aienvmp\` does not replace this instruction file. It provides the live env map, lightweight runtime SBOM, intent log, timeline, and dashboard.`;
@@ -284,6 +312,14 @@ const deps=manifest.dependencySnapshot||{};
284
312
  const depSummary=deps.summary||{ecosystems:[],manifests:0,packages:0};
285
313
  const depPackages=deps.packages||[];
286
314
  const depHtml=depPackages.length?'<table><tr><th>Packages</th><td><code>'+esc(depSummary.packages||0)+'</code></td></tr><tr><th>Ecosystems</th><td><code>'+esc((depSummary.ecosystems||[]).join(', ')||'none')+'</code></td></tr><tr><th>Manifests</th><td><code>'+esc((deps.manifests||[]).join(', ')||'none')+'</code></td></tr></table><div class="timeline">'+depPackages.slice(0,8).map(p=>\`<div class="event"><time>\${esc(p.ecosystem)}</time><div><b>\${esc(p.name)}</b> <code>\${esc(p.version)}</code><div class="path">\${esc(p.manifest)} / \${esc(p.group)}</div></div></div>\`).join('')+'</div>':'<div class="okline">No project dependency manifests detected.</div>';
315
+ const lightSbom=manifest.lightSbom||{};
316
+ const lightSbomSummary=lightSbom.summary||{};
317
+ const pmPolicy=lightSbom.packageManagerPolicy||{};
318
+ const topRisk=lightSbom.topRisk||[];
319
+ const dependencyHints=lightSbom.dependencyChangeHints||[];
320
+ const dependencyHintsHtml=dependencyHints.length?'<div class="timeline">'+dependencyHints.slice(0,5).map(h=>\`<div class="event"><time>\${esc(h.ecosystem||'deps')}</time><div><b>\${esc(h.manifest)}</b> <code>\${esc(h.manager||'unknown')}</code> \${esc(h.packages||0)} packages\${h.riskPackages?.length?\`<div class="path">risk: \${esc(h.riskPackages.map(p=>p.name).join(', '))}</div>\`:''}<div class="path">\${esc((h.groups||[]).join(', ')||'no groups')}\${h.lockfiles?.length?\` / lockfiles: \${esc(h.lockfiles.map(l=>l.file).join(', '))}\`:''}</div></div></div>\`).join('')+'</div>':'<div class="okline">No dependency change hints available.</div>';
321
+ const pmPolicyHtml='<table><tr><th>Status</th><td><code>'+esc(pmPolicy.status||'no-lockfile')+'</code></td></tr><tr><th>Guidance</th><td>'+esc(pmPolicy.guidance||'No lockfile policy detected.')+'</td></tr></table>';
322
+ const lightSbomHtml=\`<table><tr><th>Packages</th><td><code>\${esc(lightSbomSummary.packages||0)}</code></td></tr><tr><th>Vulnerabilities</th><td><code>\${esc(lightSbomSummary.vulnerabilities||0)}</code></td></tr><tr><th>Direct vulnerable</th><td><code>\${esc(lightSbomSummary.directVulnerablePackages||0)}</code></td></tr><tr><th>Manifests</th><td><code>\${esc((lightSbomSummary.manifests||[]).join(', ')||'none')}</code></td></tr><tr><th>Lockfiles</th><td><code>\${esc((lightSbomSummary.lockfiles||[]).map(l=>l.file).join(', ')||'none')}</code></td></tr></table><h3 style="margin-top:12px">Package manager policy</h3>\${pmPolicyHtml}\${topRisk.length?'<div class="timeline">'+topRisk.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.priority)}</time><div><b>\${esc(p.name)}</b> \${esc(p.severity)} \${p.directDependency?'<code>direct</code>':'<code>transitive</code>'}<div class="path">\${esc(p.manifest||p.ecosystem)} \${esc(p.version||'')}</div></div></div>\`).join('')+'</div>':'<div class="okline">No high-risk package summary in the current light SBOM.</div>'}<h3 style="margin-top:12px">Dependency change hints</h3>\${dependencyHintsHtml}\`;
287
323
  const sec=manifest.security||{};
288
324
  const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
289
325
  const secPackages=sec.topPackages||[];
@@ -316,6 +352,8 @@ const ciReadinessHtml=ciReadiness.length?'<table>'+ciReadiness.map(s=>\`<tr><th>
316
352
  const enforcementProfile=manifest.preflight?.enforcementProfile||{};
317
353
  const strictCommands=enforcementProfile.strictCommands||[];
318
354
  const enforcementHtml=\`<table><tr><th>Default</th><td><code>\${esc(enforcementProfile.defaultMode||'advisory')}</code></td></tr><tr><th>Local</th><td>\${esc(enforcementProfile.localOperation||'non-blocking')}</td></tr><tr><th>Strict</th><td>\${esc(enforcementProfile.strictUse||'CI or explicit checks only')}</td></tr><tr><th>Recommended</th><td><code>\${esc(enforcementProfile.recommendedStrictCommand||'aienvmp doctor --strict all')}</code></td></tr></table><div class="timeline">\${strictCommands.slice(0,4).map(cmd=>\`<div class="event"><time>CI</time><div><code>\${esc(cmd)}</code></div></div>\`).join('')}</div><div class="path">\${esc(enforcementProfile.reason||'Warnings stay advisory unless strict mode is requested.')}</div>\`;
355
+ const intentTargets=manifest.preflight?.intentTargets||[];
356
+ const intentTargetsHtml=intentTargets.length?'<div class="timeline">'+intentTargets.slice(0,5).map(t=>\`<div class="event"><time>\${esc(t.target)}</time><div><b>\${esc(t.target)}</b> \${esc(t.reason||'Record this target before environment changes.')}\${t.command?\`<div class="path">\${esc(t.command)}</div>\`:''}</div></div>\`).join('')+'</div>':'<div class="okline">No specific target recommendation. Use <code>aienvmp intent --actor agent:id --action planned-change</code>.</div>';
319
357
  const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
320
358
  const reviewRequired=warnings.length>0||intents.length>0;
321
359
  const recentChanges=timeline.slice(-8).length;
@@ -353,11 +391,14 @@ document.getElementById('app').innerHTML=\`
353
391
  \${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
354
392
  \${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
355
393
  \${card('Dependency Snapshot','<span class="pill">'+(depSummary.packages||0)+' packages</span>',depHtml)}
394
+ \${card('Light SBOM','<span class="pill">'+(lightSbomSummary.packages||0)+' packages</span>',lightSbomHtml)}
356
395
  \${card('Security Summary',sec.enabled?'<span class="pill warn">security</span>':'<span class="pill off">basic</span>',securityHtml)}
357
396
  </div>
358
397
  <aside>
359
398
  \${card('Recommended Actions','<span class="pill">'+actions.length+' actions</span>',actionsHtml)}
360
399
  <div style="height:14px"></div>
400
+ \${card('AI Intent Targets','<span class="pill">'+intentTargets.length+' targets</span>',intentTargetsHtml)}
401
+ <div style="height:14px"></div>
361
402
  \${card('AI Plan Artifacts',plan.markdown||plan.json?'<span class="pill">written</span>':'<span class="pill off">not written</span>',planHtml)}
362
403
  <div style="height:14px"></div>
363
404
  \${card('Remediation Steps',remediation.length?'<span class="pill warn">'+remediation.length+' items</span>':'<span class="pill off">none</span>',remediationHtml)}
@@ -451,6 +492,39 @@ function dependencyLines(snapshot = {}) {
451
492
  return lines;
452
493
  }
453
494
 
495
+ function lightSbomLines(lightSbom = {}) {
496
+ const summary = lightSbom.summary || {};
497
+ const lines = [
498
+ `- Mode: ${lightSbom.mode || "light-sbom"}`,
499
+ `- Packages: ${summary.packages || 0}`,
500
+ `- Vulnerabilities: ${summary.vulnerabilities || 0}`,
501
+ `- Direct vulnerable packages: ${summary.directVulnerablePackages || 0}`,
502
+ `- Transitive or unmatched vulnerable packages: ${summary.transitiveOrUnmatchedVulnerablePackages || 0}`,
503
+ `- Lockfiles: ${(summary.lockfiles || []).map((item) => item.file).join(", ") || "none"}`
504
+ ];
505
+ if (lightSbom.packageManagerPolicy) {
506
+ lines.push(`- Package manager policy: ${lightSbom.packageManagerPolicy.status}`);
507
+ lines.push(`- Package manager guidance: ${lightSbom.packageManagerPolicy.guidance}`);
508
+ }
509
+ const risks = lightSbom.topRisk || [];
510
+ if (risks.length) {
511
+ lines.push("- Top risk:");
512
+ for (const item of risks.slice(0, 8)) {
513
+ lines.push(` - ${item.name}: ${item.severity}; ${item.priority}/${item.score}; ${item.directDependency ? "direct" : "transitive-or-unmatched"}${item.version ? `; ${item.version}` : ""}`);
514
+ }
515
+ }
516
+ const hints = lightSbom.dependencyChangeHints || [];
517
+ if (hints.length) {
518
+ lines.push("- Dependency change hints:");
519
+ for (const hint of hints.slice(0, 6)) {
520
+ const risk = hint.riskPackages?.length ? `; risk: ${hint.riskPackages.map((pkg) => pkg.name).join(", ")}` : "";
521
+ const lockfiles = hint.lockfiles?.length ? `; lockfiles: ${hint.lockfiles.map((item) => item.file).join(", ")}` : "";
522
+ lines.push(` - ${hint.manifest}: ${hint.ecosystem}/${hint.manager}; ${hint.packages} packages${risk}${lockfiles}`);
523
+ }
524
+ }
525
+ return lines;
526
+ }
527
+
454
528
  function securityLines(security = {}) {
455
529
  if (!security.enabled) return ["- Mode: basic", "- Security scan is disabled. Run `aienvmp sync --security` when vulnerability context is needed."];
456
530
  const summary = security.summary || {};