aienvmp 0.1.38 → 0.1.40

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.40
4
+
5
+ - Added dependency handoff summaries so the next AI receives dependency read-set and protocol guidance directly in `handoff`.
6
+ - Added a compact `nextAgent` hint to status/preflight JSON for safer AI-to-AI continuation.
7
+ - Added target-level open intent coordination summaries so agents can detect dependency conflicts without parsing logs.
8
+ - Shortened the README around the 10-second AI flow, generated outputs, and advisory contract.
9
+ - Updated the dashboard handoff card with next-agent read hints, dependency files, and conflict targets.
10
+ - Printed the next-agent handoff command in the plain `status` output.
11
+
12
+ ## 0.1.39
13
+
14
+ - Added a dependency read set to preflight, `AIENV.md`, and the dashboard so agents know which manifests and lockfiles to read before package or security changes.
15
+ - Added an advisory dependency change protocol so agents follow the same intent, refresh, record, and handoff flow for package/security edits.
16
+ - Surfaced the dependency change protocol in `plan.md` so human-readable plans match the AI preflight contract.
17
+ - Pointed security remediation recommendations at the dependency intent workflow instead of a generic context read.
18
+ - Treated dependency records and package/security intents as coordination signals for stale handoff and multi-agent conflict warnings.
19
+ - Added CLI regression coverage for `context --dir <workspace> --json` so remote and CI agents can safely inspect another workspace.
20
+ - Allowed `--dir <workspace>` before the command, so AI and CI agents can use either global-style or command-style workspace targeting.
21
+
3
22
  ## 0.1.38
4
23
 
5
24
  - Added a 10-second AI quickstart flow to the shared preflight contract and status output.
package/README.md CHANGED
@@ -6,119 +6,92 @@
6
6
 
7
7
  **AI Environment Map.**
8
8
 
9
- `aienvmp` is an AI-first environment map for shared coding machines.
9
+ `aienvmp` is a lightweight env map and light SBOM for shared AI coding machines.
10
10
 
11
- It helps Codex, Claude, Gemini, and humans avoid silent Node, Python, package manager, dependency, Docker, and security drift.
11
+ It helps Codex, Claude, Gemini, and humans avoid silent runtime, 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 a light SBOM summary, and hand off safe next steps.
14
-
15
- ## Quick Start
13
+ ## 10-Second Use
16
14
 
17
15
  ```bash
18
16
  npx aienvmp sync
19
17
  npx aienvmp status
20
- npx aienvmp context
21
- npx aienvmp plan
22
- npx aienvmp handoff
23
- ```
24
-
25
- 10-second AI flow: `aienvmp status --write` -> `aienvmp context --json` -> intent before environment changes.
26
-
27
- Optional deeper read-only checks:
28
-
29
- ```bash
30
- npx aienvmp sync --deep
31
- npx aienvmp sync --security
18
+ npx aienvmp context --json
32
19
  ```
33
20
 
34
- ## AI Usage
35
-
36
- Tell each agent to read `aienvmp context --json` before environment changes.
21
+ Before environment changes:
37
22
 
38
23
  ```bash
39
- npx aienvmp context --json
40
- npx aienvmp intent --actor agent:codex --action "upgrade node" --target node
41
- npx aienvmp record --actor agent:codex --summary "updated .nvmrc" --target node
24
+ npx aienvmp intent --actor agent:codex --action "change dependency" --target dependency
25
+ npx aienvmp record --actor agent:codex --summary "dependency-change" --target dependency
42
26
  npx aienvmp handoff --record --actor agent:codex
43
27
  ```
44
28
 
45
- ## Output
29
+ Use `--dir <workspace>` when AI or CI runs outside the target project.
30
+
31
+ ## What It Creates
46
32
 
