aienvmp 0.1.3 → 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,18 @@
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
+
10
+ ## 0.1.4
11
+
12
+ - Added `aienvmp handoff` for next-agent environment handoff summaries.
13
+ - Added an AI Handoff card to the dashboard.
14
+ - Added handoff test coverage.
15
+
3
16
  ## 0.1.3
4
17
 
5
18
  - Strengthened the dashboard audit summary with AI decision, open intents, warnings, and recent changes.
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.
@@ -48,6 +48,7 @@ npx aienvmp snippet agents
48
48
  aienvmp sync # update env map, light SBOM, ledger, dashboard
49
49
  aienvmp context # AI preflight brief
50
50
  aienvmp context --json # machine-readable AI decision context
51
+ aienvmp handoff # next-agent handoff summary
51
52
  aienvmp intent # record a planned env change
52
53
  aienvmp record # record what changed
53
54
  aienvmp doctor --ci # strict CI check
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.3",
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": {
package/src/cli.js CHANGED
@@ -10,6 +10,7 @@ import { intentWorkspace } from "./commands/intent.js";
10
10
  import { resolveWorkspace } from "./commands/resolve.js";
11
11
  import { syncWorkspace } from "./commands/sync.js";
12
12
  import { snippetWorkspace } from "./commands/snippet.js";
13
+ import { handoffWorkspace } from "./commands/handoff.js";
13
14
  import { readFileSync } from "node:fs";
14
15
 
15
16
  const commands = new Map([
@@ -24,7 +25,8 @@ const commands = new Map([
24
25
  ["intent", intentWorkspace],
25
26
  ["resolve", resolveWorkspace],
26
27
  ["sync", syncWorkspace],
27
- ["snippet", snippetWorkspace]
28
+ ["snippet", snippetWorkspace],
29
+ ["handoff", handoffWorkspace]
28
30
  ]);
29
31
 
30
32
  const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
@@ -74,10 +76,12 @@ function printUsage() {
74
76
  Usage:
75
77
  aienvmp sync [--dir .] [--json] [--quiet]
76
78
  aienvmp context [--dir .] [--json]
79
+ aienvmp handoff [--dir .] [--json]
77
80
 
78
81
  Common:
79
82
  aienvmp sync update AIENV.md, manifest, ledger, intents, and dashboard
80
83
  aienvmp context print the AI preflight brief
84
+ aienvmp handoff print the next-agent handoff summary
81
85
  aienvmp snippet print an AGENTS.md pointer snippet
82
86
  aienvmp dash regenerate/open the lightweight dashboard
83
87
 
@@ -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) {
@@ -0,0 +1,57 @@
1
+ import { diagnose } from "../doctor.js";
2
+ import { readJson } from "../fsutil.js";
3
+ import { loadPolicy, policyWarnings } from "../policy.js";
4
+ import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
5
+ import { renderHandoff } from "../render.js";
6
+ import { openIntents, readJsonl, readTimeline } from "../timeline.js";
7
+
8
+ export async function handoffWorkspace(args) {
9
+ const dir = workspaceDir(args);
10
+ const manifest = await readJson(manifestPath(dir));
11
+ if (!manifest) throw new Error("missing manifest; run `aienvmp sync` first");
12
+ const timeline = await readTimeline(timelinePath(dir));
13
+ const intents = openIntents(await readJsonl(intentsPath(dir)));
14
+ const policy = await loadPolicy(dir);
15
+ const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
16
+ const handoff = buildHandoff(manifest, timeline, warnings, intents, policy);
17
+
18
+ if (args.json) {
19
+ console.log(JSON.stringify(handoff, null, 2));
20
+ } else {
21
+ console.log(renderHandoff(handoff));
22
+ }
23
+ return handoff;
24
+ }
25
+
26
+ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
27
+ const reviewRequired = warnings.length > 0 || intents.length > 0;
28
+ return {
29
+ status: reviewRequired ? "review-required" : "clear",
30
+ trust: manifest.trust || {},
31
+ schemaVersion: manifest.schemaVersion || 1,
32
+ workspace: manifest.workspace,
33
+ safeRuntime: {
34
+ node: manifest.runtimes?.node || "not detected",
35
+ python: manifest.runtimes?.python || manifest.runtimes?.python3 || "not detected",
36
+ docker: manifest.containers?.docker ? "available" : "not detected"
37
+ },
38
+ policy: {
39
+ node: policy.node || "not set",
40
+ python: policy.python || "not set",
41
+ packageManager: policy.packageManager || "not set",
42
+ runtimeChanges: policy.runtimeChanges || "ask-first",
43
+ globalInstalls: policy.globalInstalls || "ask-first"
44
+ },
45
+ openIntents: intents.slice(-5).reverse(),
46
+ warnings,
47
+ recentChanges: timeline.slice(-5).reverse(),
48
+ mustNotDo: [
49
+ "do not change global runtimes without user approval",
50
+ "do not install or remove global package managers without user approval",
51
+ "do not change Docker daemon/context assumptions without user approval"
52
+ ],
53
+ recommendedNext: reviewRequired
54
+ ? "review warnings and open intents before environment changes"
55
+ : "continue with project-local work; record intent before environment changes"
56
+ };
57
+ }
@@ -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"}`,
@@ -111,6 +115,38 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
111
115
  ].join("\n");
112
116
  }
