aienvmp 0.1.44 → 0.1.46

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/BUGFIXES.md CHANGED
@@ -52,6 +52,18 @@ Short record of bugs, fixes, and follow-up checks.
52
52
  - Fix: `doctor`, status/context, handoff, and the dashboard now expose same-target multi-agent records through advisory warnings and `agentActivity`.
53
53
  - Verification: tests cover multi-agent record detection, later handoff reset, status JSON, handoff text, dashboard HTML, and recommended actions.
54
54
 
55
+ ### Post-change loop was too many commands for routine AI handoff
56
+
57
+ - Issue: after an environment change, agents had to remember separate record, sync, status, and handoff commands.
58
+ - Fix: `aienvmp checkpoint` now performs the post-change loop and records an explicit sync ledger entry so follow-ups can be closed.
59
+ - Verification: checkpoint tests cover text/JSON output, status artifact refresh, timeline entries, handoff recording, and cleared follow-ups.
60
+
61
+ ### Light SBOM risk required too much parsing
62
+
63
+ - Issue: AI consumers could see package and vulnerability details, but had to infer risk level and next commands from several nested fields.
64
+ - Fix: `lightSbom.riskSummary` and preflight `sbomRisk` now provide a compact risk score, signals, review targets, and advisory next steps.
65
+ - Verification: tests cover risk scoring, top-risk severity fallback, scanner-off guidance, status/context exposure, dashboard rendering, and recommended actions.
66
+
55
67
  ## Template
56
68
 
57
69
  ### Title
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.46
4
+
5
+ - Added `lightSbom.riskSummary` with compact risk level, score, scanner state, signals, review targets, and next commands.
6
+ - Added `sbomRisk` to the shared AI preflight contract so status/context consumers can read SBOM risk without deep parsing.
7
+ - Surfaced SBOM risk in `context --json`, `AIENV.md`, and the dashboard Light SBOM card.
8
+ - Added recommended actions for read-only security scans and high light-SBOM risk review.
9
+ - Used top-risk package severity as a fallback when scanner summary severity counts are incomplete.
10
+ - Updated dependency change hints to point agents to `aienvmp checkpoint` after accepted dependency changes.
11
+ - Added regression tests for risk scoring, scanner-off guidance, status/context outputs, dashboard rendering, and recommended actions.
12
+
13
+ ## 0.1.45
14
+
15
+ - Added `aienvmp checkpoint` as a one-command post-environment-change flow for record, sync, status, and handoff.
16
+ - Added quiet command support so checkpoint can compose existing commands without noisy intermediate output.
17
+ - Added checkpoint sync ledger entries so follow-up detection can prove the refresh step happened.
18
+ - Added checkpoint guidance to the shared preflight, decision, manifest, dependency protocol, and status outputs.
19
+ - Updated AIENV, handoff, plan, dashboard, and agent pointer rendering to prefer checkpoint after environment changes.
20
+ - Shortened the README environment-change flow to intent plus checkpoint.
21
+ - Added checkpoint regression coverage for JSON output, ledger updates, follow-up closure, status artifacts, and rendered guidance.
22
+
3
23
  ## 0.1.44
4
24
 
5
25
  - Added multi-agent record warnings when multiple agents record environment changes for the same target after the last handoff.
package/README.md CHANGED
@@ -23,10 +23,7 @@ Before environment changes:
23
23
 
24
24
  ```bash
25
25
  npx aienvmp intent --actor agent:codex --action "change dependency" --target dependency
26
- npx aienvmp record --actor agent:codex --summary "dependency-change" --target dependency
27
- npx aienvmp sync
28
- npx aienvmp status --write
29
- npx aienvmp handoff --record --actor agent:codex
26
+ npx aienvmp checkpoint --actor agent:codex --summary "dependency-change" --target dependency
30
27
  ```
31
28
 
32
29
  Use `--dir <workspace>` when AI or CI runs outside the target project.
@@ -50,6 +47,7 @@ AIENV.md # Markdown env map for AI agents
50
47
  - `schema --json` prints the stable AI-readable output contract without scanning.
51
48
  - `status.json.nextAgent` tells the next AI what to read and whether to review first.
52
49
  - `dependencyReadSet` lists manifests and lockfiles before package or security changes.
50
+ - `sbomRisk` gives AI a compact light-SBOM risk level, signals, and next command.
53
51
  - `coordination.conflictTargets` shows where multiple agents are planning changes.
54
52
  - `agentActivity.multiActorTargets` shows where multiple agents actually recorded env changes.
55
53
  - `followUps` shows records that still need `sync`, `status`, or `handoff`.
@@ -79,6 +77,7 @@ aienvmp plan --write # read-only action plan
79
77
  aienvmp handoff --record # next-agent summary
80
78
  aienvmp intent # record planned env change
81
79
  aienvmp record # record what changed
80
+ aienvmp checkpoint # record + sync + status + handoff after env change
82
81
  aienvmp doctor --strict security|policy|coordination|all