47
33
  ```text
48
- AIENV.md
49
- .aienvmp/manifest.json
50
- .aienvmp/status.json # first file for AI: clear/review, next command, strict advice
51
- .aienvmp/intents.jsonl
52
- .aienvmp/timeline.jsonl
53
- .aienvmp/plan.json
54
- .aienvmp/plan.md
55
- .aienvmp/dashboard.html # includes dependencies, plan, remediation, and environment cards
34
+ AIENV.md # Markdown env map for AI agents
35
+ .aienvmp/status.json # first AI read: clear/review, next command, nextAgent hint
36
+ .aienvmp/manifest.json # runtime map + light SBOM
37
+ .aienvmp/intents.jsonl # planned env changes
38
+ .aienvmp/timeline.jsonl # append-only change ledger
39
+ .aienvmp/plan.md # read-only action plan
40
+ .aienvmp/dashboard.html # human dashboard
56
41
  ```
57
42
 
58
- `AIENV.md` includes the 10-second AI flow and recommended intent targets for Markdown-first agents.
43
+ ## AI Contract
59
44
 
60
- Trust states are machine-readable: `observed`, `planned`, `changed`, `review`, `verified`, `stale`.
61
- `status.json` also lists AI read order, artifact paths, and safe commands.
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.
65
-
66
- AI agents can observe, plan, and record. Only a human or CI should mark environment facts as verified.
45
+ - `status`, `context`, `plan`, and `handoff` share one preflight contract.
46
+ - `status.json.nextAgent` tells the next AI what to read and whether to review first.
47
+ - `dependencyReadSet` lists manifests and lockfiles before package or security changes.
48
+ - `coordination.conflictTargets` shows where multiple agents are planning changes.
49
+ - `handoff` carries dependency read-set and protocol guidance for the next AI.
50
+ - Everything is advisory by default; strict failure is opt-in with `doctor --strict`.
67
51
 
68
52
  ## Agent Files
69
53
 
70
- `aienvmp` does not replace AGENTS.md. It gives AGENTS.md a live environment source of truth.
54
+ `aienvmp` does not replace AGENTS.md, CLAUDE.md, or GEMINI.md. It gives them a live environment source of truth.
71
55
 
72
56
  ```bash
73
57
  npx aienvmp snippet agents
58
+ npx aienvmp snippet claude
59
+ npx aienvmp snippet gemini
74
60
  ```
75
61
 
76
- ## CI Usage
62
+ ## Commands
63
+
64
+ ```bash
65
+ aienvmp sync # update env map, status, ledger, dashboard
66
+ aienvmp status --write # refresh compact AI status
67
+ aienvmp context --json # AI decision contract
68
+ aienvmp plan --write # read-only action plan
69
+ aienvmp handoff --record # next-agent summary
70
+ aienvmp intent # record planned env change
71
+ aienvmp record # record what changed
72
+ aienvmp doctor --strict security|policy|coordination|all
73
+ ```
77
74
 
78
- Use the GitHub Action to write the env map, plan, dashboard, and optional strict checks. See [examples/github-action.yml](examples/github-action.yml).
79
- CI also writes `.aienvmp/status.json` for a compact AI-readable result.
80
- The dashboard shows which strict scopes are CI-ready before you enforce them.
75
+ ## CI
81
76
 
82
77
  ```yaml
83
78
  - uses: soovwv/aienvmp@main
84
79
  with:
85
80
  write-status: "true"
86
81
  write-plan: "true"
87
- strict: "off" # security, policy, coordination, all, or off
82
+ strict: "off"
88
83
  ```
89
84
 
90
- ## Commands
91
-
92
- ```bash
93
- aienvmp sync # update env map, light SBOM, status, ledger, dashboard
94
- aienvmp status --write # refresh .aienvmp/status.json only
95
- aienvmp context # AI preflight brief
96
- aienvmp context --json # AI decision contract + actions + compact step summary
97
- aienvmp plan # read-only AI action plan using the same decision contract
98
- aienvmp handoff # next-agent handoff summary using the same decision contract
99
- aienvmp intent # record a planned env change
100
- aienvmp record # record what changed
101
- aienvmp doctor --ci # strict CI check for all warnings
102
- aienvmp doctor --strict security # fail only scoped warnings
103
- ```
85
+ See [examples/github-action.yml](examples/github-action.yml).
104
86
 
105
87
  ## Principles
