aienvmp 0.1.4 → 0.1.5

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.5
4
+
5
+ - Added machine-readable trust states for observed, planned, changed, review, verified, and stale environment facts.
6
+ - Added multi-agent intent conflict warnings for shared runtime/package manager targets.
7
+ - Added trust and schema context to `context`, `handoff`, `doctor`, `AIENV.md`, and the dashboard.
8
+ - Repositioned the docs around AI environment coordination instead of general AI project memory.
9
+
3
10
  ## 0.1.4
4
11
 
5
12
  - Added `aienvmp handoff` for next-agent environment handoff summaries.
package/README.md CHANGED
@@ -6,22 +6,18 @@
6
6
 
7
7
  **AI Environment Map.**
8
8
 
9
- `aienvmp` gives AI agents a shared view of the current dev environment:
9
+ `aienvmp` is an AI environment coordination tool for shared coding machines.
10
10
 
11
- - runtime versions
12
- - package managers
13
- - Docker state
14
- - project version hints
15
- - planned env changes
16
- - recent env changes
11
+ It helps Codex, Claude, Gemini, and other agents avoid silently using or installing different runtime and tool versions.
17
12
 
18
- So multiple AI agents do not silently use or install different software versions.
13
+ Core loop: scan the env, give AI a preflight context, and hand off safe next steps to the next agent.
19
14
 
20
15
  ## Use
21
16
 
22
17
  ```bash
23
18
  npx aienvmp sync
24
19
  npx aienvmp context
20
+ npx aienvmp handoff
25
21
  ```
26
22
 
27
23
  ## Output
@@ -34,6 +30,10 @@ AIENV.md
34
30
  .aienvmp/dashboard.html