83
82
  ```
84
83
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
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 CHANGED
@@ -20,6 +20,7 @@ export function recommendedActions(manifest = {}, context = {}) {
20
20
  }
21
21
 
22
22
  actions.push(...securityActions(manifest.security));
23
+ actions.push(...sbomRiskActions(manifest.lightSbom?.riskSummary));
23
24
 
24
25
  if (hasRuntimePolicyWarning(warnings)) {
25
26
  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."));
@@ -36,6 +37,18 @@ export function recommendedActions(manifest = {}, context = {}) {
36
37
  return dedupeActions(actions).slice(0, 8);
37
38
  }
38
39
 
40
+ function sbomRiskActions(risk = {}) {
41
+ const actions = [];
42
+ if (!risk.level) return actions;
43
+ if (risk.scanner === "off" && risk.vulnerabilityCount === 0) {
44
+ actions.push(action("scan-sbom-risk", "medium", "security", "Run a read-only security scan before dependency or release decisions.", "aienvmp sync --security"));
45
+ }
46
+ if (["urgent", "high"].includes(risk.level)) {
47
+ actions.push(action("review-sbom-risk", "high", "security", `Review light SBOM risk summary before dependency changes: ${(risk.signals || []).slice(0, 2).join("; ") || risk.level}.`, "aienvmp plan --write"));
48
+ }
49
+ return actions;
50
+ }
51
+
39
52
  function securityActions(security = {}) {
40
53
  if (!security.enabled) return [];
41
54
 
package/src/cli.js CHANGED
@@ -14,6 +14,7 @@ import { handoffWorkspace } from "./commands/handoff.js";
14
14
  import { planWorkspace } from "./commands/plan.js";
15
15
  import { statusWorkspace } from "./commands/status.js";
16
16
  import { schemaWorkspace } from "./commands/schema.js";
17
+ import { checkpointWorkspace } from "./commands/checkpoint.js";
17
18
  import { readFileSync } from "node:fs";
18
19
 
19
20
  const commands = new Map([
@@ -32,7 +33,8 @@ const commands = new Map([
32
33
  ["handoff", handoffWorkspace],
33
34
  ["plan", planWorkspace],
34
35
  ["status", statusWorkspace],
35
- ["schema", schemaWorkspace]
36
+ ["schema", schemaWorkspace],
37
+ ["checkpoint", checkpointWorkspace]
36
38
  ]);
37
39
 
38
40
  const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
@@ -108,6 +110,7 @@ Usage:
108
110
  aienvmp context [--dir .] [--json]
109
111
  aienvmp status [--dir .] [--json] [--write] [--quiet]
110
112
  aienvmp handoff [--dir .] [--json] [--record --actor agent:id]
113
+ aienvmp checkpoint [--dir .] --actor agent:id --summary "what changed" [--target dependency] [--json]
111
114
  aienvmp plan [--dir .] [--json] [--write]
112
115
  aienvmp schema [--json]
113
116
 
@@ -116,6 +119,7 @@ Common:
116
119
  aienvmp status print one simple environment decision; --write saves .aienvmp/status.json
117
120
  aienvmp context print the AI preflight brief
118
121
  aienvmp handoff print the next-agent handoff summary
122
+ aienvmp checkpoint record, sync, status, and handoff after an env change
119
123
  aienvmp plan print a read-only AI environment action plan
120
124
  aienvmp schema print the stable AI-readable output contract
121
125
  aienvmp snippet print an AGENTS.md pointer snippet
@@ -127,6 +131,7 @@ Advanced:
127
131
  aienvmp intent [--dir .] --actor agent:codex --action "install pnpm"
128
132
  aienvmp resolve [--dir .] --actor human:you --id <intent-id> [--status resolved|cancelled]
129
133
  aienvmp record [--dir .] --actor agent:codex --summary "updated .nvmrc" [--target node] [--before 20] [--after 24]
134
+ aienvmp checkpoint [--dir .] --actor agent:codex --summary "updated dependency" [--target dependency]
130
135
  aienvmp snippet [agents|claude|gemini] [--write AGENTS.md]
131
136
  aienvmp compile [--dir .]
132
137
  aienvmp diff [--dir .]
@@ -0,0 +1,68 @@
1
+ import { handoffWorkspace } from "./handoff.js";
2
+ import { recordWorkspace } from "./record.js";
3
+ import { statusWorkspace } from "./status.js";
4
+ import { syncWorkspace } from "./sync.js";
5
+ import { appendJsonLine } from "../fsutil.js";
6
+ import { timelinePath, workspaceDir } from "../paths.js";
7
+ import { observedTrust } from "../trust.js";
8
+
9
+ export async function checkpointWorkspace(args) {
10
+ const actor = required(args.actor, "actor");
11
+ const summary = required(args.summary || args.change, "summary");
12
+ const quiet = args.quiet || args.json;
13
+ const base = { ...args, actor, summary, quiet: true };
14
+
15
+ const record = await recordWorkspace(base);
16
+ const sync = await syncWorkspace({ ...base, json: false, quiet: true });
17
+ await recordCheckpointSync(base);
18
+ const handoff = await handoffWorkspace({ ...base, json: false, record: true, quiet: true });
19
+ const status = await statusWorkspace({ ...base, json: false, write: true, quiet: true });
20
+
21
+ const result = {
22
+ status: "ok",
23
+ mode: "post-environment-change",
24
+ actor,
25
+ summary,
26
+ target: args.target || "",
27
+ record,
28
+ outputs: sync.outputs,
29
+ state: status.state,
30
+ warnings: status.counts?.warnings || 0,
31
+ followUps: status.followUps || [],
32
+ agentActivity: status.agentActivity || {},
33
+ handoff: {
34
+ status: handoff.status,
35
+ recommendedNext: handoff.recommendedNext
36
+ },
37
+ next: "Share .aienvmp/status.json or run `aienvmp context --json` before another environment change."
38
+ };
39
+
40
+ if (args.json) {
41
+ console.log(JSON.stringify(result, null, 2));
42
+ } else if (!quiet) {
43
+ console.log(`checkpoint: ${result.state}`);
44
+ console.log(`record: ${record.summary}${record.target ? ` (${record.target})` : ""}`);
45
+ console.log(`status: ${result.outputs.status}`);
46
+ console.log(`handoff: ${handoff.status}`);
47
+ console.log(`next: ${result.next}`);
48
+ }
49
+
50
+ return result;
51
+ }
52
+
53
+ async function recordCheckpointSync(args) {
54
+ const now = new Date();
55
+ await appendJsonLine(timelinePath(workspaceDir(args)), {
56
+ at: now.toISOString(),
57
+ actor: args.actor,
58
+ type: "sync",
59
+ summary: "checkpoint sync",
60
+ target: "",
61
+ trust: observedTrust(now)
62
+ });
63
+ }
64
+
65
+ function required(value, name) {
66
+ if (!value) throw new Error(`checkpoint: --${name} is required`);
67
+ return String(value);
68
+ }
@@ -27,6 +27,7 @@ export async function contextWorkspace(args) {
27
27
  status: warnings.length ? "review-required" : "clear",
28
28
  preflight,
29
29
  coordination: preflight.coordination,
30
+ sbomRisk: preflight.sbomRisk,
30
31
  followUps: preflight.followUps,
31
32
  decision,
32
33
  enforcement: enforcementAdvice(warnings),
@@ -75,6 +76,7 @@ function lightSbomSummary(lightSbom = {}) {
75
76
  guidance: "No lockfile policy detected."
76
77
  },
77
78
  dependencyChangeHints: (lightSbom.dependencyChangeHints || []).slice(0, 8),
79
+ riskSummary: lightSbom.riskSummary || {},
78
80
  source: lightSbom.source || {},
79
81
  confidence: lightSbom.confidence || {},
80
82
  limitations: lightSbom.limitations || [],
@@ -24,7 +24,7 @@ export async function handoffWorkspace(args) {
24
24
 
25
25
  if (args.json) {
26
26
  console.log(JSON.stringify(handoff, null, 2));
27
- } else {
27
+ } else if (!args.quiet) {
28
28
  console.log(renderHandoff(handoff));
29
29
  }
30
30
  return handoff;
@@ -95,6 +95,7 @@ function dependencyHandoffSummary(preflight = {}) {
95
95
  packageManagerPolicy: protocol.packageManagerPolicy || "not-detected",
96
96
  recordIntent: protocol.commands?.recordIntent || "aienvmp intent --actor agent:id --action planned-change --target dependency",
97
97
  recordAfterChange: protocol.commands?.recordAfterChange || "aienvmp record --actor agent:id --summary dependency-change --target dependency",
98
+ checkpointAfterChange: protocol.commands?.checkpointAfterChange || "aienvmp checkpoint --actor agent:id --summary dependency-change --target dependency",
98
99
  handoff: protocol.commands?.handoff || "aienvmp handoff --record --actor agent:id"
99
100
  }
100
101
  };
@@ -103,7 +103,7 @@ function environmentSteps(warnings = []) {
103
103
  }
104
104
 
105
105
  function environmentCategory(code = "") {
106
- if (["conflicting-open-intents", "stale-open-intent", "handoff-stale"].includes(code)) return "coordination";
106
+ if (["conflicting-open-intents", "stale-open-intent", "handoff-stale", "multi-agent-records"].includes(code)) return "coordination";
107
107
  if (code.includes("docker")) return "container";
108
108
  if (code.includes("lockfile") || code.includes("package-manager")) return "package-manager";
109
109
  if (code.includes("node") || code.includes("python") || code.includes("version") || code.includes("runtime")) return "runtime";
@@ -115,13 +115,13 @@ function environmentStepLines(code = "") {
115
115
  "Read .nvmrc and the detected Node version before changing tools.",
116
116
  "Prefer project-local version managers such as nvm, mise, or asdf.",
117
117
  "Ask before changing global Node or npm installations.",
118
- "Run aienvmp sync and record the change after alignment."
118
+ "Run aienvmp checkpoint --actor agent:id --summary what-changed --target node after alignment."
119
119
  ];
120
120
  if (code === "python-version-mismatch") return [
121
121
  "Read .python-version and the detected Python version before changing tools.",
122
122
  "Prefer project-local virtual environments or version managers.",
123
123
  "Ask before changing global Python installations.",
124
- "Run aienvmp sync and record the change after alignment."
124
+ "Run aienvmp checkpoint --actor agent:id --summary what-changed --target python after alignment."
125
125
  ];
126
126
  if (code === "mixed-node-lockfiles" || code === "package-manager-policy-mismatch") return [
127
127
  "Identify the intended package manager from project policy and lockfiles.",
@@ -133,13 +133,13 @@ function environmentStepLines(code = "") {
133
133
  "Confirm whether the Python project is active.",
134
134
  "Prefer project-local Python setup before global installation.",
135
135
  "Ask before installing a global Python runtime.",
136
- "Run aienvmp sync after setup."
136
+ "Run aienvmp checkpoint --actor agent:id --summary what-changed --target python after setup."
137
137
  ];
138
138
  if (code === "docker-missing") return [
139
139
  "Confirm whether Docker is required for the current task.",
140
140
  "Do not change Docker daemon or context without user approval.",
141
141
  "Document fallback commands if Docker is unavailable.",
142
- "Run aienvmp sync after Docker availability changes."
142
+ "Run aienvmp checkpoint --actor agent:id --summary what-changed --target docker after Docker availability changes."
143
143
  ];
144
144
  if (code === "manifest-stale") return [
145
145
  "Run aienvmp sync before environment changes.",
@@ -158,10 +158,14 @@ function environmentStepLines(code = "") {
158
158
  "Run aienvmp handoff --record --actor agent:id before the next AI continues.",
159
159
  "Review recent changes before new environment work."
160
160
  ];
161
+ if (code === "multi-agent-records") return [
162
+ "Review agentActivity and recent timeline records for the shared target.",
163
+ "Run aienvmp checkpoint --actor agent:id --summary what-changed --target environment before more environment work."
164
+ ];
161
165
  return [
162
166
  "Review the warning before changing environment state.",
163
167
  "Ask before global environment changes.",
164
- "Run aienvmp sync after any accepted change."
168
+ "Run aienvmp checkpoint --actor agent:id --summary what-changed --target environment after any accepted change."
165
169
  ];
166
170
  }
167
171
 
@@ -22,7 +22,8 @@ export async function recordWorkspace(args) {
22
22
  trust: changedTrust(now, requiresReview)
23
23
  };
24
24
  await appendJsonLine(timelinePath(dir), entry);
25
- console.log(`recorded ${entry.type} by ${actor}`);
25
+ if (!args.quiet) console.log(`recorded ${entry.type} by ${actor}`);
26
+ return entry;
26
27
  }
27
28
 
28
29
  export function followUpForRecord(record = {}) {
@@ -42,6 +43,7 @@ export function followUpForRecord(record = {}) {
42
43
  ? "Dependency or security records should refresh the env map and handoff context."
43
44
  : "Environment records should refresh the env map and handoff context.",
44
45
  commands: [
46
+ `aienvmp checkpoint --actor agent:id --summary ${isDependency ? "dependency-change" : "what-changed"} --target ${isDependency ? "dependency" : target || "environment"}`,
45
47
  "aienvmp sync",
46
48
  "aienvmp status --write",
47
49
  "aienvmp handoff --record --actor agent:id"
@@ -25,6 +25,7 @@ export async function statusWorkspace(args) {
25
25
  console.log(`next: ${output.nextCommand}`);
26
26
  console.log(`ai: ${output.quickstart.readFirst} -> ${output.quickstart.detailCommand}`);
27
27
  console.log(`intent: ${output.intentTargets[0]?.command || output.commands.recordIntent}`);
28
+ console.log(`checkpoint: ${output.commands.checkpoint}`);
28
29
  console.log(`handoff: ${output.nextAgent.handoffCommand}`);
29
30
  console.log(`strict: ${output.enforcement.recommendedCommand}`);
30
31
  if (artifact) console.log(`status: ${artifact}`);
package/src/contract.js CHANGED
@@ -4,7 +4,7 @@ export function preflightContract() {
4
4
  version: 1,
5
5
  stability: "additive",
6
6
  requiredFields: ["schemaVersion", "state", "decision", "quickstart", "commands", "artifacts"],
7
- aiEntryFields: ["state", "summary", "nextAgent", "coordination", "agentActivity", "followUps", "dependencyReadSet", "dependencyChangeProtocol"],
7
+ aiEntryFields: ["state", "summary", "nextAgent", "coordination", "agentActivity", "sbomRisk", "followUps", "dependencyReadSet", "dependencyChangeProtocol"],
8
8
  rule: "Consumers should ignore unknown fields and treat missing optional fields as unavailable."
9
9
  };
10
10
  }
package/src/decision.js CHANGED
@@ -25,6 +25,7 @@ export function aiDecision(warnings = [], intents = []) {
25
25
  : ["continue with project-local work", "run aienvmp intent before environment changes"],
26
26
  requiredCommands: {
27
27
  beforeEnvironmentChange: "aienvmp intent --actor agent:id --action planned-change --target <runtime|package-manager|docker>",
28
+ checkpointAfterChange: "aienvmp checkpoint --actor agent:id --summary what-changed --target environment",
28
29
  refreshAfterChange: "aienvmp sync",
29
30
  recordAfterChange: "aienvmp record --actor agent:id --summary what-changed",
30
31
  handoff: "aienvmp handoff --record --actor agent:id",
@@ -108,6 +108,7 @@ export function buildLightSbom(snapshot = {}, security = {}) {
108
108
  transitiveOrUnmatchedVulnerablePackages: transitiveOrUnmatched.length
109
109
  },
110
110
  topRisk,
111
+ riskSummary: lightSbomRiskSummary({ packages, security, directVulnerable, transitiveOrUnmatched, topRisk, lockfiles: snapshot.lockfiles || [] }),
111
112
  packageManagerPolicy: packageManagerPolicy(snapshot.lockfiles || []),
112
113
  dependencyChangeHints: dependencyChangeHints(packages, topRisk, snapshot.lockfiles || []),
113
114
  aiUse: {
@@ -119,6 +120,74 @@ export function buildLightSbom(snapshot = {}, security = {}) {
119
120
  };
120
121
  }
121
122
 
123
+ export function lightSbomRiskSummary({ packages = [], security = {}, directVulnerable = [], transitiveOrUnmatched = [], topRisk = [], lockfiles = [] } = {}) {
124
+ const signals = [];
125
+ let score = 0;
126
+ const critical = Math.max(Number(security.summary?.critical || 0), topRisk.filter((pkg) => pkg.severity === "critical").length);
127
+ const high = Math.max(Number(security.summary?.high || 0), topRisk.filter((pkg) => pkg.severity === "high").length);
128
+ const total = Number(security.summary?.total || 0);
129
+ if (critical > 0) {
130
+ score += 45;
131
+ signals.push(`${critical} critical vulnerability finding(s)`);
132
+ }
133
+ if (high > 0) {
134
+ score += 30;
135
+ signals.push(`${high} high vulnerability finding(s)`);
136
+ }
137
+ if (directVulnerable.length) {
138
+ score += 15;
139
+ signals.push(`${directVulnerable.length} vulnerable direct dependency package(s)`);
140
+ }
141
+ if (transitiveOrUnmatched.length) {
142
+ score += 8;
143
+ signals.push(`${transitiveOrUnmatched.length} vulnerable transitive or unmatched package(s)`);
144
+ }
145
+ if (packageManagerPolicy(lockfiles).status === "review-required") {
146
+ score += 10;
147
+ signals.push("mixed package manager lockfiles");
148
+ }
149
+ if (packages.length && security.enabled !== true) {
150
+ score += 5;
151
+ signals.push("security scanner summary is off");
152
+ }
153
+ const level = score >= 90 ? "urgent" : score >= 60 ? "high" : score >= 30 ? "medium" : score > 0 ? "low" : "clear";
154
+ const reviewTargets = [
155
+ ...new Set([
156
+ ...topRisk.map((pkg) => pkg.manifest).filter(Boolean),
157
+ ...topRisk.map((pkg) => pkg.name).filter(Boolean).slice(0, 5)
158
+ ])
159
+ ].slice(0, 8);
160
+ return {
161
+ level,
162
+ score,
163
+ signals,
164
+ scanner: security.enabled ? "enabled" : "off",
165
+ vulnerabilityCount: total,
166
+ directVulnerablePackages: directVulnerable.length,
167
+ reviewTargets,
168
+ next: riskNext(level, security.enabled, topRisk),
169
+ commands: riskCommands(level, security.enabled)
170
+ };
171
+ }
172
+
173
+ function riskNext(level, securityEnabled, topRisk = []) {
174
+ if (!securityEnabled) return "Run read-only security scan before dependency or release decisions.";
175
+ if (level === "urgent" || level === "high") return "Review dependency read set and topRisk before remediation; do not auto-fix without user approval.";
176
+ if (topRisk.length) return "Review topRisk packages before dependency changes.";
177
+ return "No SBOM risk signal requires action in the lightweight snapshot.";
178
+ }
179
+
180
+ function riskCommands(level, securityEnabled) {
181
+ const commands = [];
182
+ if (!securityEnabled) commands.push("aienvmp sync --security");
183
+ if (["urgent", "high", "medium"].includes(level)) {
184
+ commands.push("aienvmp intent --actor agent:id --action dependency-review --target dependency");
185
+ commands.push("aienvmp plan --write");
186
+ }
187
+ commands.push("aienvmp checkpoint --actor agent:id --summary dependency-review --target dependency");
188
+ return commands;
189
+ }
190
+
122
191
  function packageManagerPolicy(lockfiles = []) {
123
192
  const byEcosystem = {};
124
193
  for (const lockfile of lockfiles) {
@@ -196,8 +265,7 @@ function dependencyChangeHints(packages = [], topRisk = [], lockfiles = []) {
196
265
  ],
197
266
  afterChange: [
198
267
  "Run project tests or the narrowest relevant validation.",
199
- "Run aienvmp sync.",
200
- "Record the dependency change with aienvmp record."
268
+ "Run aienvmp checkpoint --actor agent:id --summary dependency-change --target dependency."
201
269
  ]
202
270
  }));
203
271
  }
package/src/manifest.js CHANGED
@@ -42,7 +42,8 @@ export async function buildManifest(dir, options = {}) {
42
42
  handoffCommand: "aienvmp handoff",
43
43
  intentCommand: "aienvmp intent --actor agent:id --action planned-change",
44
44
  recordCommand: "aienvmp record --actor agent:id --summary what-changed",
45
- afterEnvironmentChange: ["aienvmp sync"],
45
+ checkpointCommand: "aienvmp checkpoint --actor agent:id --summary what-changed --target environment",
46
+ afterEnvironmentChange: ["aienvmp checkpoint --actor agent:id --summary what-changed --target environment"],
46
47
  trustModel: {
47
48
  agentWritable: ["observed", "planned", "changed", "review", "stale"],
48
49
  verifiedRequires: "human-or-ci",
package/src/preflight.js CHANGED
@@ -16,6 +16,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = [], timel
16
16
  const coordination = coordinationSummary(intents);
17
17
  const followUps = pendingFollowUps(timeline);
18
18
  const agentActivity = agentActivitySummary(timeline);
19
+ const sbomRisk = manifest.lightSbom?.riskSummary || {};
19
20
  return {
20
21
  schemaVersion: 1,
21
22
  contract: preflightContract(),
@@ -57,6 +58,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = [], timel
57
58
  nextAgent: nextAgentHint(state, dependencyReadSet, dependencyChangeProtocol),
58
59
  coordination,
59
60
  agentActivity,
61
+ sbomRisk,
60
62
  followUps,
61
63
  intentTargets,
62
64
  dependencyReadSet,
@@ -76,6 +78,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = [], timel
76
78
  context: "aienvmp context --json",
77
79
  plan: "aienvmp plan --write",
78
80
  handoff: "aienvmp handoff --record --actor agent:id",
81
+ checkpoint: "aienvmp checkpoint --actor agent:id --summary what-changed --target environment",
79
82
  recordIntent: intentTargets[0]?.command || "aienvmp intent --actor agent:id --action planned-change"
80
83
  },
81
84
  topAction,
@@ -220,6 +223,7 @@ function dependencyProtocol(manifest = {}, dependencyReadSet = []) {
220
223
  recordIntent: "aienvmp intent --actor agent:id --action planned-change --target dependency",
221
224
  refreshAfterChange: "aienvmp sync",
222
225
  recordAfterChange: "aienvmp record --actor agent:id --summary dependency-change --target dependency",
226
+ checkpointAfterChange: "aienvmp checkpoint --actor agent:id --summary dependency-change --target dependency",
223
227
  handoff: "aienvmp handoff --record --actor agent:id"
224
228
  },
225
229
  mustNotDo: [
@@ -335,7 +339,8 @@ function agentQuickstart(reviewRequired) {
335
339
  readFirst: "aienvmp status --write",
336
340
  detailCommand: "aienvmp context --json",
337
341
  beforeEnvironmentChange: "aienvmp intent --actor agent:id --action planned-change --target <runtime|package-manager|docker|dependency>",
338
- afterEnvironmentChange: "aienvmp sync && aienvmp record --actor agent:id --summary what-changed",
342
+ afterEnvironmentChange: "aienvmp checkpoint --actor agent:id --summary what-changed --target environment",
343
+ checkpointAfterChange: "aienvmp checkpoint --actor agent:id --summary what-changed --target environment",
339
344
  handoff: "aienvmp handoff --record --actor agent:id",
340
345
  rule: reviewRequired
341
346
  ? "Review warnings or open intents before environment changes; project-local work may continue."
package/src/render.js CHANGED
@@ -13,9 +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 sync`.");
17
- lines.push("6. Record what changed with `aienvmp record --actor agent:id --summary what-changed`.");
18
- lines.push("7. At handoff, run `aienvmp handoff --record --actor agent:id`.", "");
16
+ lines.push("5. After environment changes, run `aienvmp checkpoint --actor agent:id --summary what-changed --target environment`.", "");
19
17
  lines.push(...preflightLines(manifest.preflight), "");
