aienvmp 0.1.5 → 0.1.7
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 +12 -0
- package/README.md +9 -1
- package/ROADMAP.md +4 -5
- package/package.json +1 -1
- package/src/cli.js +3 -3
- package/src/commands/context.js +10 -0
- package/src/commands/handoff.js +28 -1
- package/src/commands/scan.js +1 -1
- package/src/doctor.js +47 -0
- package/src/inventory.js +88 -0
- package/src/manifest.js +13 -4
- package/src/render.js +26 -4
- package/src/shell.js +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.7
|
|
4
|
+
|
|
5
|
+
- Added stale open intent warnings for long-running environment change plans.
|
|
6
|
+
- Added stale handoff warnings when environment changes happen after the last recorded AI handoff.
|
|
7
|
+
- Added optional `aienvmp handoff --record --actor <agent:id>` timeline entries.
|
|
8
|
+
|
|
9
|
+
## 0.1.6
|
|
10
|
+
|
|
11
|
+
- Added optional `sync --deep` / `scan --deep` read-only global tool inventory.
|
|
12
|
+
- Kept the default scan lightweight while exposing deep inventory summaries to `AIENV.md`, `context`, `handoff`, and the dashboard.
|
|
13
|
+
- Added parsers for npm global packages, pipx tools, uv tools, and Homebrew package versions.
|
|
14
|
+
|
|
3
15
|
## 0.1.5
|
|
4
16
|
|
|
5
17
|
- Added machine-readable trust states for observed, planned, changed, review, verified, and stale environment facts.
|
package/README.md
CHANGED
|
@@ -18,6 +18,13 @@ Core loop: scan the env, give AI a preflight context, and hand off safe next ste
|
|
|
18
18
|
npx aienvmp sync
|
|
19
19
|
npx aienvmp context
|
|
20
20
|
npx aienvmp handoff
|
|
21
|
+
npx aienvmp handoff --record --actor agent:codex
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Optional deeper read-only inventory:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx aienvmp sync --deep
|
|
21
28
|
```
|
|
22
29
|
|
|
23
30
|
## Output
|
|
@@ -59,7 +66,8 @@ aienvmp doctor --ci # strict CI check
|
|
|
59
66
|
- simple by default
|
|
60
67
|
- AI-first
|
|
61
68
|
- lightweight
|
|
62
|
-
-
|
|
69
|
+
- one advisory engine, optional enforcement with `doctor --ci`
|
|
70
|
+
- non-blocking unless strict mode is explicitly requested
|
|
63
71
|
|
|
64
72
|
## Development
|
|
65
73
|
|
package/ROADMAP.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- Strengthen trust states: observed, planned, changed, review, verified, stale
|
|
8
8
|
- Detect multi-agent environment intent conflicts
|
|
9
|
+
- Keep one advisory decision engine with optional strict enforcement
|
|
9
10
|
- Stabilize `.aienvmp/manifest.json` and JSON command schemas
|
|
10
11
|
- Keep `sync`, `context`, and `handoff` as the simple core flow
|
|
11
12
|
- Improve the dashboard for 10-second human review
|
|
@@ -17,13 +18,11 @@
|
|
|
17
18
|
- pyenv, uv, conda
|
|
18
19
|
- mise, asdf
|
|
19
20
|
- Global tool inventory:
|
|
20
|
-
- `npm -g`
|
|
21
|
-
- `
|
|
22
|
-
- `uv tool list`
|
|
21
|
+
- richer summaries for `npm -g`, `pipx list`, `uv tool list`, and Homebrew
|
|
22
|
+
- optional `--deep` scanners for more toolchains
|
|
23
23
|
- Conflict detection:
|
|
24
|
-
- stale unresolved intents
|
|
25
|
-
- recent runtime changes without a fresh handoff
|
|
26
24
|
- package manager policy vs lockfile mismatch
|
|
25
|
+
- monorepo/project boundary aware intent targets
|
|
27
26
|
- CI mode:
|
|
28
27
|
- stable exit codes
|
|
29
28
|
- GitHub Action example
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -74,9 +74,9 @@ 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
|
-
aienvmp handoff [--dir .] [--json]
|
|
79
|
+
aienvmp handoff [--dir .] [--json] [--record --actor agent:id]
|
|
80
80
|
|
|
81
81
|
Common:
|
|
82
82
|
aienvmp sync update AIENV.md, manifest, ledger, intents, and dashboard
|
|
@@ -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]
|
package/src/commands/context.js
CHANGED
|
@@ -24,6 +24,7 @@ export async function contextWorkspace(args) {
|
|
|
24
24
|
runtimes: manifest.runtimes,
|
|
25
25
|
packageManagers: manifest.packageManagers,
|
|
26
26
|
containers: manifest.containers,
|
|
27
|
+
inventory: inventorySummary(manifest.inventory),
|
|
27
28
|
projectHints: manifest.projectHints,
|
|
28
29
|
warnings,
|
|
29
30
|
policy,
|
|
@@ -36,6 +37,15 @@ export async function contextWorkspace(args) {
|
|
|
36
37
|
console.log(renderContext(manifest, timeline, warnings, intents, policy));
|
|
37
38
|
}
|
|
38
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
|
+
|
|
39
49
|
function contextDecision(warnings, intents) {
|
|
40
50
|
const reviewRequired = warnings.length > 0 || intents.length > 0;
|
|
41
51
|
return {
|
package/src/commands/handoff.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { diagnose } from "../doctor.js";
|
|
2
|
-
import { readJson } from "../fsutil.js";
|
|
2
|
+
import { appendJsonLine, readJson } from "../fsutil.js";
|
|
3
3
|
import { loadPolicy, policyWarnings } from "../policy.js";
|
|
4
4
|
import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
|
|
5
5
|
import { renderHandoff } from "../render.js";
|
|
6
6
|
import { openIntents, readJsonl, readTimeline } from "../timeline.js";
|
|
7
|
+
import { changedTrust } from "../trust.js";
|
|
7
8
|
|
|
8
9
|
export async function handoffWorkspace(args) {
|
|
9
10
|
const dir = workspaceDir(args);
|
|
@@ -14,6 +15,9 @@ export async function handoffWorkspace(args) {
|
|
|
14
15
|
const policy = await loadPolicy(dir);
|
|
15
16
|
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
16
17
|
const handoff = buildHandoff(manifest, timeline, warnings, intents, policy);
|
|
18
|
+
if (args.record) {
|
|
19
|
+
await recordHandoff(timelinePath(dir), handoff, args.actor || "agent:unknown");
|
|
20
|
+
}
|
|
17
21
|
|
|
18
22
|
if (args.json) {
|
|
19
23
|
console.log(JSON.stringify(handoff, null, 2));
|
|
@@ -23,6 +27,19 @@ export async function handoffWorkspace(args) {
|
|
|
23
27
|
return handoff;
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
async function recordHandoff(file, handoff, actor) {
|
|
31
|
+
const now = new Date();
|
|
32
|
+
await appendJsonLine(file, {
|
|
33
|
+
at: now.toISOString(),
|
|
34
|
+
actor,
|
|
35
|
+
type: "agent-handoff",
|
|
36
|
+
summary: `handoff ${handoff.status}`,
|
|
37
|
+
status: handoff.status,
|
|
38
|
+
warnings: handoff.warnings.map((warning) => warning.code),
|
|
39
|
+
trust: changedTrust(now, handoff.status !== "clear")
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
export function buildHandoff(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
|
|
27
44
|
const reviewRequired = warnings.length > 0 || intents.length > 0;
|
|
28
45
|
return {
|
|
@@ -35,6 +52,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
35
52
|
python: manifest.runtimes?.python || manifest.runtimes?.python3 || "not detected",
|
|
36
53
|
docker: manifest.containers?.docker ? "available" : "not detected"
|
|
37
54
|
},
|
|
55
|
+
inventory: inventorySummary(manifest.inventory),
|
|
38
56
|
policy: {
|
|
39
57
|
node: policy.node || "not set",
|
|
40
58
|
python: policy.python || "not set",
|
|
@@ -55,3 +73,12 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
55
73
|
: "continue with project-local work; record intent before environment changes"
|
|
56
74
|
};
|
|
57
75
|
}
|
|
76
|
+
|
|
77
|
+
function inventorySummary(inventory = {}) {
|
|
78
|
+
const tools = inventory.tools || {};
|
|
79
|
+
return {
|
|
80
|
+
mode: inventory.mode || "basic",
|
|
81
|
+
enabled: inventory.enabled === true,
|
|
82
|
+
groups: Object.fromEntries(Object.entries(tools).map(([name, items]) => [name, items.length]))
|
|
83
|
+
};
|
|
84
|
+
}
|
package/src/commands/scan.js
CHANGED
|
@@ -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
|
@@ -43,6 +43,8 @@ export function diagnose(manifest, context = {}) {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
warnings.push(...coordinationWarnings(context.intents || []));
|
|
46
|
+
warnings.push(...staleIntentWarnings(context.intents || []));
|
|
47
|
+
warnings.push(...handoffWarnings(context.timeline || []));
|
|
46
48
|
return warnings;
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -69,6 +71,30 @@ export function coordinationWarnings(intents = []) {
|
|
|
69
71
|
return warnings;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
export function staleIntentWarnings(intents = [], now = new Date(), maxAgeHours = 4) {
|
|
75
|
+
const warnings = [];
|
|
76
|
+
for (const intent of intents) {
|
|
77
|
+
if (!isStaleTimestamp(intent.at, now, maxAgeHours)) continue;
|
|
78
|
+
warnings.push({
|
|
79
|
+
code: "stale-open-intent",
|
|
80
|
+
target: intent.target || inferTarget(intent.action) || "",
|
|
81
|
+
message: `Open intent ${intent.id || ""} from ${intent.actor || "unknown"} is older than ${maxAgeHours} hours. Resolve it or confirm it before environment changes.`.replace(/\s+/g, " ").trim()
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return warnings;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function handoffWarnings(timeline = []) {
|
|
88
|
+
const lastEnvChange = [...timeline].reverse().find(isEnvironmentChange);
|
|
89
|
+
if (!lastEnvChange) return [];
|
|
90
|
+
const lastHandoff = [...timeline].reverse().find((item) => item.type === "agent-handoff");
|
|
91
|
+
if (lastHandoff && new Date(lastHandoff.at).getTime() >= new Date(lastEnvChange.at).getTime()) return [];
|
|
92
|
+
return [{
|
|
93
|
+
code: "handoff-stale",
|
|
94
|
+
message: "Environment changes were recorded after the last AI handoff. Run `aienvmp handoff --record --actor agent:id` before the next agent continues."
|
|
95
|
+
}];
|
|
96
|
+
}
|
|
97
|
+
|
|
72
98
|
function inferTarget(action = "") {
|
|
73
99
|
const normalized = String(action).toLowerCase();
|
|
74
100
|
for (const target of ["node", "python", "docker", "npm", "pnpm", "yarn", "uv", "pip", "pipx"]) {
|
|
@@ -76,3 +102,24 @@ function inferTarget(action = "") {
|
|
|
76
102
|
}
|
|
77
103
|
return "";
|
|
78
104
|
}
|
|
105
|
+
|
|
106
|
+
function isEnvironmentChange(item = {}) {
|
|
107
|
+
if (["runtime", "package-manager", "container"].includes(item.change?.scope)) return true;
|
|
108
|
+
if (item.type === "detected-change") return false;
|
|
109
|
+
const text = `${item.type || ""} ${item.target || ""} ${item.summary || ""} ${item.action || ""} ${item.change?.scope || ""} ${item.change?.key || ""}`.toLowerCase();
|
|
110
|
+
return [
|
|
111
|
+
"runtime",
|
|
112
|
+
"node",
|
|
113
|
+
"python",
|
|
114
|
+
"docker",
|
|
115
|
+
"package manager",
|
|
116
|
+
"package-manager",
|
|
117
|
+
"npm",
|
|
118
|
+
"pnpm",
|
|
119
|
+
"yarn",
|
|
120
|
+
"uv",
|
|
121
|
+
"pip",
|
|
122
|
+
"pipx",
|
|
123
|
+
"global"
|
|
124
|
+
].some((token) => text.includes(token));
|
|
125
|
+
}
|
package/src/inventory.js
ADDED
|
@@ -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
|
@@ -4,8 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { commandOutput, commandVersion } from "./shell.js";
|
|
5
5
|
import { exists } from "./fsutil.js";
|
|
6
6
|
import { observedTrust } from "./trust.js";
|
|
7
|
+
import { scanGlobalInventory } from "./inventory.js";
|
|
7
8
|
|
|
8
|
-
export async function buildManifest(dir) {
|
|
9
|
+
export async function buildManifest(dir, options = {}) {
|
|
9
10
|
const now = new Date().toISOString();
|
|
10
11
|
const manifest = {
|
|
11
12
|
schemaName: "aienvmp.runtime-sbom",
|
|
@@ -13,7 +14,7 @@ export async function buildManifest(dir) {
|
|
|
13
14
|
generatedAt: now,
|
|
14
15
|
generatedBy: {
|
|
15
16
|
name: "aienvmp",
|
|
16
|
-
command: "aienvmp sync"
|
|
17
|
+
command: options.deep ? "aienvmp sync --deep" : "aienvmp sync"
|
|
17
18
|
},
|
|
18
19
|
trust: observedTrust(new Date(now)),
|
|
19
20
|
workspace: {
|
|
@@ -25,13 +26,14 @@ export async function buildManifest(dir) {
|
|
|
25
26
|
packageManagers: await scanPackageManagers(),
|
|
26
27
|
containers: await scanContainers(),
|
|
27
28
|
projectHints: await scanProjectHints(dir),
|
|
29
|
+
inventory: await scanGlobalInventory({ deep: options.deep }),
|
|
28
30
|
agentFiles: await scanAgentFiles(dir),
|
|
29
31
|
agentProtocol: {
|
|
30
32
|
sourceOfTruth: "AIENV.md",
|
|
31
33
|
preflightCommand: "aienvmp context",
|
|
32
34
|
handoffCommand: "aienvmp handoff",
|
|
33
|
-
intentCommand: "aienvmp intent --actor
|
|
34
|
-
recordCommand: "aienvmp record --actor
|
|
35
|
+
intentCommand: "aienvmp intent --actor agent:id --action planned-change",
|
|
36
|
+
recordCommand: "aienvmp record --actor agent:id --summary what-changed",
|
|
35
37
|
afterEnvironmentChange: ["aienvmp sync"],
|
|
36
38
|
trustModel: {
|
|
37
39
|
agentWritable: ["observed", "planned", "changed", "review", "stale"],
|
|
@@ -69,6 +71,13 @@ export async function buildManifest(dir) {
|
|
|
69
71
|
docker: "docker --version",
|
|
70
72
|
compose: "docker compose version or docker-compose --version"
|
|
71
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
|
+
},
|
|
72
81
|
projectHints: [
|
|
73
82
|
".nvmrc",
|
|
74
83
|
".python-version",
|
package/src/render.js
CHANGED
|
@@ -12,9 +12,10 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
12
12
|
lines.push("1. Run `aienvmp context`.");
|
|
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
|
-
lines.push("4. Record planned environment changes with `aienvmp intent --actor
|
|
15
|
+
lines.push("4. Record planned environment changes with `aienvmp intent --actor agent:id --action planned-change`.");
|
|
16
16
|
lines.push("5. After environment changes, run `aienvmp sync`.");
|
|
17
|
-
lines.push("6. Record what changed with `aienvmp record --actor
|
|
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`.", "");
|
|
18
19
|
lines.push("## Current Policy", "");
|
|
19
20
|
lines.push(...policyLines(policy));
|
|
20
21
|
lines.push("- Enforcement: non-blocking by default; warnings require review but do not lock the machine.");
|
|
@@ -28,6 +29,8 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
|
|
|
28
29
|
pushMap(lines, "Runtimes", manifest.runtimes);
|
|
29
30
|
pushMap(lines, "Package Managers", manifest.packageManagers);
|
|
30
31
|
pushMap(lines, "Containers", manifest.containers);
|
|
32
|
+
lines.push("## Global Tool Inventory", "");
|
|
33
|
+
lines.push(...inventoryLines(manifest.inventory), "");
|
|
31
34
|
lines.push("## Project Requirements And Hints", "");
|
|
32
35
|
pushMap(lines, "Detected", manifest.projectHints);
|
|
33
36
|
lines.push("## Drift And Warnings", "");
|
|
@@ -74,6 +77,7 @@ Before changing runtimes, package managers, Docker settings, global packages, or
|
|
|
74
77
|
4. Record planned environment changes with \`aienvmp intent\`.
|
|
75
78
|
5. After environment changes, run \`aienvmp sync\`.
|
|
76
79
|
6. Record what changed with \`aienvmp record\`.
|
|
80
|
+
7. At handoff, run \`aienvmp handoff --record --actor agent:id\`.
|
|
77
81
|
|
|
78
82
|
\`aienvmp\` does not replace this instruction file. It provides the live env map, lightweight runtime SBOM, intent log, timeline, and dashboard.`;
|
|
79
83
|
}
|
|
@@ -91,6 +95,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
91
95
|
`Node: ${manifest.runtimes.node || "not detected"}`,
|
|
92
96
|
`Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
|
|
93
97
|
`Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
|
|
98
|
+
`Inventory: ${manifest.inventory?.mode || "basic"}${manifest.inventory?.enabled ? " enabled" : " disabled"}`,
|
|
94
99
|
`Policy Node: ${policy.node || "not set"}`,
|
|
95
100
|
`Policy Python: ${policy.python || "not set"}`,
|
|
96
101
|
`Policy Package Manager: ${policy.packageManager || "not set"}`,
|
|
@@ -99,9 +104,10 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
99
104
|
"- Ask the user before global runtime, package manager, Docker, or global package changes.",
|
|
100
105
|
"- Treat policy mismatches as review-required, not as permission to break ongoing operations.",
|
|
101
106
|
"- Prefer project-local version files and local environments.",
|
|
102
|
-
"- Before planned env changes, run `aienvmp intent --actor
|
|
107
|
+
"- Before planned env changes, run `aienvmp intent --actor agent:id --action planned-change`.",
|
|
103
108
|
"- After env changes, run `aienvmp sync`.",
|
|
104
|
-
"- Then run `aienvmp record --actor
|
|
109
|
+
"- Then run `aienvmp record --actor agent:id --summary what-changed`.",
|
|
110
|
+
"- Before handing work to another AI, run `aienvmp handoff --record --actor agent:id`.",
|
|
105
111
|
"",
|
|
106
112
|
"Warnings:",
|
|
107
113
|
...(warnings.length ? warnings.map((w) => `- ${w.message}`) : ["- none"]),
|
|
@@ -128,6 +134,7 @@ export function renderHandoff(handoff) {
|
|
|
128
134
|
`- Node: ${handoff.safeRuntime.node}`,
|
|
129
135
|
`- Python: ${handoff.safeRuntime.python}`,
|
|
130
136
|
`- Docker: ${handoff.safeRuntime.docker}`,
|
|
137
|
+
`- Inventory: ${handoff.inventory?.mode || "basic"}${handoff.inventory?.enabled ? " enabled" : " disabled"}`,
|
|
131
138
|
"",
|
|
132
139
|
"Open intents:",
|
|
133
140
|
...(handoff.openIntents.length ? handoff.openIntents.map((i) => `- ${i.actor}: ${i.action}${i.target ? ` (${i.target})` : ""}`) : ["- none"]),
|
|
@@ -203,6 +210,9 @@ const {manifest,timeline,warnings,intents,policy}=JSON.parse(document.getElement
|
|
|
203
210
|
function esc(s){return String(s).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"')}
|
|
204
211
|
const entries=o=>Object.entries(o||{});
|
|
205
212
|
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>';
|
|
213
|
+
const inventoryGroups=manifest.inventory?.tools||{};
|
|
214
|
+
const inventoryCount=Object.values(inventoryGroups).reduce((sum,items)=>sum+(Array.isArray(items)?items.length:0),0);
|
|
215
|
+
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>';
|
|
206
216
|
const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
|
|
207
217
|
const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
|
|
208
218
|
const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
|
|
@@ -246,6 +256,7 @@ document.getElementById('app').innerHTML=\`
|
|
|
246
256
|
\${card('Package Managers',\`<span class="pill">\${entries(manifest.packageManagers).length} found</span>\`,\`<table>\${rows(manifest.packageManagers)}</table>\`)}
|
|
247
257
|
\${card('Containers',manifest.containers?.docker?'<span class="pill">available</span>':'<span class="pill off">not detected</span>',\`<table>\${rows(manifest.containers)}</table>\`)}
|
|
248
258
|
\${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
|
|
259
|
+
\${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
|
|
249
260
|
</div>
|
|
250
261
|
<aside>
|
|
251
262
|
\${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
|
|
@@ -304,6 +315,17 @@ function contextLines(manifest, warnings, intents) {
|
|
|
304
315
|
];
|
|
305
316
|
}
|
|
306
317
|
|
|
318
|
+
function inventoryLines(inventory = {}) {
|
|
319
|
+
if (!inventory.enabled) return ["- Mode: basic", "- Deep global inventory is disabled. Run `aienvmp sync --deep` when needed."];
|
|
320
|
+
const groups = Object.entries(inventory.tools || {});
|
|
321
|
+
if (!groups.length) return ["- Mode: deep", "- No global tools detected by optional scanners."];
|
|
322
|
+
const lines = ["- Mode: deep"];
|
|
323
|
+
for (const [name, items] of groups) {
|
|
324
|
+
lines.push(`- ${name}: ${items.length} tools`);
|
|
325
|
+
}
|
|
326
|
+
return lines;
|
|
327
|
+
}
|
|
328
|
+
|
|
307
329
|
function policyLines(policy) {
|
|
308
330
|
const lines = [];
|
|
309
331
|
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();
|