35
31
  ```
36
32
 
33
+ Trust states are machine-readable: `observed`, `planned`, `changed`, `review`, `verified`, `stale`.
34
+
35
+ AI agents can observe, plan, and record. Only a human or CI should mark environment facts as verified.
36
+
37
37
  ## For AGENTS.md
38
38
 
39
39
  `aienvmp` does not replace AGENTS.md. It gives AGENTS.md a live environment source of truth.
package/ROADMAP.md CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
  ## Near Term
6
6
 
7
- - Policy checks for Node, Python, and package manager drift
8
- - Intent lifecycle: open, resolve, cancel
9
- - JSON output for AI/tool integrations
10
- - Non-blocking by default, strict only with `--ci`
11
- - Dashboard improvements for policy and agent coordination
7
+ - Strengthen trust states: observed, planned, changed, review, verified, stale
8
+ - Detect multi-agent environment intent conflicts
9
+ - Stabilize `.aienvmp/manifest.json` and JSON command schemas
10
+ - Keep `sync`, `context`, and `handoff` as the simple core flow
11
+ - Improve the dashboard for 10-second human review
12
12
 
13
13
  ## Next
14
14
 
@@ -21,8 +21,8 @@
21
21
  - `pipx list`
22
22
  - `uv tool list`
23
23
  - Conflict detection:
24
- - multiple open intents for the same target
25
24
  - stale unresolved intents
25
+ - recent runtime changes without a fresh handoff
26
26
  - package manager policy vs lockfile mismatch
27
27
  - CI mode:
28
28
  - stable exit codes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@ export async function compileWorkspace(args) {
13
13
  const timeline = await readTimeline(timelinePath(dir));
14
14
  const intents = openIntents(await readJsonl(intentsPath(dir)));
15
15
  const policy = await loadPolicy(dir);
16
- const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
16
+ const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
17
17
  const rendered = renderAIEnv(manifest, timeline, warnings, intents, policy);
18
18
  await fs.writeFile(aiEnvPath(dir), rendered, "utf8");
19
19
  if (!args.quiet) {
@@ -12,12 +12,13 @@ export async function contextWorkspace(args) {
12
12
  const timeline = await readTimeline(timelinePath(dir));
13
13
  const intents = openIntents(await readJsonl(intentsPath(dir)));
14
14
  const policy = await loadPolicy(dir);
15
- const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
15
+ const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
16
16
  const decision = contextDecision(warnings, intents);
17
17
  if (args.json) {
18
18
  console.log(JSON.stringify({
19
19
  status: warnings.length ? "review-required" : "clear",
20
20
  decision,
21
+ trust: manifest.trust || {},
21
22
  guidance: decision,
22
23
  workspace: manifest.workspace,
23
24
  runtimes: manifest.runtimes,
@@ -15,7 +15,7 @@ export async function dashWorkspace(args) {
15
15
  const timeline = await readTimeline(timelinePath(dir));
16
16
  const intents = openIntents(await readJsonl(intentsPath(dir)));
17
17
  const policy = await loadPolicy(dir);
18
- const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
18
+ const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
19
19
  const html = renderDashboard(manifest, timeline, warnings, intents, policy);
20
20
  const out = dashboardPath(dir);
21
21
  await fs.mkdir(path.dirname(out), { recursive: true });
@@ -1,6 +1,7 @@
1
1
  import { diagnose } from "../doctor.js";
2
2
  import { readJson } from "../fsutil.js";
3
- import { manifestPath, workspaceDir } from "../paths.js";
3
+ import { openIntents, readJsonl, readTimeline } from "../timeline.js";
4
+ import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
4
5
  import { loadPolicy, policyWarnings } from "../policy.js";
5
6
 
6
7
  export async function doctorWorkspace(args) {
@@ -8,11 +9,15 @@ export async function doctorWorkspace(args) {
8
9
  const manifest = await readJson(manifestPath(dir));
9
10
  if (!manifest) throw new Error("missing manifest; run `aienvmp sync` first");
10
11
  const policy = await loadPolicy(dir);
11
- const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
12
+ const timeline = await readTimeline(timelinePath(dir));
13
+ const intents = openIntents(await readJsonl(intentsPath(dir)));
14
+ const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
12
15
  if (args.json) {
13
16
  console.log(JSON.stringify({
14
17
  status: warnings.length ? "warning" : "ok",
18
+ trust: manifest.trust || {},
15
19
  policy,
20
+ openIntentCount: intents.length,
16
21
  warnings
17
22
  }, null, 2));
18
23
  if (args.ci && warnings.length) {
@@ -12,7 +12,7 @@ export async function handoffWorkspace(args) {
12
12
  const timeline = await readTimeline(timelinePath(dir));
13
13
  const intents = openIntents(await readJsonl(intentsPath(dir)));
14
14
  const policy = await loadPolicy(dir);
15
- const warnings = [...diagnose(manifest), ...policyWarnings(manifest, policy)];
15
+ const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
16
16
  const handoff = buildHandoff(manifest, timeline, warnings, intents, policy);
17
17
 
18
18
  if (args.json) {
@@ -27,6 +27,8 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
27
27
  const reviewRequired = warnings.length > 0 || intents.length > 0;
28
28
  return {
29
29
  status: reviewRequired ? "review-required" : "clear",
30
+ trust: manifest.trust || {},
31
+ schemaVersion: manifest.schemaVersion || 1,
30
32
  workspace: manifest.workspace,
31
33
  safeRuntime: {
32
34
  node: manifest.runtimes?.node || "not detected",
@@ -1,19 +1,22 @@
1
1
  import { appendJsonLine } from "../fsutil.js";
2
2
  import { intentsPath, workspaceDir } from "../paths.js";
3
3
  import { newIntentID } from "../timeline.js";
4
+ import { plannedTrust } from "../trust.js";
4
5
 
5
6
  export async function intentWorkspace(args) {
6
7
  const dir = workspaceDir(args);
7
8
  const actor = required(args.actor, "actor");
8
9
  const action = required(args.action, "action");
10
+ const now = new Date();
9
11
  const entry = {
10
- at: new Date().toISOString(),
12
+ at: now.toISOString(),
11
13
  type: "intent",
12
14
  actor,
13
15
  action,
14
16
  target: args.target || "",
15
17
  reason: args.reason || "",
16
- status: "open"
18
+ status: "open",
19
+ trust: plannedTrust(now)
17
20
  };
18
21
  entry.id = newIntentID();
19
22
  await appendJsonLine(intentsPath(dir), entry);
@@ -1,12 +1,15 @@
1
1
  import { appendJsonLine } from "../fsutil.js";
2
2
  import { timelinePath, workspaceDir } from "../paths.js";
3
+ import { changedTrust } from "../trust.js";
3
4
 
4
5
  export async function recordWorkspace(args) {
5
6
  const dir = workspaceDir(args);
6
7
  const actor = required(args.actor, "actor");
7
8
  const summary = required(args.summary || args.change, "summary");
9
+ const now = new Date();
10
+ const requiresReview = args.review === true || args.review === "true";
8
11
  const entry = {
9
- at: new Date().toISOString(),
12
+ at: now.toISOString(),
10
13
  actor,
11
14
  type: args.type || "agent-record",
12
15
  summary,
@@ -14,7 +17,8 @@ export async function recordWorkspace(args) {
14
17
  before: args.before || "",
15
18
  after: args.after || "",
16
19
  evidence: args.evidence || "",
17
- requiresReview: args.review === true || args.review === "true"
20
+ requiresReview,
21
+ trust: changedTrust(now, requiresReview)
18
22
  };
19
23
  await appendJsonLine(timelinePath(dir), entry);
20
24
  console.log(`recorded ${entry.type} by ${actor}`);
package/src/doctor.js CHANGED
@@ -1,4 +1,6 @@
1
- export function diagnose(manifest) {
1
+ import { isStaleTimestamp } from "./trust.js";
2
+
3
+ export function diagnose(manifest, context = {}) {
2
4
  const warnings = [];
3
5
  const hints = manifest.projectHints || {};
4
6
  const runtimes = manifest.runtimes || {};
@@ -34,5 +36,43 @@ export function diagnose(manifest) {
34
36
  message: "Dockerfile detected, but Docker CLI was not found."
35
37
  });
36
38
  }
39
+ if (isStaleTimestamp(manifest.generatedAt)) {
40
+ warnings.push({
41
+ code: "manifest-stale",
42
+ message: "Environment snapshot is older than 24 hours. Run `aienvmp sync` before changing the environment."
43
+ });
44
+ }
45
+ warnings.push(...coordinationWarnings(context.intents || []));
46
+ return warnings;
47
+ }
48
+
49
+ export function coordinationWarnings(intents = []) {
50
+ const warnings = [];
51
+ const byTarget = new Map();
52
+ for (const intent of intents) {
53
+ const target = String(intent.target || inferTarget(intent.action) || "").trim().toLowerCase();
54
+ if (!target) continue;
55
+ const list = byTarget.get(target) || [];
56
+ list.push(intent);
57
+ byTarget.set(target, list);
58
+ }
59
+ for (const [target, list] of byTarget) {
60
+ const actors = new Set(list.map((intent) => intent.actor).filter(Boolean));
61
+ if (list.length > 1 && actors.size > 1) {
62
+ warnings.push({
63
+ code: "conflicting-open-intents",
64
+ target,
65
+ message: `Multiple agents have open environment intents for ${target}. Resolve or coordinate before changing it.`
66
+ });
67
+ }
68
+ }
37
69
  return warnings;
38
70
  }
71
+
72
+ function inferTarget(action = "") {
73
+ const normalized = String(action).toLowerCase();
74
+ for (const target of ["node", "python", "docker", "npm", "pnpm", "yarn", "uv", "pip", "pipx"]) {
75
+ if (normalized.includes(target)) return target;
76
+ }
77
+ return "";
78
+ }
package/src/manifest.js CHANGED
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { commandOutput, commandVersion } from "./shell.js";
5
5
  import { exists } from "./fsutil.js";
6
+ import { observedTrust } from "./trust.js";
6
7
 
7
8
  export async function buildManifest(dir) {
8
9
  const now = new Date().toISOString();
@@ -14,6 +15,7 @@ export async function buildManifest(dir) {
14
15
  name: "aienvmp",
15
16
  command: "aienvmp sync"
16
17
  },
18
+ trust: observedTrust(new Date(now)),
17
19
  workspace: {
18
20
  path: dir,
19
21
  name: path.basename(dir)
@@ -27,9 +29,15 @@ export async function buildManifest(dir) {
27
29
  agentProtocol: {
28
30
  sourceOfTruth: "AIENV.md",
29
31
  preflightCommand: "aienvmp context",
32
+ handoffCommand: "aienvmp handoff",
30
33
  intentCommand: "aienvmp intent --actor <agent:id> --action <planned-change>",
31
34
  recordCommand: "aienvmp record --actor <agent:id> --summary <what-changed>",
32
35
  afterEnvironmentChange: ["aienvmp sync"],
36
+ trustModel: {
37
+ agentWritable: ["observed", "planned", "changed", "review", "stale"],
38
+ verifiedRequires: "human-or-ci",
39
+ rule: "AI agents may report observations and plans, but must not mark environment facts as verified."
40
+ },
33
41
  globalRuntimeChangeRequiresUserApproval: true,
34
42
  globalInstallPolicy: "ask-first",
35
43
  projectLocalChanges: "allowed-when-task-requires"
package/src/render.js CHANGED
@@ -19,6 +19,9 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
19
19
  lines.push(...policyLines(policy));
20
20
  lines.push("- Enforcement: non-blocking by default; warnings require review but do not lock the machine.");
21
21
  lines.push("- Project-local dependency installs: allowed when required by the user task.", "");
22
+ lines.push("## Trust State", "");
23
+ lines.push(`- State: ${manifest.trust?.state || "observed"}`);
24
+ lines.push("- Rule: AI agents may observe, plan, and record changes, but verified requires human or CI review.", "");
22
25
  lines.push("## AI Preflight Summary", "");
23
26
  lines.push(...contextLines(manifest, warnings, intents), "");
24
27
  lines.push("## Runtime Map", "");
@@ -83,6 +86,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
83
86
  "",
84
87
  `Status: ${status}`,
85
88
  `Next: ${next}`,
89
+ `Trust: ${manifest.trust?.state || "observed"} (verified requires human or CI)`,
86
90
  `Workspace: ${manifest.workspace.path}`,
87
91
  `Node: ${manifest.runtimes.node || "not detected"}`,
88
92
  `Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