106
88
 
107
- - simple by default
108
- - AI-first
109
- - lightweight
110
- - read-only planning, no automatic fixes
111
- - one advisory engine, optional enforcement with `doctor --ci`
112
- - scoped enforcement with `doctor --strict security|policy|coordination|all`
113
- - context and plan expose suggested strict scopes for CI
114
- - dashboard and preflight explain advisory default vs optional strict mode
115
- - non-blocking unless strict mode is explicitly requested
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
89
+ - simple commands
90
+ - AI-first outputs
91
+ - lightweight, read-only scanning
92
+ - light SBOM from project files
93
+ - advisory locally, strict only when requested
94
+ - humans or CI verify; AI agents observe, plan, record, and hand off
122
95
 
123
96
  ## Development
124
97
 
@@ -128,8 +101,6 @@ npm run smoke
128
101
  npm pack --dry-run
129
102
  ```
130
103
 
131
- ## Links
132
-
133
104
  [Roadmap](ROADMAP.md) / [Security](SECURITY.md) / [Troubleshooting](TROUBLESHOOTING.md) / [Bugfix Log](BUGFIXES.md) / [Contributing](CONTRIBUTING.md)
134
105
 
135
106
  Apache-2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
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
@@ -51,9 +51,9 @@ function securityActions(security = {}) {
51
51
  highRisk ? "high" : "medium",
52
52
  "security",
53
53
  packageHints.length
54
- ? `Review vulnerable packages before dependency or deployment changes: ${packageHints.join("; ")}.`
55
- : "Review vulnerability summary before dependency or deployment changes.",
56
- "aienvmp context --json"
54
+ ? `Review dependency read set and protocol before remediation: ${packageHints.join("; ")}.`
55
+ : "Review dependency read set and protocol before vulnerability remediation.",
56
+ "aienvmp intent --actor agent:id --action planned-change --target dependency"
57
57
  )];
58
58
  }
59
59
 
package/src/cli.js CHANGED
@@ -34,9 +34,10 @@ const commands = new Map([
34
34
  ]);
35
35
 
36
36
  const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
37
+ const globalValueOptions = new Set(["--dir"]);
37
38
 
38
39
  export async function main(argv) {
39
- const [command, ...rest] = argv;
40
+ const { command, rest, globalArgs } = splitCommand(argv);
40
41
  if (command === "-v" || command === "--version" || command === "version") {
41
42
  console.log(version);
42
43
  return;
@@ -50,7 +51,30 @@ export async function main(argv) {
50
51
  printUsage();
51
52
  throw new Error(`unknown command "${command}"`);
52
53
  }
53
- await run(parseArgs(rest));
54
+ await run({ ...globalArgs, ...parseArgs(rest) });
55
+ }
56
+
57
+ function splitCommand(argv) {
58
+ const leading = [];
59
+ for (let i = 0; i < argv.length; i++) {
60
+ const arg = argv[i];
61
+ if (!arg.startsWith("--")) {
62
+ return {
63
+ command: arg,
64
+ rest: argv.slice(i + 1),
65
+ globalArgs: parseArgs(leading)
66
+ };
67
+ }
68
+ leading.push(arg);
69
+ if (!arg.includes("=") && globalValueOptions.has(arg) && argv[i + 1] && !argv[i + 1].startsWith("--")) {
70
+ leading.push(argv[++i]);
71
+ }
72
+ }
73
+ return {
74
+ command: argv[0],
75
+ rest: [],
76
+ globalArgs: parseArgs(leading)
77
+ };
54
78
  }
55
79
 
56
80
  export function parseArgs(argv) {
@@ -46,11 +46,12 @@ async function recordHandoff(file, handoff, actor) {
46
46
  export function buildHandoff(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
47
47
  const reviewRequired = warnings.length > 0 || intents.length > 0;
48
48
  const actions = recommendedActions(manifest, { warnings, intents });
49
+ const preflight = buildPreflight(manifest, warnings, intents);
49
50
  return {
50
51
  status: reviewRequired ? "review-required" : "clear",
51
52
  trust: manifest.trust || {},
52
53
  schemaVersion: manifest.schemaVersion || 1,
53
- preflight: buildPreflight(manifest, warnings, intents),
54
+ preflight,
54
55
  decision: aiDecision(warnings, intents),
55
56
  workspace: manifest.workspace,
56
57
  safeRuntime: {
@@ -60,6 +61,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
60
61
  },
61
62
  inventory: inventorySummary(manifest.inventory),
62
63
  security: securitySummary(manifest.security),
64
+ dependencyHandoff: dependencyHandoffSummary(preflight),
63
65
  policy: {
64
66
  node: policy.node || "not set",
65
67
  python: policy.python || "not set",
@@ -82,6 +84,20 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
82
84
  };
83
85
  }
84
86
 
87
+ function dependencyHandoffSummary(preflight = {}) {
88
+ const protocol = preflight.dependencyChangeProtocol || {};
89
+ return {
90
+ readSet: (preflight.dependencyReadSet || []).slice(0, 5),
91
+ protocol: {
92
+ mode: protocol.mode || "advisory",
93
+ packageManagerPolicy: protocol.packageManagerPolicy || "not-detected",
94
+ recordIntent: protocol.commands?.recordIntent || "aienvmp intent --actor agent:id --action planned-change --target dependency",
95
+ recordAfterChange: protocol.commands?.recordAfterChange || "aienvmp record --actor agent:id --summary dependency-change --target dependency",
96
+ handoff: protocol.commands?.handoff || "aienvmp handoff --record --actor agent:id"
97
+ }
98
+ };
99
+ }
100
+
85
101
  function securitySummary(security = {}) {
86
102
  return {
87
103
  mode: security.mode || "basic",
@@ -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(`handoff: ${output.nextAgent.handoffCommand}`);
28
29
  console.log(`strict: ${output.enforcement.recommendedCommand}`);
29
30
  if (artifact) console.log(`status: ${artifact}`);
30
31
  }
package/src/doctor.js CHANGED
@@ -110,17 +110,23 @@ export function handoffWarnings(timeline = []) {
110
110
 
111
111
  function inferTarget(action = "") {
112
112
  const normalized = String(action).toLowerCase();
113
- for (const target of ["node", "python", "docker", "npm", "pnpm", "yarn", "uv", "pip", "pipx"]) {
113
+ for (const target of ["dependency", "node", "python", "docker", "npm", "pnpm", "yarn", "uv", "pip", "pipx"]) {
114
114
  if (normalized.includes(target)) return target;
115
115
  }
116
+ if (normalized.includes("package") || normalized.includes("lockfile") || normalized.includes("vulnerab")) return "dependency";
116
117
  return "";
117
118
  }
118
119
 
119
120
  function isEnvironmentChange(item = {}) {
120
- if (["runtime", "package-manager", "container"].includes(item.change?.scope)) return true;
121
+ if (["runtime", "package-manager", "container", "dependency"].includes(item.change?.scope)) return true;
121
122
  if (item.type === "detected-change") return false;
122
123
  const text = `${item.type || ""} ${item.target || ""} ${item.summary || ""} ${item.action || ""} ${item.change?.scope || ""} ${item.change?.key || ""}`.toLowerCase();
123
124
  return [
125
+ "dependency",
126
+ "dependencies",
127
+ "package",
128
+ "lockfile",
129
+ "vulnerability",
124
130
  "runtime",
125
131
  "node",
126
132
  "python",
package/src/preflight.js CHANGED
@@ -9,6 +9,9 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
9
9
  const state = decision.reviewRequired ? "review-required" : "clear";
10
10
  const topAction = actions[0] || null;
11
11
  const intentTargets = recommendedIntentTargets(manifest, warnings, intents);
12
+ const dependencyReadSet = dependencyPreflightReadSet(manifest);
13
+ const dependencyChangeProtocol = dependencyProtocol(manifest, dependencyReadSet);
14
+ const coordination = coordinationSummary(intents);
12
15
  return {
13
16
  schemaVersion: 1,
14
17
  state,
@@ -45,7 +48,11 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
45
48
  environmentChanges: decision.canChangeEnvironmentWithoutReview ? "allowed" : "intent-and-review-first"
46
49
  },
47
50
  quickstart: agentQuickstart(decision.reviewRequired),
51
+ nextAgent: nextAgentHint(state, dependencyReadSet, dependencyChangeProtocol),
52
+ coordination,
48
53
  intentTargets,
54
+ dependencyReadSet,
55
+ dependencyChangeProtocol,
49
56
  artifacts: preflightArtifacts(),
50
57
  readOrder: [
51
58
  ".aienvmp/status.json",
@@ -68,6 +75,114 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
68
75
  };
69
76
  }
70
77
 
78
+ function coordinationSummary(intents = []) {
79
+ const byTarget = new Map();
80
+ for (const intent of intents) {
81
+ const target = normalizeTarget(intent.target || targetFromText(intent.action) || "environment") || "environment";
82
+ const item = byTarget.get(target) || { target, count: 0, actors: [], actions: [], conflict: false };
83
+ item.count += 1;
84
+ if (intent.actor && !item.actors.includes(intent.actor)) item.actors.push(intent.actor);
85
+ if (intent.action) item.actions.push(intent.action);
86
+ byTarget.set(target, item);
87
+ }
88
+ const targets = [...byTarget.values()].map((item) => ({
89
+ target: item.target,
90
+ count: item.count,
91
+ actors: item.actors.slice(0, 5),
92
+ actions: item.actions.slice(-3),
93
+ conflict: item.count > 1 && item.actors.length > 1
94
+ }));
95
+ const conflictTargets = targets.filter((item) => item.conflict).map((item) => item.target);
96
+ return {
97
+ openIntentCount: intents.length,
98
+ targets,
99
+ conflictTargets,
100
+ next: conflictTargets.length
101
+ ? "Review or resolve conflicting intents before environment changes."
102
+ : intents.length
103
+ ? "Check open intents before environment changes."
104
+ : "No open environment intents."
105
+ };
106
+ }
107
+
108
+ function nextAgentHint(state, dependencyReadSet = [], dependencyChangeProtocol = {}) {
109
+ const firstDependency = dependencyReadSet[0];
110
+ const dependencyFiles = firstDependency
111
+ ? [firstDependency.manifest, ...(firstDependency.lockfiles || [])].filter(Boolean)
112
+ : [];
113
+ return {
114
+ handoffCommand: dependencyChangeProtocol.commands?.handoff || "aienvmp handoff --record --actor agent:id",
115
+ readFirst: ".aienvmp/status.json",
116
+ readNext: "aienvmp context --json",
117
+ reviewState: state,
118
+ dependencyFiles,
119
+ dependencyProtocol: dependencyChangeProtocol.commands?.recordIntent
120
+ ? "record dependency intent, sync after change, then record dependency-change"
121
+ : "no dependency protocol detected",
122
+ rule: state === "clear"
123
+ ? "Next AI may continue project-local work; record intent before environment changes."
124
+ : "Next AI should review warnings or open intents before environment changes."
125
+ };
126
+ }
127
+
128
+ function dependencyProtocol(manifest = {}, dependencyReadSet = []) {
129
+ const pmPolicy = manifest.lightSbom?.packageManagerPolicy || {};
130
+ return {
131
+ mode: "advisory",
132
+ appliesWhen: "Before package, lockfile, or vulnerability remediation changes.",
133
+ packageManagerPolicy: pmPolicy.status || "not-detected",
134
+ beforeChange: [
135
+ "Read dependencyReadSet manifests and lockfiles.",
136
+ "Check lightSbom.packageManagerPolicy before choosing npm, pnpm, yarn, pip, uv, or another manager.",
137
+ "Record dependency intent before edits."
138
+ ],
139
+ commands: {
140
+ readContext: "aienvmp context --json",
141
+ recordIntent: "aienvmp intent --actor agent:id --action planned-change --target dependency",
142
+ refreshAfterChange: "aienvmp sync",
143
+ recordAfterChange: "aienvmp record --actor agent:id --summary dependency-change --target dependency",
144
+ handoff: "aienvmp handoff --record --actor agent:id"
145
+ },
146
+ mustNotDo: [
147
+ "Do not switch package managers because another lockfile exists without user approval.",
148
+ "Do not delete lockfiles or rewrite dependency manifests only to satisfy a tool preference.",
149
+ "Do not run automatic fix commands without reviewing the dependency read set first."
150
+ ],
151
+ readSetCount: dependencyReadSet.length
152
+ };
153
+ }
154
+
155
+ function dependencyPreflightReadSet(manifest = {}) {
156
+ const hints = manifest.lightSbom?.dependencyChangeHints || [];
157
+ const readSet = hints.map((hint) => ({
158
+ manifest: hint.manifest,
159
+ ecosystem: hint.ecosystem || "unknown",
160
+ manager: hint.manager || "unknown",
161
+ groups: hint.groups || [],
162
+ lockfiles: (hint.lockfiles || []).map((item) => item.file).filter(Boolean),
163
+ riskPackages: (hint.riskPackages || []).map((item) => item.name).filter(Boolean).slice(0, 5),
164
+ reason: hint.riskPackages?.length
165
+ ? "Read before dependency or security remediation; vulnerable packages are linked to this manifest."
166
+ : "Read before dependency changes; this manifest defines project packages."
167
+ }));
168
+ if (readSet.length) return readSet.slice(0, 8);
169
+
170
+ const manifests = manifest.lightSbom?.summary?.manifests || manifest.dependencySnapshot?.manifests || [];
171
+ const lockfiles = (manifest.lightSbom?.summary?.lockfiles || manifest.dependencySnapshot?.lockfiles || [])
172
+ .map((item) => item.file || item)
173
+ .filter(Boolean);
174
+ if (!manifests.length && !lockfiles.length) return [];
175
+ return [{
176
+ manifest: manifests[0] || "",
177
+ ecosystem: "unknown",
178
+ manager: "unknown",
179
+ groups: [],
180
+ lockfiles,
181
+ riskPackages: [],
182
+ reason: "Read detected dependency files before package changes."
183
+ }];
184
+ }
185
+
71
186
  function recommendedIntentTargets(manifest = {}, warnings = [], intents = []) {
72
187
  const targets = [];
73
188
  for (const warning of warnings) {
package/src/render.js CHANGED
@@ -92,6 +92,25 @@ function preflightLines(preflight = {}) {
92
92
  } else {
93
93
  lines.push("- environment: `aienvmp intent --actor agent:id --action planned-change --target environment`");
94
94
  }
95
+ const dependencyReadSet = preflight.dependencyReadSet || [];
96
+ if (dependencyReadSet.length) {
97
+ lines.push("", "## Dependency Read Set", "");
98
+ for (const item of dependencyReadSet.slice(0, 5)) {
99
+ const files = [item.manifest, ...(item.lockfiles || [])].filter(Boolean).join(", ");
100
+ const risk = item.riskPackages?.length ? `; risk: ${item.riskPackages.join(", ")}` : "";
101
+ lines.push(`- ${files}: ${item.ecosystem}/${item.manager}${risk} - ${item.reason}`);
102
+ }
103
+ }
104
+ const dependencyProtocol = preflight.dependencyChangeProtocol;
105
+ if (dependencyProtocol) {
106
+ lines.push("", "## Dependency Change Protocol", "");
107
+ lines.push(`- Mode: ${dependencyProtocol.mode}`);
108
+ lines.push(`- Package manager policy: ${dependencyProtocol.packageManagerPolicy}`);
109
+ lines.push(`- Intent: \`${dependencyProtocol.commands.recordIntent}\``);
110
+ lines.push(`- After change: \`${dependencyProtocol.commands.refreshAfterChange}\` then \`${dependencyProtocol.commands.recordAfterChange}\``);
111
+ lines.push(`- Handoff: \`${dependencyProtocol.commands.handoff}\``);
112
+ for (const item of dependencyProtocol.mustNotDo.slice(0, 3)) lines.push(`- Must not: ${item}`);
113
+ }
95
114
  return lines;
