aienvmp 0.1.4 → 0.1.6

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.6
4
+
5
+ - Added optional `sync --deep` / `scan --deep` read-only global tool inventory.
6
+ - Kept the default scan lightweight while exposing deep inventory summaries to `AIENV.md`, `context`, `handoff`, and the dashboard.
7
+ - Added parsers for npm global packages, pipx tools, uv tools, and Homebrew package versions.
8
+
9
+ ## 0.1.5
10
+
11
+ - Added machine-readable trust states for observed, planned, changed, review, verified, and stale environment facts.
12
+ - Added multi-agent intent conflict warnings for shared runtime/package manager targets.
13
+ - Added trust and schema context to `context`, `handoff`, `doctor`, `AIENV.md`, and the dashboard.
14
+ - Repositioned the docs around AI environment coordination instead of general AI project memory.
15
+
3
16
  ## 0.1.4
4
17
 
5
18
  - Added `aienvmp handoff` for next-agent environment handoff summaries.
package/README.md CHANGED
@@ -6,22 +6,24 @@
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
21
+ ```
22
+
23
+ Optional deeper read-only inventory:
24
+
25
+ ```bash
26
+ npx aienvmp sync --deep
25
27
  ```
26
28
 
27
29
  ## Output
@@ -34,6 +36,10 @@ AIENV.md
34
36
  .aienvmp/dashboard.html
35
37
  ```
36
38
 
39
+ Trust states are machine-readable: `observed`, `planned`, `changed`, `review`, `verified`, `stale`.
40
+
41
+ AI agents can observe, plan, and record. Only a human or CI should mark environment facts as verified.
42
+
37
43
  ## For AGENTS.md
38
44
 
39
45
  `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
 
@@ -17,12 +17,11 @@
17
17
  - pyenv, uv, conda
18
18
  - mise, asdf
19
19
  - Global tool inventory:
20
- - `npm -g`
21
- - `pipx list`
22
- - `uv tool list`
20
+ - richer summaries for `npm -g`, `pipx list`, `uv tool list`, and Homebrew
21
+ - optional `--deep` scanners for more toolchains
23
22
  - Conflict detection:
24
- - multiple open intents for the same target
25
23
  - stale unresolved intents
24
+ - recent runtime changes without a fresh handoff
26
25
  - package manager policy vs lockfile mismatch
27
26
  - CI mode:
28
27
  - 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.6",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -74,7 +74,7 @@ function printUsage() {
74
74
  console.log(`aienvmp - AI-first env map + lightweight runtime SBOM
75
75
 
76
76
  Usage:
77
- aienvmp sync [--dir .] [--json] [--quiet]
77
+ aienvmp sync [--dir .] [--json] [--quiet] [--deep]
78
78
  aienvmp context [--dir .] [--json]
79
79
  aienvmp handoff [--dir .] [--json]
80
80
 
@@ -87,7 +87,7 @@ Common:
87
87
 
88
88
  Advanced:
89
89
  aienvmp init [--dir .]
90
- aienvmp scan [--dir .]
90
+ aienvmp scan [--dir .] [--deep]
91
91
  aienvmp intent [--dir .] --actor agent:codex --action "install pnpm"
92
92
  aienvmp resolve [--dir .] --actor human:you --id <intent-id> [--status resolved|cancelled]
93
93
  aienvmp record [--dir .] --actor agent:codex --summary "updated .nvmrc" [--target node] [--before 20] [--after 24]
@@ -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,17 +12,19 @@ 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,
24
25
  packageManagers: manifest.packageManagers,
25
26
  containers: manifest.containers,
27
+ inventory: inventorySummary(manifest.inventory),
26
28
  projectHints: manifest.projectHints,
27
29
  warnings,
28
30
  policy,
@@ -35,6 +37,15 @@ export async function contextWorkspace(args) {
35
37
  console.log(renderContext(manifest, timeline, warnings, intents, policy));
36
38
  }
37
39
 
40
+ function inventorySummary(inventory = {}) {
41
+ const tools = inventory.tools || {};
42
+ return {
43
+ mode: inventory.mode || "basic",
44
+ enabled: inventory.enabled === true,
45
+ groups: Object.fromEntries(Object.entries(tools).map(([name, items]) => [name, items.length]))
46
+ };
47
+ }
48
+
38
49
  function contextDecision(warnings, intents) {
39
50
  const reviewRequired = warnings.length > 0 || intents.length > 0;
40
51
  return {
@@ -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,12 +27,15 @@ 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",
33
35
  python: manifest.runtimes?.python || manifest.runtimes?.python3 || "not detected",
34
36
  docker: manifest.containers?.docker ? "available" : "not detected"
35
37
  },
38
+ inventory: inventorySummary(manifest.inventory),
36
39
  policy: {
37
40
  node: policy.node || "not set",
38
41
  python: policy.python || "not set",
@@ -53,3 +56,12 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
53
56
  : "continue with project-local work; record intent before environment changes"
54
57
  };
55
58
  }