@@ -116,6 +120,8 @@ export function renderHandoff(handoff) {
116
120
  "# AI Handoff",
117
121
  "",
118
122
  `Status: ${handoff.status}`,
123
+ `Trust: ${handoff.trust?.state || "observed"} (not AI-verified)`,
124
+ `Schema: ${handoff.schemaVersion}`,
119
125
  `Workspace: ${handoff.workspace?.path || "unknown"}`,
120
126
  "",
121
127
  "Safe runtime:",
@@ -208,9 +214,11 @@ const policyHtml=entries(policy).length?\`<table>\${rows(policy)}</table>\`:'<di
208
214
  const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
209
215
  const reviewRequired=warnings.length>0||intents.length>0;
210
216
  const recentChanges=timeline.slice(-8).length;
217
+ const trustState=manifest.trust?.state||'observed';
211
218
  const nextAction=reviewRequired?'Review before environment changes':'Proceed with project-local work';
212
219
  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>\`;
213
- const handoffHtml=\`<table><tr><th>Status</th><td>\${reviewRequired?'review-required':'clear'}</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>\`;
220
+ const driftLabel=warnings.length?'detected':'none';
221
+ 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>\`;
214
222
  document.getElementById('app').innerHTML=\`
215
223
  <header>
216
224
  <div>
@@ -222,9 +230,9 @@ document.getElementById('app').innerHTML=\`
222
230
  </header>
223
231
  <section class="audit" aria-label="Audit summary">
224
232
  \${auditItem('AI decision',reviewRequired?'review required':'can proceed',nextAction,reviewRequired?'review':'primary')}
225
- \${auditItem('Open intents',String(intents.length),intents.length?'Resolve or coordinate before changes':'No pending env changes')}
226
- \${auditItem('Warnings',String(warnings.length),warnings.length?'Policy or drift needs attention':'No warnings detected')}
227
- \${auditItem('Recent changes',String(recentChanges),recentChanges?'Check ledger before continuing':'No recent env ledger entries')}
233
+ \${auditItem('Runtime drift',driftLabel,warnings.length?'Policy, runtime, or coordination warning detected':'No drift warnings detected',warnings.length?'review':'')}
234
+ \${auditItem('Open env changes',String(intents.length),intents.length?'Resolve or coordinate before changes':'No pending env changes')}
235
+ \${auditItem('Trust',trustState,trustState==='verified'?'Human or CI verified':'Machine observed; not AI-verified')}
228
236
  </section>
229
237
  <section class="metrics">
230
238
  <div class="metric"><div class="num">\${entries(manifest.runtimes).length}</div><div class="label">runtimes</div></div>
package/src/trust.js ADDED
@@ -0,0 +1,41 @@
1
+ export const TRUST_STATES = {
2
+ OBSERVED: "observed",
3
+ PLANNED: "planned",
4
+ CHANGED: "changed",
5
+ REVIEW: "review",
6
+ VERIFIED: "verified",
7
+ STALE: "stale"
8
+ };
9
+
10
+ export function observedTrust(now = new Date()) {
11
+ return {
12
+ state: TRUST_STATES.OBSERVED,
13
+ at: now.toISOString(),
14
+ by: "aienvmp scan",
15
+ verified: false,
16
+ note: "Machine-observed only. AI agents cannot mark environment facts as verified."
17
+ };
18
+ }
19
+
20
+ export function plannedTrust(now = new Date()) {
21
+ return {
22
+ state: TRUST_STATES.PLANNED,
23
+ at: now.toISOString(),
24
+ verified: false
25
+ };
26
+ }
27
+
28
+ export function changedTrust(now = new Date(), requiresReview = false) {
29
+ return {
30
+ state: requiresReview ? TRUST_STATES.REVIEW : TRUST_STATES.CHANGED,
31
+ at: now.toISOString(),
32
+ verified: false
33
+ };
34
+ }
35
+
36
+ export function isStaleTimestamp(value, now = new Date(), maxAgeHours = 24) {
37
+ if (!value) return false;
38
+ const then = new Date(value).getTime();
39
+ if (!Number.isFinite(then)) return false;
40
+ return now.getTime() - then > maxAgeHours * 60 * 60 * 1000;
41
+ }