20
18
  lines.push("## Current Policy", "");
21
19
  lines.push(...policyLines(policy));
@@ -107,7 +105,7 @@ function preflightLines(preflight = {}) {
107
105
  lines.push(`- Mode: ${dependencyProtocol.mode}`);
108
106
  lines.push(`- Package manager policy: ${dependencyProtocol.packageManagerPolicy}`);
109
107
  lines.push(`- Intent: \`${dependencyProtocol.commands.recordIntent}\``);
110
- lines.push(`- After change: \`${dependencyProtocol.commands.refreshAfterChange}\` then \`${dependencyProtocol.commands.recordAfterChange}\``);
108
+ lines.push(`- After change: \`${dependencyProtocol.commands.checkpointAfterChange || dependencyProtocol.commands.recordAfterChange}\``);
111
109
  lines.push(`- Handoff: \`${dependencyProtocol.commands.handoff}\``);
112
110
  for (const item of dependencyProtocol.mustNotDo.slice(0, 3)) lines.push(`- Must not: ${item}`);
113
111
  }
@@ -127,8 +125,7 @@ Before changing runtimes, package managers, Docker settings, global packages, or
127
125
  3. Read \`AIENV.md\`.
128
126
  4. If status or context says \`review-required\`, ask the user before changing the environment.
129
127
  5. Record planned environment changes with the recommended target, for example \`aienvmp intent --actor agent:id --action planned-change --target dependency\`.
130
- 6. After environment changes, run \`aienvmp sync\` and \`aienvmp record --actor agent:id --summary what-changed\`.
131
- 7. At handoff, run \`aienvmp handoff --record --actor agent:id\`.
128
+ 6. After environment changes, run \`aienvmp checkpoint --actor agent:id --summary what-changed --target environment\`.
132
129
 
133
130
  \`aienvmp\` does not replace this instruction file. It provides the live env map, lightweight runtime SBOM, intent log, timeline, and dashboard.`;
