aienvmp 0.1.43 → 0.1.45

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
@@ -46,6 +46,18 @@ Short record of bugs, fixes, and follow-up checks.
46
46
  - Fix: `record` timeline entries now include follow-up metadata, and status/context/dashboard surface unresolved follow-ups.
47
47
  - Verification: Windows and macOS candidate smoke checks confirmed `record --target dependency` appears in `status --json` followUps and the dashboard Follow-ups card.
48
48
 
49
+ ### Multi-agent records were less visible than open intents
50
+
51
+ - Issue: open intents showed planning conflicts, but two agents could record changes to the same env target after a handoff without a compact coordination signal.
52
+ - Fix: `doctor`, status/context, handoff, and the dashboard now expose same-target multi-agent records through advisory warnings and `agentActivity`.
53
+ - Verification: tests cover multi-agent record detection, later handoff reset, status JSON, handoff text, dashboard HTML, and recommended actions.
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
+
49
61
  ## Template
50
62
 
51
63
  ### Title
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.45
4
+
5
+ - Added `aienvmp checkpoint` as a one-command post-environment-change flow for record, sync, status, and handoff.
6
+ - Added quiet command support so checkpoint can compose existing commands without noisy intermediate output.
7
+ - Added checkpoint sync ledger entries so follow-up detection can prove the refresh step happened.
8
+ - Added checkpoint guidance to the shared preflight, decision, manifest, dependency protocol, and status outputs.
9
+ - Updated AIENV, handoff, plan, dashboard, and agent pointer rendering to prefer checkpoint after environment changes.
10
+ - Shortened the README environment-change flow to intent plus checkpoint.
11
+ - Added checkpoint regression coverage for JSON output, ledger updates, follow-up closure, status artifacts, and rendered guidance.
12
+
13
+ ## 0.1.44
14
+
15
+ - Added multi-agent record warnings when multiple agents record environment changes for the same target after the last handoff.
16
+ - Added `agentActivity` to the shared preflight contract so status/context/handoff can expose recent env records by target and actor.
17
+ - Surfaced agent activity in context, handoff, and the dashboard for faster shared-server review.
18
+ - Added a recommended handoff action for multi-agent record activity.
19
+ - Mapped `multi-agent-records` into the optional `doctor --strict coordination` scope while keeping local behavior advisory by default.
20
+ - Documented the new AI contract field in the compact README.
21
+ - Added regression tests for warning detection, handoff reset behavior, status JSON, dashboard HTML, and recommended actions.
22
+
3
23
  ## 0.1.43
4
24
 
5
25
  - Added follow-up metadata to `record` timeline entries so dependency/security changes point agents back to sync, status, and 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.
@@ -51,6 +48,7 @@ AIENV.md # Markdown env map for AI agents
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.
53
50
  - `coordination.conflictTargets` shows where multiple agents are planning changes.
51
+ - `agentActivity.multiActorTargets` shows where multiple agents actually recorded env changes.
54
52
  - `followUps` shows records that still need `sync`, `status`, or `handoff`.
55
53
  - `handoff` carries dependency read-set and protocol guidance for the next AI.
56
54
  - Light SBOM includes source/confidence hints; verify security claims with dedicated scanners.
@@ -78,6 +76,7 @@ aienvmp plan --write # read-only action plan
78
76
  aienvmp handoff --record # next-agent summary
79
77
  aienvmp intent # record planned env change
80
78
  aienvmp record # record what changed
79
+ aienvmp checkpoint # record + sync + status + handoff after env change
81
80
  aienvmp doctor --strict security|policy|coordination|all