113
117
 
118
+ export function renderHandoff(handoff) {
119
+ const lines = [
120
+ "# AI Handoff",
121
+ "",
122
+ `Status: ${handoff.status}`,
123
+ `Trust: ${handoff.trust?.state || "observed"} (not AI-verified)`,
124
+ `Schema: ${handoff.schemaVersion}`,
125
+ `Workspace: ${handoff.workspace?.path || "unknown"}`,
126
+ "",
127
+ "Safe runtime:",
128
+ `- Node: ${handoff.safeRuntime.node}`,
129
+ `- Python: ${handoff.safeRuntime.python}`,
130
+ `- Docker: ${handoff.safeRuntime.docker}`,
131
+ "",
132
+ "Open intents:",
133
+ ...(handoff.openIntents.length ? handoff.openIntents.map((i) => `- ${i.actor}: ${i.action}${i.target ? ` (${i.target})` : ""}`) : ["- none"]),
134
+ "",
135
+ "Warnings:",
136
+ ...(handoff.warnings.length ? handoff.warnings.map((w) => `- ${w.message}`) : ["- none"]),
137
+ "",
138
+ "Recent changes:",
139
+ ...(handoff.recentChanges.length ? handoff.recentChanges.map((t) => `- ${formatTimeline(t)}`) : ["- none"]),
140
+ "",
141
+ "Must not do:",
142
+ ...handoff.mustNotDo.map((item) => `- ${item}`),
143
+ "",
144
+ `Recommended next: ${handoff.recommendedNext}`,
145
+ ""
146
+ ];
147
+ return lines.join("\n");
148
+ }
149
+
114
150
  export function renderDashboard(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
115
151
  const data = JSON.stringify({ manifest, timeline, warnings, intents, policy });
116
152
  return `<!doctype html>
@@ -178,8 +214,11 @@ const policyHtml=entries(policy).length?\`<table>\${rows(policy)}</table>\`:'<di
178
214
  const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
179
215
  const reviewRequired=warnings.length>0||intents.length>0;
180
216
  const recentChanges=timeline.slice(-8).length;
217
+ const trustState=manifest.trust?.state||'observed';
181
218
  const nextAction=reviewRequired?'Review before environment changes':'Proceed with project-local work';
182
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>\`;
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>\`;
183
222
  document.getElementById('app').innerHTML=\`
184
223
  <header>
185
224
  <div>
@@ -191,9 +230,9 @@ document.getElementById('app').innerHTML=\`
191
230
  </header>
192
231
  <section class="audit" aria-label="Audit summary">
193
232
  \${auditItem('AI decision',reviewRequired?'review required':'can proceed',nextAction,reviewRequired?'review':'primary')}
194
- \${auditItem('Open intents',String(intents.length),intents.length?'Resolve or coordinate before changes':'No pending env changes')}
195
- \${auditItem('Warnings',String(warnings.length),warnings.length?'Policy or drift needs attention':'No warnings detected')}
196
- \${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')}
197
236
  </section>
198
237
  <section class="metrics">
199
238
  <div class="metric"><div class="num">\${entries(manifest.runtimes).length}</div><div class="label">runtimes</div></div>
@@ -215,6 +254,8 @@ document.getElementById('app').innerHTML=\`
215
254
  <div style="height:14px"></div>
216
255
  \${card('Agent Intents','<span class="pill">'+intents.length+' open</span>',intentsHtml)}
217
256
  <div style="height:14px"></div>
257
+ \${card('AI Handoff',reviewRequired?'<span class="pill warn">review</span>':'<span class="pill">ready</span>',handoffHtml)}
258
+ <div style="height:14px"></div>
218
259
  \${card('Agent Pointers','<span class="pill">'+entries(manifest.agentFiles).filter(([,v])=>v).length+' detected</span>','<div class="agents">'+agentCards+'</div>')}
219
260
  <div style="height:14px"></div>
220
261
  \${card('Snapshot','',\`<table><tr><th>OS</th><td>\${esc(manifest.os.platform)} \${esc(manifest.os.release)} \${esc(manifest.os.arch)}</td></tr><tr><th>Shell</th><td>\${esc(manifest.os.shell||'unknown')}</td></tr><tr><th>Workspace</th><td><div class="path">\${esc(manifest.workspace.path)}</div></td></tr></table>\`)}
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
+ }