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 +13 -0
- package/README.md +9 -8
- package/ROADMAP.md +6 -6
- package/package.json +1 -1
- package/src/cli.js +5 -1
- package/src/commands/compile.js +1 -1
- package/src/commands/context.js +2 -1
- package/src/commands/dash.js +1 -1
- package/src/commands/doctor.js +7 -2
- package/src/commands/handoff.js +57 -0
- package/src/commands/intent.js +5 -2
- package/src/commands/record.js +6 -2
- package/src/doctor.js +41 -1
- package/src/manifest.js +8 -0
- package/src/render.js +44 -3
- package/src/trust.js +41 -0
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`
|
|
9
|
+
`aienvmp` is an AI environment coordination tool for shared coding machines.
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
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
|
|
package/src/commands/compile.js
CHANGED
|
@@ -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) {
|
package/src/commands/context.js
CHANGED
|
@@ -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,
|
package/src/commands/dash.js
CHANGED
|
@@ -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 });
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { diagnose } from "../doctor.js";
|
|
2
2
|
import { readJson } from "../fsutil.js";
|
|
3
|
-
import {
|
|
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
|
|
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
|
+
}
|
package/src/commands/intent.js
CHANGED
|
@@ -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:
|
|
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);
|
package/src/commands/record.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
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('
|
|
195
|
-
\${auditItem('
|
|
196
|
-
\${auditItem('
|
|
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
|
+
}
|