96
115
  }
97
116
 
@@ -186,6 +205,9 @@ export function renderHandoff(handoff) {
186
205
  "Recommended actions:",
187
206
  ...(handoff.recommendedActions?.length ? handoff.recommendedActions.map((item) => `- [${item.priority}] ${item.summary}${item.command ? ` (${item.command})` : ""}`) : ["- none"]),
188
207
  "",
208
+ "Dependency handoff:",
209
+ ...dependencyHandoffLines(handoff.dependencyHandoff),
210
+ "",
189
211
  "Recent changes:",
190
212
  ...(handoff.recentChanges.length ? handoff.recentChanges.map((t) => `- ${formatTimeline(t)}`) : ["- none"]),
191
213
  "",
@@ -198,6 +220,24 @@ export function renderHandoff(handoff) {
198
220
  return lines.join("\n");
199
221
  }
200
222
 
223
+ function dependencyHandoffLines(dependencyHandoff = {}) {
224
+ const readSet = dependencyHandoff.readSet || [];
225
+ const protocol = dependencyHandoff.protocol || {};
226
+ const lines = [];
227
+ if (readSet.length) {
228
+ for (const item of readSet.slice(0, 3)) {
229
+ const files = [item.manifest, ...(item.lockfiles || [])].filter(Boolean).join(", ");
230
+ lines.push(`- Read: ${files || "dependency files"} (${item.ecosystem || "deps"}/${item.manager || "unknown"})`);
231
+ }
232
+ } else {
233
+ lines.push("- Read: no dependency files detected");
234
+ }
235
+ lines.push(`- Intent: ${protocol.recordIntent || "aienvmp intent --actor agent:id --action planned-change --target dependency"}`);
236
+ lines.push(`- After change: ${protocol.recordAfterChange || "aienvmp record --actor agent:id --summary dependency-change --target dependency"}`);
237
+ lines.push(`- Handoff: ${protocol.handoff || "aienvmp handoff --record --actor agent:id"}`);
238
+ return lines;
239
+ }
240
+
201
241
  export function renderPlan(plan) {
202
242
  const lines = [
203
243
  "# AI Environment Plan",
@@ -218,6 +258,9 @@ export function renderPlan(plan) {
218
258
  "Review gates:",
219
259
  ...plan.reviewGates.map((item) => `- ${item}`),
220
260
  "",
261
+ "Dependency protocol:",
262
+ ...dependencyProtocolPlanLines(plan.preflight?.dependencyChangeProtocol),
263
+ "",
221
264
  "Remediation steps:",
222
265
  ...(plan.remediationSteps?.length ? plan.remediationSteps.slice(0, 5).flatMap(remediationLines) : ["- none"]),
223
266
  "",
@@ -231,6 +274,17 @@ export function renderPlan(plan) {
231
274
  return lines.join("\n");
232
275
  }
233
276
 
277
+ function dependencyProtocolPlanLines(protocol = {}) {
278
+ if (!protocol.commands) return ["- none"];
279
+ return [
280
+ `- Mode: ${protocol.mode || "advisory"}`,
281
+ `- Package manager policy: ${protocol.packageManagerPolicy || "not-detected"}`,
282
+ `- Intent: ${protocol.commands.recordIntent}`,
283
+ `- After change: ${protocol.commands.refreshAfterChange}; ${protocol.commands.recordAfterChange}`,
284
+ ...(protocol.mustNotDo || []).slice(0, 3).map((item) => `- Must not: ${item}`)
285
+ ];
286
+ }
287
+
234
288
  function environmentLines(item) {
235
289
  return [
236
290
  `- ${item.category}: ${item.summary}`,
@@ -354,6 +408,10 @@ const strictCommands=enforcementProfile.strictCommands||[];
354
408
  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
409
  const intentTargets=manifest.preflight?.intentTargets||[];
356
410
  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>';
411
+ const dependencyReadSet=manifest.preflight?.dependencyReadSet||[];
412
+ 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>';
413
+ const dependencyProtocol=manifest.preflight?.dependencyChangeProtocol||{};
414
+ 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>';
357
415
  const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
358
416
  const reviewRequired=warnings.length>0||intents.length>0;
359
417
  const recentChanges=timeline.slice(-8).length;
@@ -361,7 +419,12 @@ const trustState=manifest.trust?.state||'observed';
361
419
  const nextAction=reviewRequired?'Review before environment changes':'Proceed with project-local work';
362
420
  const auditItem=(key,value,hint,klass='')=>\`<div class="audit-item \${klass}"><div class="audit-k">\${key}</div><div class="audit-v">\${value}</div><div class="audit-hint">\${hint}</div></div>\`;
363
421
  const driftLabel=warnings.length?'detected':'none';
364
- const handoffHtml=\`<table><tr><th>Status</th><td>\${reviewRequired?'review-required':'clear'}</td></tr><tr><th>Trust</th><td><code>\${esc(trustState)}</code></td></tr><tr><th>Node</th><td><code>\${esc(manifest.runtimes.node||'not detected')}</code></td></tr><tr><th>Python</th><td><code>\${esc(manifest.runtimes.python||manifest.runtimes.python3||'not detected')}</code></td></tr><tr><th>Docker</th><td>\${manifest.containers?.docker?'available':'not detected'}</td></tr><tr><th>Next</th><td>\${reviewRequired?'Review warnings and open intents':'Continue project-local work'}</td></tr></table>\`;
422
+ const nextAgent=manifest.preflight?.nextAgent||{};
423
+ const coordination=manifest.preflight?.coordination||{};
424
+ const conflictTargets=coordination.conflictTargets||[];
425
+ const handoffFiles=nextAgent.dependencyFiles?.length?nextAgent.dependencyFiles:(dependencyReadSet[0]?[dependencyReadSet[0].manifest,...(dependencyReadSet[0].lockfiles||[])].filter(Boolean):[]);
426
+ const handoffNext=nextAgent.rule||(reviewRequired?'Review warnings and open intents':'Continue project-local work');
427
+ const handoffHtml=\`<table><tr><th>Status</th><td>\${reviewRequired?'review-required':'clear'}</td></tr><tr><th>Trust</th><td><code>\${esc(trustState)}</code></td></tr><tr><th>Read first</th><td><code>\${esc(nextAgent.readFirst||'.aienvmp/status.json')}</code></td></tr><tr><th>Dependency files</th><td>\${handoffFiles.length?'<code>'+esc(handoffFiles.join(', '))+'</code>':'none'}</td></tr><tr><th>Conflicts</th><td>\${conflictTargets.length?'<code>'+esc(conflictTargets.join(', '))+'</code>':'none'}</td></tr><tr><th>Next</th><td>\${esc(handoffNext)}</td></tr></table>\`;
365
428
  document.getElementById('app').innerHTML=\`
366
429
  <header>
367
430
  <div>
@@ -399,6 +462,10 @@ document.getElementById('app').innerHTML=\`
399
462
  <div style="height:14px"></div>
400
463
  \${card('AI Intent Targets','<span class="pill">'+intentTargets.length+' targets</span>',intentTargetsHtml)}
401
464
  <div style="height:14px"></div>
465
+ \${card('Dependency Read Set','<span class="pill">'+dependencyReadSet.length+' files</span>',dependencyReadSetHtml)}
466
+ <div style="height:14px"></div>
467
+ \${card('Dependency Protocol','<span class="pill">'+(dependencyProtocol.mode||'advisory')+'</span>',dependencyProtocolHtml)}
468
+ <div style="height:14px"></div>
402
469
  \${card('AI Plan Artifacts',plan.markdown||plan.json?'<span class="pill">written</span>':'<span class="pill off">not written</span>',planHtml)}
403
470
  <div style="height:14px"></div>
404
471
  \${card('Remediation Steps',remediation.length?'<span class="pill warn">'+remediation.length+' items</span>':'<span class="pill off">none</span>',remediationHtml)}