134
131
  }
@@ -160,9 +157,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
160
157
  "- Treat policy mismatches as review-required, not as permission to break ongoing operations.",
161
158
  "- Prefer project-local version files and local environments.",
162
159
  "- Before planned env changes, run `aienvmp intent --actor agent:id --action planned-change`.",
163
- "- After env changes, run `aienvmp sync`.",
164
- "- Then run `aienvmp record --actor agent:id --summary what-changed`.",
165
- "- Before handing work to another AI, run `aienvmp handoff --record --actor agent:id`.",
160
+ "- After env changes, run `aienvmp checkpoint --actor agent:id --summary what-changed --target environment`.",
166
161
  "",
167
162
  "Warnings:",
168
163
  ...(warnings.length ? warnings.map((w) => `- ${w.message}`) : ["- none"]),
@@ -254,7 +249,7 @@ function dependencyHandoffLines(dependencyHandoff = {}) {
254
249
  lines.push("- Read: no dependency files detected");
255
250
  }
256
251
  lines.push(`- Intent: ${protocol.recordIntent || "aienvmp intent --actor agent:id --action planned-change --target dependency"}`);
257
- lines.push(`- After change: ${protocol.recordAfterChange || "aienvmp record --actor agent:id --summary dependency-change --target dependency"}`);
252
+ lines.push(`- After change: ${protocol.checkpointAfterChange || protocol.recordAfterChange || "aienvmp checkpoint --actor agent:id --summary dependency-change --target dependency"}`);
258
253
  lines.push(`- Handoff: ${protocol.handoff || "aienvmp handoff --record --actor agent:id"}`);