59
+
60
+ function inventorySummary(inventory = {}) {
61
+ const tools = inventory.tools || {};
62
+ return {
63
+ mode: inventory.mode || "basic",
64
+ enabled: inventory.enabled === true,
65
+ groups: Object.fromEntries(Object.entries(tools).map(([name, items]) => [name, items.length]))
66
+ };
67
+ }
@@ -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}`);
@@ -11,7 +11,7 @@ export async function scanWorkspace(args) {
11
11
  if (previous && await exists(currentPath)) {
12
12
  await fs.copyFile(currentPath, previousManifestPath(dir));
13
13
  }
14
- const manifest = await buildManifest(dir);
14
+ const manifest = await buildManifest(dir, { deep: args.deep });
15
15
  await writeJson(currentPath, manifest);
16
16
  const changes = diffManifests(previous, manifest);
17
17
  for (const change of changes) {
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
+ }
@@ -0,0 +1,88 @@
1
+ import { commandOutput } from "./shell.js";
2
+
3
+ const MAX_ITEMS = 40;
4
+
5
+ export async function scanGlobalInventory(options = {}) {
6
+ if (!options.deep) {
7
+ return {
8
+ mode: "basic",
9
+ enabled: false,
10
+ note: "Run `aienvmp sync --deep` to collect read-only global tool inventory."
11
+ };
12
+ }
13
+
14
+ return {
15
+ mode: "deep",
16
+ enabled: true,
17
+ note: "Read-only global tool inventory. Use for AI awareness, not enforcement.",
18
+ tools: compact({
19
+ npmGlobal: await scanNpmGlobal(),
20
+ pipx: await scanPipx(),
21
+ uvTools: await scanUvTools(),
22
+ brew: await scanBrew()
23
+ })
24
+ };
25
+ }
26
+
27
+ export async function scanNpmGlobal() {
28
+ const command = process.platform === "win32" ? "npm.cmd" : "npm";
29
+ const raw = await commandOutput(command, ["list", "-g", "--depth=0", "--json"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
30
+ return parseNpmGlobal(raw);
31
+ }
32
+
33
+ export async function scanPipx() {
34
+ const raw = await commandOutput("pipx", ["list", "--json"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
35
+ return parsePipx(raw);
36
+ }
37
+
38
+ export async function scanUvTools() {
39
+ const raw = await commandOutput("uv", ["tool", "list"], { timeout: 5000 });
40
+ return parseNameVersionLines(raw);
41
+ }
42
+
43
+ export async function scanBrew() {
44
+ if (process.platform === "win32") return [];
45
+ const raw = await commandOutput("brew", ["list", "--versions"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
46
+ return parseNameVersionLines(raw);
47
+ }
48
+
49
+ export function parseNpmGlobal(raw) {
50
+ try {
51
+ const parsed = JSON.parse(raw);
52
+ return Object.entries(parsed.dependencies || {})
53
+ .slice(0, MAX_ITEMS)
54
+ .map(([name, value]) => ({ name, version: value.version || "unknown" }));
55
+ } catch {
56
+ return [];
57
+ }
58
+ }
59
+
60
+ export function parsePipx(raw) {
61
+ try {
62
+ const parsed = JSON.parse(raw);
63
+ return Object.entries(parsed.venvs || {})
64
+ .slice(0, MAX_ITEMS)
65
+ .map(([name, value]) => ({
66
+ name,
67
+ version: value.metadata?.main_package?.package_version || "unknown"
68
+ }));
69
+ } catch {
70
+ return [];
71
+ }
72
+ }
73
+
74
+ export function parseNameVersionLines(raw) {
75
+ return String(raw || "")
76
+ .split(/\r?\n/)
77
+ .map((line) => line.trim())
78
+ .filter(Boolean)
79
+ .slice(0, MAX_ITEMS)
80
+ .map((line) => {
81
+ const [name, ...rest] = line.split(/\s+/);
82
+ return { name, version: rest.join(" ") || "unknown" };
83
+ });
84
+ }
85
+
86
+ function compact(obj) {
87
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => Array.isArray(value) && value.length));
88
+ }
package/src/manifest.js CHANGED
@@ -3,8 +3,10 @@ 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";
7
+ import { scanGlobalInventory } from "./inventory.js";
6
8
 
7
- export async function buildManifest(dir) {
9
+ export async function buildManifest(dir, options = {}) {
8
10
  const now = new Date().toISOString();
9
11
  const manifest = {
10
12
  schemaName: "aienvmp.runtime-sbom",
@@ -12,8 +14,9 @@ export async function buildManifest(dir) {
12
14
  generatedAt: now,
13
15
  generatedBy: {
14
16
  name: "aienvmp",
15
- command: "aienvmp sync"
17
+ command: options.deep ? "aienvmp sync --deep" : "aienvmp sync"
16
18
  },
19
+ trust: observedTrust(new Date(now)),
17
20
  workspace: {
18
21
  path: dir,
19
22
  name: path.basename(dir)
@@ -23,13 +26,20 @@ export async function buildManifest(dir) {
23
26
  packageManagers: await scanPackageManagers(),
24
27
  containers: await scanContainers(),
25
28
  projectHints: await scanProjectHints(dir),
29
+ inventory: await scanGlobalInventory({ deep: options.deep }),
26
30
  agentFiles: await scanAgentFiles(dir),
27
31
  agentProtocol: {
28
32
  sourceOfTruth: "AIENV.md",
29
33
  preflightCommand: "aienvmp context",
34
+ handoffCommand: "aienvmp handoff",
30
35
  intentCommand: "aienvmp intent --actor <agent:id> --action <planned-change>",
31
36
  recordCommand: "aienvmp record --actor <agent:id> --summary <what-changed>",
32
37
  afterEnvironmentChange: ["aienvmp sync"],
38
+ trustModel: {
39
+ agentWritable: ["observed", "planned", "changed", "review", "stale"],
40
+ verifiedRequires: "human-or-ci",
41
+ rule: "AI agents may report observations and plans, but must not mark environment facts as verified."
42
+ },
33
43
  globalRuntimeChangeRequiresUserApproval: true,
34
44
  globalInstallPolicy: "ask-first",
35
45
  projectLocalChanges: "allowed-when-task-requires"
@@ -61,6 +71,13 @@ export async function buildManifest(dir) {
61
71
  docker: "docker --version",
62
72
  compose: "docker compose version or docker-compose --version"
63
73
  },
74
+ globalInventory: {
75
+ mode: "basic by default; deep only when requested",
76
+ npmGlobal: "npm list -g --depth=0 --json",
77
+ pipx: "pipx list --json",
78
+ uvTools: "uv tool list",
79
+ brew: "brew list --versions"
80
+ },
64
81
  projectHints: [
65
82
  ".nvmrc",
66
83
  ".python-version",
package/src/render.js CHANGED
@@ -19,12 +19,17 @@ 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", "");
25
28
  pushMap(lines, "Runtimes", manifest.runtimes);
26
29
  pushMap(lines, "Package Managers", manifest.packageManagers);
27
30
  pushMap(lines, "Containers", manifest.containers);
31
+ lines.push("## Global Tool Inventory", "");
32
+ lines.push(...inventoryLines(manifest.inventory), "");
28
33
  lines.push("## Project Requirements And Hints", "");
29
34
  pushMap(lines, "Detected", manifest.projectHints);
30
35
  lines.push("## Drift And Warnings", "");
@@ -83,10 +88,12 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
83
88
  "",
84
89
  `Status: ${status}`,
85
90
  `Next: ${next}`,
91
+ `Trust: ${manifest.trust?.state || "observed"} (verified requires human or CI)`,
86
92
  `Workspace: ${manifest.workspace.path}`,
87
93
  `Node: ${manifest.runtimes.node || "not detected"}`,
88
94
  `Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