82
81
  ```
83
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
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
@@ -15,6 +15,10 @@ export function recommendedActions(manifest = {}, context = {}) {
15
15
  actions.push(action("coordinate-agents", "high", "coordination", "Multiple agents are planning changes to the same environment target. Coordinate with the user before proceeding."));
16
16
  }
17
17
 
18
+ if (warnings.some((warning) => warning.code === "multi-agent-records")) {
19
+ actions.push(action("review-agent-activity", "high", "coordination", "Multiple agents recorded changes for the same environment target. Review activity and record a handoff before more environment work.", "aienvmp handoff --record --actor agent:id"));
20
+ }
21
+
18
22
  actions.push(...securityActions(manifest.security));
19
23
 
20
24
  if (hasRuntimePolicyWarning(warnings)) {
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
+ }
@@ -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;
@@ -63,6 +63,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
63
63
  security: securitySummary(manifest.security),
64
64
  dependencyHandoff: dependencyHandoffSummary(preflight),
65
65
  coordination: preflight.coordination,
66
+ agentActivity: preflight.agentActivity,
66
67
  policy: {
67
68
  node: policy.node || "not set",
68
69
  python: policy.python || "not set",
@@ -94,6 +95,7 @@ function dependencyHandoffSummary(preflight = {}) {
94
95
  packageManagerPolicy: protocol.packageManagerPolicy || "not-detected",
95
96
  recordIntent: protocol.commands?.recordIntent || "aienvmp intent --actor agent:id --action planned-change --target dependency",
96
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",
97
99
  handoff: protocol.commands?.handoff || "aienvmp handoff --record --actor agent:id"
98
100
  }
99
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", "followUps", "dependencyReadSet", "dependencyChangeProtocol"],
7
+ aiEntryFields: ["state", "summary", "nextAgent", "coordination", "agentActivity", "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",
package/src/doctor.js CHANGED
@@ -46,6 +46,7 @@ export function diagnose(manifest, context = {}) {
46
46
  warnings.push(...coordinationWarnings(context.intents || []));
47
47
  warnings.push(...staleIntentWarnings(context.intents || []));
48
48
  warnings.push(...handoffWarnings(context.timeline || []));
49
+ warnings.push(...multiAgentRecordWarnings(context.timeline || []));
49
50
  return warnings;
50
51
  }
51
52
 
@@ -108,6 +109,32 @@ export function handoffWarnings(timeline = []) {
108
109
  }];
109
110
  }
110
111
 
112
+ export function multiAgentRecordWarnings(timeline = []) {
113
+ const lastHandoffAt = lastTime(timeline, (item) => item.type === "agent-handoff");
114
+ const byTarget = new Map();
115
+ for (const item of timeline) {
116
+ if (!isEnvironmentChange(item)) continue;
117
+ if (lastHandoffAt && timeOf(item) <= lastHandoffAt) continue;
118
+ const actor = String(item.actor || "").trim();
119
+ if (!actor) continue;
120
+ const target = normalizeTarget(item.target || inferTarget(`${item.summary || ""} ${item.action || ""} ${item.change?.key || ""}`) || "environment");
121
+ const list = byTarget.get(target) || [];
122
+ list.push(item);
123
+ byTarget.set(target, list);
124
+ }
125
+ const warnings = [];
126
+ for (const [target, list] of byTarget) {
127
+ const actors = [...new Set(list.map((item) => item.actor).filter(Boolean))];
128
+ if (actors.length < 2) continue;
129
+ warnings.push({
130
+ code: "multi-agent-records",
131
+ target,
132
+ message: `Multiple agents recorded environment changes for ${target} after the last handoff. Run handoff and review follow-ups before continuing.`
133
+ });
134
+ }
135
+ return warnings;
136
+ }
137
+
111
138
  function inferTarget(action = "") {
112
139
  const normalized = String(action).toLowerCase();
113
140
  for (const target of ["dependency", "node", "python", "docker", "npm", "pnpm", "yarn", "uv", "pip", "pipx"]) {
@@ -117,6 +144,23 @@ function inferTarget(action = "") {
117
144
  return "";
118
145
  }
119
146
 
147
+ function normalizeTarget(target = "") {
148
+ const normalized = String(target).trim().toLowerCase();
149
+ if (["npm", "pnpm", "yarn"].includes(normalized)) return "package-manager";
150
+ if (["pip", "pipx", "uv"].includes(normalized)) return "python";
151
+ return normalized || "environment";
152
+ }
153
+
154
+ function lastTime(items = [], predicate) {
155
+ const item = [...items].reverse().find(predicate);
156
+ return item ? timeOf(item) : 0;
157
+ }
158
+
159
+ function timeOf(item = {}) {
160
+ const value = new Date(item.at || 0).getTime();
161
+ return Number.isFinite(value) ? value : 0;
162
+ }
163
+
120
164
  function isEnvironmentChange(item = {}) {
121
165
  if (["runtime", "package-manager", "container", "dependency"].includes(item.change?.scope)) return true;
122
166
  if (item.type === "detected-change") return false;
@@ -67,6 +67,6 @@ function warningMatchesScope(warning, scope) {
67
67
 
68
68
  function warningScope(code = "") {
69
69
  if (code === "security-vulnerabilities") return "security";
70
- if (["conflicting-open-intents", "stale-open-intent", "handoff-stale"].includes(code)) return "coordination";
70
+ if (["conflicting-open-intents", "stale-open-intent", "handoff-stale", "multi-agent-records"].includes(code)) return "coordination";
71
71
  return "policy";
72
72
  }
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
@@ -15,6 +15,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = [], timel
15
15
  const dependencyChangeProtocol = dependencyProtocol(manifest, dependencyReadSet);
16
16
  const coordination = coordinationSummary(intents);
17
17
  const followUps = pendingFollowUps(timeline);
18
+ const agentActivity = agentActivitySummary(timeline);
18
19
  return {
19
20
  schemaVersion: 1,
20
21
  contract: preflightContract(),
@@ -55,6 +56,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = [], timel
55
56
  quickstart: agentQuickstart(decision.reviewRequired),
56
57
  nextAgent: nextAgentHint(state, dependencyReadSet, dependencyChangeProtocol),
57
58
  coordination,
59
+ agentActivity,
58
60
  followUps,
59
61
  intentTargets,
60
62
  dependencyReadSet,
@@ -74,6 +76,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = [], timel
74
76
  context: "aienvmp context --json",
75
77
  plan: "aienvmp plan --write",
76
78
  handoff: "aienvmp handoff --record --actor agent:id",
79
+ checkpoint: "aienvmp checkpoint --actor agent:id --summary what-changed --target environment",
77
80
  recordIntent: intentTargets[0]?.command || "aienvmp intent --actor agent:id --action planned-change"
78
81
  },
79
82
  topAction,
@@ -81,6 +84,42 @@ export function buildPreflight(manifest = {}, warnings = [], intents = [], timel
81
84
  };
82
85
  }
83
86
 
87
+ function agentActivitySummary(timeline = []) {
88
+ const lastHandoffAt = lastTime(timeline, (item) => item.type === "agent-handoff");
89
+ const envRecords = timeline
90
+ .filter((item) => isEnvironmentRecord(item))
91
+ .filter((item) => !lastHandoffAt || timeOf(item) > lastHandoffAt);
92
+ const byTarget = new Map();
93
+ for (const item of envRecords) {
94
+ const target = normalizeTarget(item.target || targetFromText(`${item.summary || ""} ${item.action || ""}`) || "environment") || "environment";
95
+ const summary = byTarget.get(target) || { target, count: 0, actors: [], latest: null, multiActor: false };
96
+ summary.count += 1;
97
+ if (item.actor && !summary.actors.includes(item.actor)) summary.actors.push(item.actor);
98
+ if (!summary.latest || timeOf(item) > timeOf(summary.latest)) summary.latest = item;
99
+ byTarget.set(target, summary);
100
+ }
101
+ const targets = [...byTarget.values()].map((item) => ({
102
+ target: item.target,
103
+ count: item.count,
104
+ actors: item.actors.slice(0, 5),
105
+ latestAt: item.latest?.at || "",
106
+ latestSummary: item.latest?.summary || item.latest?.action || item.latest?.type || "",
107
+ multiActor: item.actors.length > 1
108
+ }));
109
+ const multiActorTargets = targets.filter((item) => item.multiActor).map((item) => item.target);
110
+ return {
111
+ sinceLastHandoff: lastHandoffAt ? "after-last-handoff" : "all-recorded",
112
+ environmentRecordCount: envRecords.length,
113
+ targets,
114
+ multiActorTargets,
115
+ next: multiActorTargets.length
116
+ ? "Run handoff and review follow-ups before another environment change."
117
+ : envRecords.length
118
+ ? "Run handoff before another AI continues environment work."
119
+ : "No environment records need coordination."
120
+ };
121
+ }
122
+
84
123
  function coordinationSummary(intents = []) {
85
124
  const byTarget = new Map();
86
125
  for (const intent of intents) {
@@ -111,6 +150,41 @@ function coordinationSummary(intents = []) {
111
150
  };
112
151
  }
113
152
 
153
+ function isEnvironmentRecord(item = {}) {
154
+ if (item.type === "detected-change") return false;
155
+ const text = `${item.type || ""} ${item.target || ""} ${item.summary || ""} ${item.action || ""} ${item.change?.scope || ""} ${item.change?.key || ""}`.toLowerCase();
156
+ return [
157
+ "dependency",
158
+ "dependencies",
159
+ "package",
160
+ "lockfile",
161
+ "vulnerability",
162
+ "runtime",
163
+ "node",
164
+ "python",
165
+ "docker",
166
+ "package manager",
167
+ "package-manager",
168
+ "npm",
169
+ "pnpm",
170
+ "yarn",
171
+ "uv",
172
+ "pip",
173
+ "pipx",
174
+ "global"
175
+ ].some((token) => text.includes(token));
176
+ }
177
+
178
+ function lastTime(items = [], predicate) {
179
+ const item = [...items].reverse().find(predicate);
180
+ return item ? timeOf(item) : 0;
181
+ }
182
+
183
+ function timeOf(item = {}) {
184
+ const value = new Date(item.at || 0).getTime();
185
+ return Number.isFinite(value) ? value : 0;
186
+ }
187
+
114
188
  function nextAgentHint(state, dependencyReadSet = [], dependencyChangeProtocol = {}) {
115
189
  const firstDependency = dependencyReadSet[0];
116
190
  const dependencyFiles = firstDependency
@@ -147,6 +221,7 @@ function dependencyProtocol(manifest = {}, dependencyReadSet = []) {
147
221
  recordIntent: "aienvmp intent --actor agent:id --action planned-change --target dependency",
148
222
  refreshAfterChange: "aienvmp sync",
149
223
  recordAfterChange: "aienvmp record --actor agent:id --summary dependency-change --target dependency",
224
+ checkpointAfterChange: "aienvmp checkpoint --actor agent:id --summary dependency-change --target dependency",
150
225
  handoff: "aienvmp handoff --record --actor agent:id"
151
226
  },
152
227
  mustNotDo: [
@@ -262,7 +337,8 @@ function agentQuickstart(reviewRequired) {
262
337
  readFirst: "aienvmp status --write",
263
338
  detailCommand: "aienvmp context --json",
264
339
  beforeEnvironmentChange: "aienvmp intent --actor agent:id --action planned-change --target <runtime|package-manager|docker|dependency>",
265
- afterEnvironmentChange: "aienvmp sync && aienvmp record --actor agent:id --summary what-changed",
340
+ afterEnvironmentChange: "aienvmp checkpoint --actor agent:id --summary what-changed --target environment",
341
+ checkpointAfterChange: "aienvmp checkpoint --actor agent:id --summary what-changed --target environment",
266
342
  handoff: "aienvmp handoff --record --actor agent:id",
267
343
  rule: reviewRequired
268
344
  ? "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"]),
@@ -176,6 +171,9 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
176
171
  "Follow-ups:",
177
172
  ...followUpLines(manifest.preflight?.followUps),
178
173
  "",
174
+ "Agent activity:",
175
+ ...agentActivityLines(manifest.preflight?.agentActivity),
176
+ "",
179
177
  "Open intents:",
180
178
  ...(intents.length ? intents.slice(-5).reverse().map((i) => `- ${i.actor}: ${i.action}`) : ["- none"]),
181
179
  "",
@@ -208,6 +206,9 @@ export function renderHandoff(handoff) {
208
206
  "Coordination:",
209
207
  ...coordinationHandoffLines(handoff.coordination),
210
208
  "",
209
+ "Agent activity:",
210
+ ...agentActivityLines(handoff.agentActivity),
211
+ "",
211
212
  "Warnings:",
212
213
  ...(handoff.warnings.length ? handoff.warnings.map((w) => `- ${w.message}`) : ["- none"]),
213
214
  "",
@@ -248,7 +249,7 @@ function dependencyHandoffLines(dependencyHandoff = {}) {
248
249
  lines.push("- Read: no dependency files detected");
249
250
  }
250
251
  lines.push(`- Intent: ${protocol.recordIntent || "aienvmp intent --actor agent:id --action planned-change --target dependency"}`);
251
- 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"}`);
252
253
  lines.push(`- Handoff: ${protocol.handoff || "aienvmp handoff --record --actor agent:id"}`);