259
254
  return lines;
260
255
  }
@@ -304,7 +299,7 @@ function dependencyProtocolPlanLines(protocol = {}) {
304
299
  `- Mode: ${protocol.mode || "advisory"}`,
305
300
  `- Package manager policy: ${protocol.packageManagerPolicy || "not-detected"}`,
306
301
  `- Intent: ${protocol.commands.recordIntent}`,
307
- `- After change: ${protocol.commands.refreshAfterChange}; ${protocol.commands.recordAfterChange}`,
302
+ `- After change: ${protocol.commands.checkpointAfterChange || `${protocol.commands.refreshAfterChange}; ${protocol.commands.recordAfterChange}`}`,
308
303
  ...(protocol.mustNotDo || []).slice(0, 3).map((item) => `- Must not: ${item}`)
309
304
  ];
310
305
  }
@@ -421,10 +416,12 @@ const lightSbom=manifest.lightSbom||{};
421
416
  const lightSbomSummary=lightSbom.summary||{};
422
417
  const pmPolicy=lightSbom.packageManagerPolicy||{};
423
418
  const topRisk=lightSbom.topRisk||[];
419
+ const riskSummary=lightSbom.riskSummary||{};
424
420
  const dependencyHints=lightSbom.dependencyChangeHints||[];
425
421
  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>';