89
95
  `Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
96
+ `Inventory: ${manifest.inventory?.mode || "basic"}${manifest.inventory?.enabled ? " enabled" : " disabled"}`,
90
97
  `Policy Node: ${policy.node || "not set"}`,
91
98
  `Policy Python: ${policy.python || "not set"}`,
92
99
  `Policy Package Manager: ${policy.packageManager || "not set"}`,
@@ -116,12 +123,15 @@ export function renderHandoff(handoff) {
116
123
  "# AI Handoff",
117
124
  "",
118
125
  `Status: ${handoff.status}`,
126
+ `Trust: ${handoff.trust?.state || "observed"} (not AI-verified)`,
127
+ `Schema: ${handoff.schemaVersion}`,
119
128
  `Workspace: ${handoff.workspace?.path || "unknown"}`,
120
129
  "",
121
130
  "Safe runtime:",
122
131
  `- Node: ${handoff.safeRuntime.node}`,
123
132
  `- Python: ${handoff.safeRuntime.python}`,
124
133
  `- Docker: ${handoff.safeRuntime.docker}`,
134
+ `- Inventory: ${handoff.inventory?.mode || "basic"}${handoff.inventory?.enabled ? " enabled" : " disabled"}`,
125
135
  "",
126
136
  "Open intents:",
127
137
  ...(handoff.openIntents.length ? handoff.openIntents.map((i) => `- ${i.actor}: ${i.action}${i.target ? ` (${i.target})` : ""}`) : ["- none"]),
@@ -197,6 +207,9 @@ const {manifest,timeline,warnings,intents,policy}=JSON.parse(document.getElement
197
207
  function esc(s){return String(s).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('"','&quot;')}
198
208
  const entries=o=>Object.entries(o||{});
199
209
  const rows=o=>entries(o).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${esc(String(v))}</code></td></tr>\`).join('')||'<tr><td colspan="2">None detected</td></tr>';
210
+ const inventoryGroups=manifest.inventory?.tools||{};
211
+ const inventoryCount=Object.values(inventoryGroups).reduce((sum,items)=>sum+(Array.isArray(items)?items.length:0),0);
212
+ const inventoryHtml=manifest.inventory?.enabled?('<table>'+Object.entries(inventoryGroups).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${Array.isArray(v)?v.length:0} tools</code></td></tr>\`).join('')+'</table>'):'<div class="okline">Deep global inventory is off. Run <code>aienvmp sync --deep</code> when an AI needs global tool awareness.</div>';
200
213
  const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
201
214
  const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
202
215
  const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
@@ -208,9 +221,11 @@ const policyHtml=entries(policy).length?\`<table>\${rows(policy)}</table>\`:'<di
208
221
  const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
209
222
  const reviewRequired=warnings.length>0||intents.length>0;
210
223
  const recentChanges=timeline.slice(-8).length;
224
+ const trustState=manifest.trust?.state||'observed';
211
225
  const nextAction=reviewRequired?'Review before environment changes':'Proceed with project-local work';
212
226
  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>\`;
227
+ const driftLabel=warnings.length?'detected':'none';
228
+ 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
229
  document.getElementById('app').innerHTML=\`
215
230
  <header>
216
231
  <div>
@@ -222,9 +237,9 @@ document.getElementById('app').innerHTML=\`
222
237
  </header>
223
238
  <section class="audit" aria-label="Audit summary">
224
239
  \${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')}
240
+ \${auditItem('Runtime drift',driftLabel,warnings.length?'Policy, runtime, or coordination warning detected':'No drift warnings detected',warnings.length?'review':'')}
241
+ \${auditItem('Open env changes',String(intents.length),intents.length?'Resolve or coordinate before changes':'No pending env changes')}
242
+ \${auditItem('Trust',trustState,trustState==='verified'?'Human or CI verified':'Machine observed; not AI-verified')}
228
243
  </section>
229
244
  <section class="metrics">
230
245
  <div class="metric"><div class="num">\${entries(manifest.runtimes).length}</div><div class="label">runtimes</div></div>
@@ -238,6 +253,7 @@ document.getElementById('app').innerHTML=\`
238
253
  \${card('Package Managers',\`<span class="pill">\${entries(manifest.packageManagers).length} found</span>\`,\`<table>\${rows(manifest.packageManagers)}</table>\`)}
239
254
  \${card('Containers',manifest.containers?.docker?'<span class="pill">available</span>':'<span class="pill off">not detected</span>',\`<table>\${rows(manifest.containers)}</table>\`)}
240
255
  \${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
256
+ \${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
241
257
  </div>
242
258
  <aside>
243
259
  \${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
@@ -296,6 +312,17 @@ function contextLines(manifest, warnings, intents) {
296
312
  ];
297
313
  }
298
314
 
315
+ function inventoryLines(inventory = {}) {
316
+ if (!inventory.enabled) return ["- Mode: basic", "- Deep global inventory is disabled. Run `aienvmp sync --deep` when needed."];
317
+ const groups = Object.entries(inventory.tools || {});
318
+ if (!groups.length) return ["- Mode: deep", "- No global tools detected by optional scanners."];
319
+ const lines = ["- Mode: deep"];
320
+ for (const [name, items] of groups) {
321
+ lines.push(`- ${name}: ${items.length} tools`);
322
+ }
323
+ return lines;
324
+ }
325
+
299
326
  function policyLines(policy) {
300
327
  const lines = [];
301
328
  if (policy.node) lines.push(`- Node version policy: ${policy.node}`);
package/src/shell.js CHANGED
@@ -15,10 +15,11 @@ export async function commandVersion(command, args = ["--version"]) {
15
15
  }
16
16
  }
17
17
 
18
- export async function commandOutput(command, args = []) {
18
+ export async function commandOutput(command, args = [], options = {}) {
19
19
  try {
20
20
  const { stdout } = await execFileAsync(command, args, {
21
- timeout: 2500,
21
+ timeout: options.timeout || 2500,
22
+ maxBuffer: options.maxBuffer || 1024 * 1024,
22
23
  windowsHide: true
23
24
  });
24
25
  return stdout.trim();
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
+ }