253
254
  return lines;
254
255
  }
@@ -298,7 +299,7 @@ function dependencyProtocolPlanLines(protocol = {}) {
298
299
  `- Mode: ${protocol.mode || "advisory"}`,
299
300
  `- Package manager policy: ${protocol.packageManagerPolicy || "not-detected"}`,
300
301
  `- Intent: ${protocol.commands.recordIntent}`,
301
- `- After change: ${protocol.commands.refreshAfterChange}; ${protocol.commands.recordAfterChange}`,
302
+ `- After change: ${protocol.commands.checkpointAfterChange || `${protocol.commands.refreshAfterChange}; ${protocol.commands.recordAfterChange}`}`,
302
303
  ...(protocol.mustNotDo || []).slice(0, 3).map((item) => `- Must not: ${item}`)
303
304
  ];
304
305
  }
@@ -320,6 +321,16 @@ function followUpLines(followUps = []) {
320
321
  });
321
322
  }
322
323
 
324
+ function agentActivityLines(activity = {}) {
325
+ const targets = activity.targets || [];
326
+ if (!targets.length) return ["- none"];
327
+ return targets.slice(0, 5).map((item) => {
328
+ const actors = item.actors?.length ? item.actors.join(", ") : "unknown";
329
+ const flag = item.multiActor ? "multi-agent" : "single-agent";
330
+ return `- ${item.target}: ${item.count} record(s), ${actors}, ${flag}${item.latestSummary ? ` - ${item.latestSummary}` : ""}`;
331
+ });
332
+ }
333
+
323
334
  function environmentLines(item) {
324
335
  return [
325
336
  `- ${item.category}: ${item.summary}`,
@@ -448,10 +459,13 @@ const intentTargets=manifest.preflight?.intentTargets||[];
448
459
  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>';
449
460
  const followUps=manifest.preflight?.followUps||[];
450
461
  const followUpsHtml=followUps.length?'<div class="timeline">'+followUps.slice(0,5).map(f=>\`<div class="event"><time>\${esc(f.target||'env')}</time><div><b>\${esc(f.summary||'follow-up')}</b> \${esc(f.reason||'Refresh shared context.')}\${f.commands?.length?\`<div class="path">\${esc(f.commands.join(' -> '))}</div>\`:''}</div></div>\`).join('')+'</div>':'<div class="okline">No pending follow-ups after environment records.</div>';
462
+ const agentActivity=manifest.preflight?.agentActivity||{};
463
+ const activityTargets=agentActivity.targets||[];
464
+ const activityHtml=activityTargets.length?'<div class="timeline">'+activityTargets.slice(0,5).map(a=>\`<div class="event"><time>\${esc(a.target||'env')}</time><div><b>\${esc((a.actors||[]).join(', ')||'unknown')}</b> \${esc(a.count||0)} record(s) \${a.multiActor?'<code>multi-agent</code>':'<code>single-agent</code>'}\${a.latestSummary?\`<div class="path">\${esc(a.latestSummary)}</div>\`:''}</div></div>\`).join('')+'</div><div class="path">'+esc(agentActivity.next||'Review activity before environment changes.')+'</div>':'<div class="okline">No recorded environment activity needs coordination.</div>';
451
465
  const dependencyReadSet=manifest.preflight?.dependencyReadSet||[];
452
466
  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>';
453
467
  const dependencyProtocol=manifest.preflight?.dependencyChangeProtocol||{};
454
- 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>';
468
+ 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>';
455
469
  const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
456
470
  const reviewRequired=warnings.length>0||intents.length>0;
457
471
  const recentChanges=timeline.slice(-8).length;
@@ -504,6 +518,8 @@ document.getElementById('app').innerHTML=\`
504
518
  <div style="height:14px"></div>
505
519
  \${card('Follow-ups',followUps.length?'<span class="pill warn">'+followUps.length+' pending</span>':'<span class="pill">clear</span>',followUpsHtml)}
506
520
  <div style="height:14px"></div>
521
+ \${card('Agent Activity',agentActivity.multiActorTargets?.length?'<span class="pill warn">'+agentActivity.multiActorTargets.length+' shared</span>':'<span class="pill">clear</span>',activityHtml)}
522
+ <div style="height:14px"></div>
507
523
  \${card('AI Contract','<span class="pill">'+(contract.stability||'additive')+'</span>',contractHtml)}
508
524
  <div style="height:14px"></div>
509
525
  \${card('Dependency Read Set','<span class="pill">'+dependencyReadSet.length+' files</span>',dependencyReadSetHtml)}