426
422
  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>';
427
- 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}\`;
423
+ const riskSummaryHtml=riskSummary.level?\`<table><tr><th>Level</th><td><code>\${esc(riskSummary.level)}</code> \${esc(riskSummary.score||0)}</td></tr><tr><th>Scanner</th><td><code>\${esc(riskSummary.scanner||'unknown')}</code></td></tr><tr><th>Next</th><td>\${esc(riskSummary.next||'No SBOM action required.')}</td></tr><tr><th>Targets</th><td>\${esc((riskSummary.reviewTargets||[]).join(', ')||'none')}</td></tr></table>\${riskSummary.signals?.length?'<div class="timeline">'+riskSummary.signals.slice(0,5).map(s=>\`<div class="event"><time>risk</time><div>\${esc(s)}</div></div>\`).join('')+'</div>':''}\`:'<div class="okline">No risk summary available.</div>';
424
+ 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">Risk summary</h3>\${riskSummaryHtml}<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}\`;
428
425
  const sec=manifest.security||{};
429
426
  const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
430
427
  const secPackages=sec.topPackages||[];
@@ -470,7 +467,7 @@ const activityHtml=activityTargets.length?'<div class="timeline">'+activityTarge
470
467
  const dependencyReadSet=manifest.preflight?.dependencyReadSet||[];
471
468
  const dependencyReadSetHtml=dependencyReadSet.length?'<div class="timeline">'+dependencyReadSet.slice(0,5).map(d=>\`<div class="event"><time>\${esc(d.ecosystem||'deps')}</time><div><b>\${esc(d.manifest||'dependency files')}</b> <code>\${esc(d.manager||'unknown')}</code><div class="path">\${esc([d.manifest,...(d.lockfiles||[])].filter(Boolean).join(', '))}</div>\${d.riskPackages?.length?\`<div class="path">risk: \${esc(d.riskPackages.join(', '))}</div>\`:''}</div></div>\`).join('')+'</div>':'<div class="okline">No dependency files detected.</div>';
472
469
  const dependencyProtocol=manifest.preflight?.dependencyChangeProtocol||{};
473
- const dependencyProtocolHtml=dependencyProtocol.commands?'<table><tr><th>Mode</th><td><code>'+esc(dependencyProtocol.mode||'advisory')+'</code></td></tr><tr><th>Policy</th><td><code>'+esc(dependencyProtocol.packageManagerPolicy||'not-detected')+'</code></td></tr><tr><th>Intent</th><td><code>'+esc(dependencyProtocol.commands.recordIntent)+'</code></td></tr><tr><th>After</th><td><code>'+esc(dependencyProtocol.commands.refreshAfterChange)+'</code> then <code>'+esc(dependencyProtocol.commands.recordAfterChange)+'</code></td></tr></table><div class="timeline">'+(dependencyProtocol.mustNotDo||[]).slice(0,3).map(item=>\`<div class="event"><time>avoid</time><div>\${esc(item)}</div></div>\`).join('')+'</div>':'<div class="okline">No dependency change protocol available.</div>';
470
+ const dependencyProtocolHtml=dependencyProtocol.commands?'<table><tr><th>Mode</th><td><code>'+esc(dependencyProtocol.mode||'advisory')+'</code></td></tr><tr><th>Policy</th><td><code>'+esc(dependencyProtocol.packageManagerPolicy||'not-detected')+'</code></td></tr><tr><th>Intent</th><td><code>'+esc(dependencyProtocol.commands.recordIntent)+'</code></td></tr><tr><th>After</th><td><code>'+esc(dependencyProtocol.commands.checkpointAfterChange||dependencyProtocol.commands.recordAfterChange)+'</code></td></tr></table><div class="timeline">'+(dependencyProtocol.mustNotDo||[]).slice(0,3).map(item=>\`<div class="event"><time>avoid</time><div>\${esc(item)}</div></div>\`).join('')+'</div>':'<div class="okline">No dependency change protocol available.</div>';
474
471
  const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
475
472
  const reviewRequired=warnings.length>0||intents.length>0;
476
473
  const recentChanges=timeline.slice(-8).length;
@@ -638,6 +635,10 @@ function lightSbomLines(lightSbom = {}) {
638
635
  lines.push(`- Source: ${lightSbom.source?.dependencies || "project manifests"}; vulnerabilities: ${lightSbom.source?.vulnerabilities || "not scanned"}`);
639
636
  lines.push(`- Confidence: direct ${lightSbom.confidence?.directDependencies || "unknown"}; transitive ${lightSbom.confidence?.transitiveDependencies || "unknown"}`);
640
637
  }
638
+ if (lightSbom.riskSummary) {
639
+ lines.push(`- Risk summary: ${lightSbom.riskSummary.level || "clear"}/${lightSbom.riskSummary.score || 0}; ${lightSbom.riskSummary.next || "no action"}`);
640
+ if (lightSbom.riskSummary.signals?.length) lines.push(`- Risk signals: ${lightSbom.riskSummary.signals.join("; ")}`);
641
+ }
641
642
  if (lightSbom.packageManagerPolicy) {
642
643
  lines.push(`- Package manager policy: ${lightSbom.packageManagerPolicy.status}`);
643
644
  lines.push(`- Package manager guidance: ${lightSbom.packageManagerPolicy.guidance}`);