aienvmp 0.1.40 → 0.1.42
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/BUGFIXES.md +6 -0
- package/CHANGELOG.md +18 -0
- package/README.md +9 -1
- package/TROUBLESHOOTING.md +30 -0
- package/action.yml +24 -0
- package/examples/github-action.yml +4 -0
- package/package.json +1 -1
- package/src/cli.js +5 -1
- package/src/commands/context.js +5 -1
- package/src/commands/doctor.js +9 -0
- package/src/commands/handoff.js +1 -0
- package/src/commands/schema.js +16 -0
- package/src/contract.js +42 -0
- package/src/dependencies.js +19 -1
- package/src/enforcement.js +16 -1
- package/src/preflight.js +4 -1
- package/src/render.js +34 -1
package/BUGFIXES.md
CHANGED
|
@@ -34,6 +34,12 @@ Short record of bugs, fixes, and follow-up checks.
|
|
|
34
34
|
- Fix: npm and Python security summaries now include fix versions and advisory references when scanners provide them.
|
|
35
35
|
- Verification: parser tests cover npm remediation objects and pip-audit advisory ids; Windows and macOS tarball tests completed `sync --security`.
|
|
36
36
|
|
|
37
|
+
### Advisory doctor behavior needed clearer verification
|
|
38
|
+
|
|
39
|
+
- Issue: `doctor` warnings can look like failures to AI/CI consumers even though local operation should stay non-blocking by default.
|
|
40
|
+
- Fix: `doctor --json` now exposes `exitBehavior`, and enforcement gate metadata explains when strict mode sets a failure exit code.
|
|
41
|
+
- Verification: Windows and macOS candidate smoke checks confirmed default `doctor --json` exits successfully while `doctor --strict policy --json` fails on matching policy warnings.
|
|
42
|
+
|
|
37
43
|
## Template
|
|
38
44
|
|
|
39
45
|
### Title
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.42
|
|
4
|
+
|
|
5
|
+
- Added explicit enforcement gate metadata so AI and CI consumers know local checks are warn-only unless `--strict` or `--ci` is requested.
|
|
6
|
+
- Added `doctor --json` exit behavior metadata to distinguish advisory warnings from strict failure conditions.
|
|
7
|
+
- Surfaced enforcement gate details in context, plan, and dashboard outputs.
|
|
8
|
+
- Extended the GitHub Action to write `.aienvmp/schema.json` and `.aienvmp/doctor.json` artifacts by default.
|
|
9
|
+
- Clarified README guidance for advisory-by-default behavior and opt-in strict/CI failure.
|
|
10
|
+
- Documented advisory doctor exit behavior and strict verification steps in troubleshooting and bugfix notes.
|
|
11
|
+
|
|
12
|
+
## 0.1.41
|
|
13
|
+
|
|
14
|
+
- Added preflight contract metadata so AI and CI consumers can rely on stable entry fields while ignoring additive changes.
|
|
15
|
+
- Added `aienvmp schema --json` to print the stable AI-readable output contract without scanning a workspace.
|
|
16
|
+
- Exposed coordination summaries at the root of `context --json` and `handoff --json`, with a human-readable handoff section.
|
|
17
|
+
- Added light SBOM source, confidence, and limitation hints so agents know what the lightweight snapshot does and does not prove.
|
|
18
|
+
- Updated the README with the schema command and light SBOM verification boundary while keeping the quick-start compact.
|
|
19
|
+
- Added an AI Contract dashboard card so humans can review the same stable fields that agents consume.
|
|
20
|
+
|
|
3
21
|
## 0.1.40
|
|
4
22
|
|
|
5
23
|
- Added dependency handoff summaries so the next AI receives dependency read-set and protocol guidance directly in `handoff`.
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ It helps Codex, Claude, Gemini, and humans avoid silent runtime, package manager
|
|
|
16
16
|
npx aienvmp sync
|
|
17
17
|
npx aienvmp status
|
|
18
18
|
npx aienvmp context --json
|
|
19
|
+
npx aienvmp schema --json
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
Before environment changes:
|
|
@@ -27,6 +28,7 @@ npx aienvmp handoff --record --actor agent:codex
|
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
Use `--dir <workspace>` when AI or CI runs outside the target project.
|
|
31
|
+
Warnings are advisory by default. Use `doctor --strict <scope>` only when you want CI-style failure.
|
|
30
32
|
|
|
31
33
|
## What It Creates
|
|
32
34
|
|
|
@@ -43,11 +45,14 @@ AIENV.md # Markdown env map for AI agents
|
|
|
43
45
|
## AI Contract
|
|
44
46
|
|
|
45
47
|
- `status`, `context`, `plan`, and `handoff` share one preflight contract.
|
|
48
|
+
- `schema --json` prints the stable AI-readable output contract without scanning.
|
|
46
49
|
- `status.json.nextAgent` tells the next AI what to read and whether to review first.
|
|
47
50
|
- `dependencyReadSet` lists manifests and lockfiles before package or security changes.
|
|
48
51
|
- `coordination.conflictTargets` shows where multiple agents are planning changes.
|
|
49
52
|
- `handoff` carries dependency read-set and protocol guidance for the next AI.
|
|
50
|
-
-
|
|
53
|
+
- Light SBOM includes source/confidence hints; verify security claims with dedicated scanners.
|
|
54
|
+
- `enforcementProfile.gate` explains when checks warn, fail, and set exit codes.
|
|
55
|
+
- Everything is advisory by default; strict failure is opt-in with `doctor --strict` or `--ci`.
|
|
51
56
|
|
|
52
57
|
## Agent Files
|
|
53
58
|
|
|
@@ -65,6 +70,7 @@ npx aienvmp snippet gemini
|
|
|
65
70
|
aienvmp sync # update env map, status, ledger, dashboard
|
|
66
71
|
aienvmp status --write # refresh compact AI status
|
|
67
72
|
aienvmp context --json # AI decision contract
|
|
73
|
+
aienvmp schema --json # stable output contract for AI/CI consumers
|
|
68
74
|
aienvmp plan --write # read-only action plan
|
|
69
75
|
aienvmp handoff --record # next-agent summary
|
|
70
76
|
aienvmp intent # record planned env change
|
|
@@ -74,6 +80,8 @@ aienvmp doctor --strict security|policy|coordination|all
|
|
|
74
80
|
|
|
75
81
|
## CI
|
|
76
82
|
|
|
83
|
+
The GitHub Action writes status, schema, doctor, plan, and dashboard artifacts. `strict: "off"` reports warnings without failing the job.
|
|
84
|
+
|
|
77
85
|
```yaml
|
|
78
86
|
- uses: soovwv/aienvmp@main
|
|
79
87
|
with:
|
package/TROUBLESHOOTING.md
CHANGED
|
@@ -101,3 +101,33 @@ Then retry:
|
|
|
101
101
|
```bash
|
|
102
102
|
npx aienvmp context
|
|
103
103
|
```
|
|
104
|
+
|
|
105
|
+
## Doctor shows warnings but exits successfully
|
|
106
|
+
|
|
107
|
+
Symptom:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
npx aienvmp doctor --json
|
|
111
|
+
# status: warning
|
|
112
|
+
# exit code: 0
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Cause:
|
|
116
|
+
|
|
117
|
+
- Local checks are advisory by default so shared machines are not blocked unexpectedly.
|
|
118
|
+
|
|
119
|
+
Fail CI explicitly:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx aienvmp doctor --strict policy
|
|
123
|
+
npx aienvmp doctor --strict security
|
|
124
|
+
npx aienvmp doctor --strict coordination
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Check the machine-readable rule:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx aienvmp doctor --json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Read `exitBehavior` and `strict.gate` to see when a failure exit code will be set.
|
package/action.yml
CHANGED
|
@@ -19,6 +19,14 @@ inputs:
|
|
|
19
19
|
description: Write compact AI status artifact
|
|
20
20
|
required: false
|
|
21
21
|
default: "true"
|
|
22
|
+
write-schema:
|
|
23
|
+
description: Write stable AI output contract artifact
|
|
24
|
+
required: false
|
|
25
|
+
default: "true"
|
|
26
|
+
write-doctor-json:
|
|
27
|
+
description: Write doctor JSON with advisory/strict exit behavior
|
|
28
|
+
required: false
|
|
29
|
+
default: "true"
|
|
22
30
|
|
|
23
31
|
runs:
|
|
24
32
|
using: composite
|
|
@@ -41,6 +49,22 @@ runs:
|
|
|
41
49
|
node "$GITHUB_ACTION_PATH/bin/aienvmp.js" status --dir "${{ inputs.directory }}" --write --quiet
|
|
42
50
|
fi
|
|
43
51
|
|
|
52
|
+
- name: Write AI schema
|
|
53
|
+
shell: bash
|
|
54
|
+
run: |
|
|
55
|
+
if [ "${{ inputs.write-schema }}" = "true" ]; then
|
|
56
|
+
mkdir -p "${{ inputs.directory }}/.aienvmp"
|
|
57
|
+
node "$GITHUB_ACTION_PATH/bin/aienvmp.js" schema --json > "${{ inputs.directory }}/.aienvmp/schema.json"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
- name: Write doctor advisory JSON
|
|
61
|
+
shell: bash
|
|
62
|
+
run: |
|
|
63
|
+
if [ "${{ inputs.write-doctor-json }}" = "true" ]; then
|
|
64
|
+
mkdir -p "${{ inputs.directory }}/.aienvmp"
|
|
65
|
+
node "$GITHUB_ACTION_PATH/bin/aienvmp.js" doctor --dir "${{ inputs.directory }}" --json > "${{ inputs.directory }}/.aienvmp/doctor.json"
|
|
66
|
+
fi
|
|
67
|
+
|
|
44
68
|
- name: Doctor
|
|
45
69
|
shell: bash
|
|
46
70
|
run: |
|
|
@@ -14,6 +14,8 @@ jobs:
|
|
|
14
14
|
with:
|
|
15
15
|
directory: "."
|
|
16
16
|
write-plan: "true"
|
|
17
|
+
write-schema: "true"
|
|
18
|
+
write-doctor-json: "true"
|
|
17
19
|
strict: "off" # security, policy, coordination, all, or off
|
|
18
20
|
|
|
19
21
|
- uses: actions/upload-artifact@v4
|
|
@@ -24,6 +26,8 @@ jobs:
|
|
|
24
26
|
AIENV.md
|
|
25
27
|
.aienvmp/manifest.json
|
|
26
28
|
.aienvmp/status.json
|
|
29
|
+
.aienvmp/schema.json
|
|
30
|
+
.aienvmp/doctor.json
|
|
27
31
|
.aienvmp/plan.json
|
|
28
32
|
.aienvmp/plan.md
|
|
29
33
|
.aienvmp/dashboard.html
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { snippetWorkspace } from "./commands/snippet.js";
|
|
|
13
13
|
import { handoffWorkspace } from "./commands/handoff.js";
|
|
14
14
|
import { planWorkspace } from "./commands/plan.js";
|
|
15
15
|
import { statusWorkspace } from "./commands/status.js";
|
|
16
|
+
import { schemaWorkspace } from "./commands/schema.js";
|
|
16
17
|
import { readFileSync } from "node:fs";
|
|
17
18
|
|
|
18
19
|
const commands = new Map([
|
|
@@ -30,7 +31,8 @@ const commands = new Map([
|
|
|
30
31
|
["snippet", snippetWorkspace],
|
|
31
32
|
["handoff", handoffWorkspace],
|
|
32
33
|
["plan", planWorkspace],
|
|
33
|
-
["status", statusWorkspace]
|
|
34
|
+
["status", statusWorkspace],
|
|
35
|
+
["schema", schemaWorkspace]
|
|
34
36
|
]);
|
|
35
37
|
|
|
36
38
|
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
@@ -107,6 +109,7 @@ Usage:
|
|
|
107
109
|
aienvmp status [--dir .] [--json] [--write] [--quiet]
|
|
108
110
|
aienvmp handoff [--dir .] [--json] [--record --actor agent:id]
|
|
109
111
|
aienvmp plan [--dir .] [--json] [--write]
|
|
112
|
+
aienvmp schema [--json]
|
|
110
113
|
|
|
111
114
|
Common:
|
|
112
115
|
aienvmp sync update AIENV.md, manifest, status, ledger, intents, and dashboard
|
|
@@ -114,6 +117,7 @@ Common:
|
|
|
114
117
|
aienvmp context print the AI preflight brief
|
|
115
118
|
aienvmp handoff print the next-agent handoff summary
|
|
116
119
|
aienvmp plan print a read-only AI environment action plan
|
|
120
|
+
aienvmp schema print the stable AI-readable output contract
|
|
117
121
|
aienvmp snippet print an AGENTS.md pointer snippet
|
|
118
122
|
aienvmp dash regenerate/open the lightweight dashboard
|
|
119
123
|
|
package/src/commands/context.js
CHANGED
|
@@ -26,6 +26,7 @@ export async function contextWorkspace(args) {
|
|
|
26
26
|
console.log(JSON.stringify({
|
|
27
27
|
status: warnings.length ? "review-required" : "clear",
|
|
28
28
|
preflight,
|
|
29
|
+
coordination: preflight.coordination,
|
|
29
30
|
decision,
|
|
30
31
|
enforcement: enforcementAdvice(warnings),
|
|
31
32
|
recommendedActions: actions,
|
|
@@ -49,7 +50,7 @@ export async function contextWorkspace(args) {
|
|
|
49
50
|
}, null, 2));
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
|
-
console.log(renderContext(manifest, timeline, warnings, intents, policy, actions));
|
|
53
|
+
console.log(renderContext({ ...manifest, preflight }, timeline, warnings, intents, policy, actions));
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function lightSbomSummary(lightSbom = {}) {
|
|
@@ -73,6 +74,9 @@ function lightSbomSummary(lightSbom = {}) {
|
|
|
73
74
|
guidance: "No lockfile policy detected."
|
|
74
75
|
},
|
|
75
76
|
dependencyChangeHints: (lightSbom.dependencyChangeHints || []).slice(0, 8),
|
|
77
|
+
source: lightSbom.source || {},
|
|
78
|
+
confidence: lightSbom.confidence || {},
|
|
79
|
+
limitations: lightSbom.limitations || [],
|
|
76
80
|
aiUse: lightSbom.aiUse || {}
|
|
77
81
|
};
|
|
78
82
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -18,9 +18,18 @@ export async function doctorWorkspace(args) {
|
|
|
18
18
|
const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
|
|
19
19
|
const actions = recommendedActions(manifest, { warnings, intents });
|
|
20
20
|
const strict = strictResult(warnings, args);
|
|
21
|
+
const exitBehavior = {
|
|
22
|
+
mode: strict.enabled ? "strict" : "advisory",
|
|
23
|
+
willSetFailureExitCode: strict.fail,
|
|
24
|
+
reason: strict.enabled
|
|
25
|
+
? `strict scope ${strict.scope} is enabled`
|
|
26
|
+
: "strict mode is off; warnings are reported without failing local operation",
|
|
27
|
+
gate: strict.gate
|
|
28
|
+
};
|
|
21
29
|
if (args.json) {
|
|
22
30
|
console.log(JSON.stringify({
|
|
23
31
|
status: strict.fail ? "fail" : warnings.length ? "warning" : "ok",
|
|
32
|
+
exitBehavior,
|
|
24
33
|
trust: manifest.trust || {},
|
|
25
34
|
policy,
|
|
26
35
|
openIntentCount: intents.length,
|
package/src/commands/handoff.js
CHANGED
|
@@ -62,6 +62,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
|
|
|
62
62
|
inventory: inventorySummary(manifest.inventory),
|
|
63
63
|
security: securitySummary(manifest.security),
|
|
64
64
|
dependencyHandoff: dependencyHandoffSummary(preflight),
|
|
65
|
+
coordination: preflight.coordination,
|
|
65
66
|
policy: {
|
|
66
67
|
node: policy.node || "not set",
|
|
67
68
|
python: policy.python || "not set",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { schemaContract } from "../contract.js";
|
|
2
|
+
|
|
3
|
+
export async function schemaWorkspace(args = {}) {
|
|
4
|
+
const schema = schemaContract();
|
|
5
|
+
if (args.json) {
|
|
6
|
+
console.log(JSON.stringify(schema, null, 2));
|
|
7
|
+
} else {
|
|
8
|
+
console.log("aienvmp contract");
|
|
9
|
+
console.log(`schema: ${schema.name} v${schema.schemaVersion}`);
|
|
10
|
+
console.log(`status: ${schema.outputs.status.command}`);
|
|
11
|
+
console.log(`context: ${schema.outputs.context.command}`);
|
|
12
|
+
console.log(`handoff: ${schema.outputs.handoff.command}`);
|
|
13
|
+
console.log(`rule: ${schema.compatibility.consumerRule}`);
|
|
14
|
+
}
|
|
15
|
+
return schema;
|
|
16
|
+
}
|
package/src/contract.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function preflightContract() {
|
|
2
|
+
return {
|
|
3
|
+
name: "aienvmp-preflight",
|
|
4
|
+
version: 1,
|
|
5
|
+
stability: "additive",
|
|
6
|
+
requiredFields: ["schemaVersion", "state", "decision", "quickstart", "commands", "artifacts"],
|
|
7
|
+
aiEntryFields: ["state", "summary", "nextAgent", "coordination", "dependencyReadSet", "dependencyChangeProtocol"],
|
|
8
|
+
rule: "Consumers should ignore unknown fields and treat missing optional fields as unavailable."
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function schemaContract() {
|
|
13
|
+
return {
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
name: "aienvmp-contract",
|
|
16
|
+
purpose: "Stable AI-readable contract for aienvmp outputs.",
|
|
17
|
+
outputs: {
|
|
18
|
+
status: {
|
|
19
|
+
file: ".aienvmp/status.json",
|
|
20
|
+
command: "aienvmp status --json",
|
|
21
|
+
contract: preflightContract()
|
|
22
|
+
},
|
|
23
|
+
context: {
|
|
24
|
+
command: "aienvmp context --json",
|
|
25
|
+
rootFields: ["status", "preflight", "coordination", "decision", "recommendedActions", "workspace", "dependencySnapshot", "lightSbom", "warnings"]
|
|
26
|
+
},
|
|
27
|
+
handoff: {
|
|
28
|
+
command: "aienvmp handoff --json",
|
|
29
|
+
rootFields: ["status", "decision", "preflight", "coordination", "dependencyHandoff", "openIntents", "warnings", "recommendedActions", "recentChanges"]
|
|
30
|
+
},
|
|
31
|
+
manifest: {
|
|
32
|
+
file: ".aienvmp/manifest.json",
|
|
33
|
+
rootFields: ["schemaVersion", "workspace", "runtimes", "packageManagers", "dependencySnapshot", "lightSbom", "security", "trust"]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
compatibility: {
|
|
37
|
+
stability: "additive",
|
|
38
|
+
consumerRule: "Ignore unknown fields. Do not require optional fields unless listed in requiredFields.",
|
|
39
|
+
localBehavior: "read-only; this command does not scan, install, update, or lock anything."
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
package/src/dependencies.js
CHANGED
|
@@ -79,6 +79,23 @@ export function buildLightSbom(snapshot = {}, security = {}) {
|
|
|
79
79
|
schemaVersion: 1,
|
|
80
80
|
mode: "light-sbom",
|
|
81
81
|
note: "AI-ready package and vulnerability summary from read-only project files and optional scanners.",
|
|
82
|
+
source: {
|
|
83
|
+
dependencies: "project manifests",
|
|
84
|
+
lockfiles: "file presence only",
|
|
85
|
+
vulnerabilities: security.enabled ? "optional scanner summary" : "not scanned",
|
|
86
|
+
resolver: "not run"
|
|
87
|
+
},
|
|
88
|
+
confidence: {
|
|
89
|
+
directDependencies: "high",
|
|
90
|
+
lockfileManager: snapshot.lockfiles?.length ? "medium" : "none",
|
|
91
|
+
transitiveDependencies: "not-resolved",
|
|
92
|
+
vulnerabilities: security.enabled ? "scanner-provided" : "not-scanned"
|
|
93
|
+
},
|
|
94
|
+
limitations: [
|
|
95
|
+
"Does not install packages.",
|
|
96
|
+
"Does not resolve full transitive dependency graphs.",
|
|
97
|
+
"Does not replace CycloneDX, SPDX, Syft, Trivy, npm audit, or pip-audit outputs."
|
|
98
|
+
],
|
|
82
99
|
summary: {
|
|
83
100
|
ecosystems: countBy(packages, "ecosystem"),
|
|
84
101
|
managers: countBy(packages, "manager"),
|
|
@@ -96,7 +113,8 @@ export function buildLightSbom(snapshot = {}, security = {}) {
|
|
|
96
113
|
aiUse: {
|
|
97
114
|
beforeDependencyChanges: "Read lightSbom.summary and lightSbom.topRisk before changing dependencies.",
|
|
98
115
|
securityMode: security.enabled ? "scanner-summary" : "scanner-off",
|
|
99
|
-
dependencySource: "project manifests only; no install or resolver is run"
|
|
116
|
+
dependencySource: "project manifests only; no install or resolver is run",
|
|
117
|
+
trustRule: "Use lightSbom as a fast AI planning map; verify with dedicated scanners before security claims."
|
|
100
118
|
}
|
|
101
119
|
};
|
|
102
120
|
}
|
package/src/enforcement.js
CHANGED
|
@@ -3,12 +3,14 @@ const STRICT_SCOPES = ["security", "policy", "coordination", "all"];
|
|
|
3
3
|
export function strictResult(warnings = [], args = {}) {
|
|
4
4
|
const scope = normalizeStrictScope(args.strict || (args.ci ? "all" : ""));
|
|
5
5
|
const matchedWarnings = scope ? warnings.filter((warning) => warningMatchesScope(warning, scope)) : [];
|
|
6
|
+
const gate = enforcementGate(scope);
|
|
6
7
|
return {
|
|
7
8
|
enabled: Boolean(scope),
|
|
8
9
|
scope: scope || "off",
|
|
9
10
|
fail: matchedWarnings.length > 0,
|
|
10
11
|
matchedWarningCodes: matchedWarnings.map((warning) => warning.code),
|
|
11
|
-
availableScopes: STRICT_SCOPES
|
|
12
|
+
availableScopes: STRICT_SCOPES,
|
|
13
|
+
gate
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -28,6 +30,7 @@ export function enforcementAdvice(warnings = []) {
|
|
|
28
30
|
mode: "advisory-by-default",
|
|
29
31
|
localBehavior: "non-blocking",
|
|
30
32
|
ciBehavior: "strict-only-when-requested",
|
|
33
|
+
gate: enforcementGate(""),
|
|
31
34
|
suggestedStrictScopes,
|
|
32
35
|
scopes: scopeResults,
|
|
33
36
|
recommendedCommand: suggestedStrictScopes.length
|
|
@@ -37,6 +40,18 @@ export function enforcementAdvice(warnings = []) {
|
|
|
37
40
|
};
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
export function enforcementGate(scope = "") {
|
|
44
|
+
const strictScope = normalizeStrictScope(scope);
|
|
45
|
+
return {
|
|
46
|
+
defaultMode: "advisory",
|
|
47
|
+
strictMode: strictScope || "off",
|
|
48
|
+
localDefault: "warn-only",
|
|
49
|
+
failCondition: strictScope ? `matching warnings in ${strictScope}` : "never in default mode",
|
|
50
|
+
exitCode: strictScope ? "1 when matching warnings exist" : "0 unless the command itself errors",
|
|
51
|
+
rule: "Do not block local or shared machine operation unless --strict or --ci is explicitly requested."
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
function normalizeStrictScope(value) {
|
|
41
56
|
if (value === true) return "all";
|
|
42
57
|
const scope = String(value || "").trim().toLowerCase();
|
package/src/preflight.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { recommendedActions } from "./actions.js";
|
|
2
2
|
import { aiDecision } from "./decision.js";
|
|
3
|
-
import { enforcementAdvice } from "./enforcement.js";
|
|
3
|
+
import { enforcementAdvice, enforcementGate } from "./enforcement.js";
|
|
4
|
+
import { preflightContract } from "./contract.js";
|
|
4
5
|
|
|
5
6
|
export function buildPreflight(manifest = {}, warnings = [], intents = []) {
|
|
6
7
|
const decision = aiDecision(warnings, intents);
|
|
@@ -14,6 +15,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
|
|
|
14
15
|
const coordination = coordinationSummary(intents);
|
|
15
16
|
return {
|
|
16
17
|
schemaVersion: 1,
|
|
18
|
+
contract: preflightContract(),
|
|
17
19
|
state,
|
|
18
20
|
summary: state === "clear"
|
|
19
21
|
? "Project-local work can continue. Record intent before environment changes."
|
|
@@ -25,6 +27,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
|
|
|
25
27
|
localOperation: "non-blocking",
|
|
26
28
|
strictMode: "optional",
|
|
27
29
|
strictUse: "CI or explicit human-requested checks only",
|
|
30
|
+
gate: enforcementGate(""),
|
|
28
31
|
reason: "Avoid disrupting shared servers or developer machines while still making drift visible.",
|
|
29
32
|
recommendedStrictCommand: enforcement.recommendedCommand,
|
|
30
33
|
strictCommands: [
|
package/src/render.js
CHANGED
|
@@ -170,6 +170,9 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
|
|
|
170
170
|
"Recommended actions:",
|
|
171
171
|
...(recommendedActions.length ? recommendedActions.map((item) => `- [${item.priority}] ${item.summary}${item.command ? ` (${item.command})` : ""}`) : ["- none"]),
|
|
172
172
|
"",
|
|
173
|
+
"Enforcement gate:",
|
|
174
|
+
...enforcementGateLines(manifest.preflight?.enforcementProfile?.gate),
|
|
175
|
+
"",
|
|
173
176
|
"Open intents:",
|
|
174
177
|
...(intents.length ? intents.slice(-5).reverse().map((i) => `- ${i.actor}: ${i.action}`) : ["- none"]),
|
|
175
178
|
"",
|
|
@@ -199,6 +202,9 @@ export function renderHandoff(handoff) {
|
|
|
199
202
|
"Open intents:",
|
|
200
203
|
...(handoff.openIntents.length ? handoff.openIntents.map((i) => `- ${i.actor}: ${i.action}${i.target ? ` (${i.target})` : ""}`) : ["- none"]),
|
|
201
204
|
"",
|
|
205
|
+
"Coordination:",
|
|
206
|
+
...coordinationHandoffLines(handoff.coordination),
|
|
207
|
+
"",
|
|
202
208
|
"Warnings:",
|
|
203
209
|
...(handoff.warnings.length ? handoff.warnings.map((w) => `- ${w.message}`) : ["- none"]),
|
|
204
210
|
"",
|
|
@@ -220,6 +226,12 @@ export function renderHandoff(handoff) {
|
|
|
220
226
|
return lines.join("\n");
|
|
221
227
|
}
|
|
222
228
|
|
|
229
|
+
function coordinationHandoffLines(coordination = {}) {
|
|
230
|
+
const conflicts = coordination.conflictTargets || [];
|
|
231
|
+
if (conflicts.length) return [`- Conflicts: ${conflicts.join(", ")}`, `- Next: ${coordination.next || "review open intents"}`];
|
|
232
|
+
return [`- Open intents: ${coordination.openIntentCount || 0}`, `- Next: ${coordination.next || "no open environment intents"}`];
|
|
233
|
+
}
|
|
234
|
+
|
|
223
235
|
function dependencyHandoffLines(dependencyHandoff = {}) {
|
|
224
236
|
const readSet = dependencyHandoff.readSet || [];
|
|
225
237
|
const protocol = dependencyHandoff.protocol || {};
|
|
@@ -258,6 +270,9 @@ export function renderPlan(plan) {
|
|
|
258
270
|
"Review gates:",
|
|
259
271
|
...plan.reviewGates.map((item) => `- ${item}`),
|
|
260
272
|
"",
|
|
273
|
+
"Enforcement gate:",
|
|
274
|
+
...enforcementGateLines(plan.preflight?.enforcementProfile?.gate),
|
|
275
|
+
"",
|
|
261
276
|
"Dependency protocol:",
|
|
262
277
|
...dependencyProtocolPlanLines(plan.preflight?.dependencyChangeProtocol),
|
|
263
278
|
"",
|
|
@@ -285,6 +300,15 @@ function dependencyProtocolPlanLines(protocol = {}) {
|
|
|
285
300
|
];
|
|
286
301
|
}
|
|
287
302
|
|
|
303
|
+
function enforcementGateLines(gate = {}) {
|
|
304
|
+
return [
|
|
305
|
+
`- Default: ${gate.defaultMode || "advisory"} (${gate.localDefault || "warn-only"})`,
|
|
306
|
+
`- Strict: ${gate.strictMode || "off"}`,
|
|
307
|
+
`- Fail condition: ${gate.failCondition || "never in default mode"}`,
|
|
308
|
+
`- Exit code: ${gate.exitCode || "0 unless the command itself errors"}`
|
|
309
|
+
];
|
|
310
|
+
}
|
|
311
|
+
|
|
288
312
|
function environmentLines(item) {
|
|
289
313
|
return [
|
|
290
314
|
`- ${item.category}: ${item.summary}`,
|
|
@@ -405,7 +429,10 @@ const ciHasFailure=ciReadiness.some(s=>s.status==='fail');
|
|
|
405
429
|
const ciReadinessHtml=ciReadiness.length?'<table>'+ciReadiness.map(s=>\`<tr><th>\${esc(s.scope)}</th><td><code>\${esc(s.status)}</code>\${s.matchedWarningCodes?.length?\` \${esc(s.matchedWarningCodes.join(', '))}\`:''}</td></tr>\`).join('')+'</table>':'<div class="okline">Run <code>aienvmp doctor --strict security|policy|coordination|all</code> to choose CI enforcement scope.</div>';
|
|
406
430
|
const enforcementProfile=manifest.preflight?.enforcementProfile||{};
|
|
407
431
|
const strictCommands=enforcementProfile.strictCommands||[];
|
|
408
|
-
const
|
|
432
|
+
const gate=enforcementProfile.gate||{};
|
|
433
|
+
const enforcementHtml=\`<table><tr><th>Default</th><td><code>\${esc(gate.defaultMode||enforcementProfile.defaultMode||'advisory')}</code> \${esc(gate.localDefault||'warn-only')}</td></tr><tr><th>Strict</th><td><code>\${esc(gate.strictMode||'off')}</code></td></tr><tr><th>Fail</th><td>\${esc(gate.failCondition||'never in default mode')}</td></tr><tr><th>Exit</th><td>\${esc(gate.exitCode||'0 unless the command itself errors')}</td></tr><tr><th>Recommended</th><td><code>\${esc(enforcementProfile.recommendedStrictCommand||'aienvmp doctor --strict all')}</code></td></tr></table><div class="timeline">\${strictCommands.slice(0,4).map(cmd=>\`<div class="event"><time>CI</time><div><code>\${esc(cmd)}</code></div></div>\`).join('')}</div><div class="path">\${esc(gate.rule||enforcementProfile.reason||'Warnings stay advisory unless strict mode is requested.')}</div>\`;
|
|
434
|
+
const contract=manifest.preflight?.contract||{};
|
|
435
|
+
const contractHtml=contract.name?\`<table><tr><th>Name</th><td><code>\${esc(contract.name)}</code></td></tr><tr><th>Version</th><td><code>\${esc(contract.version||1)}</code></td></tr><tr><th>Stability</th><td><code>\${esc(contract.stability||'additive')}</code></td></tr><tr><th>AI fields</th><td>\${esc((contract.aiEntryFields||[]).join(', ')||'none')}</td></tr></table><div class="path">\${esc(contract.rule||'Ignore unknown fields.')}</div>\`:'<div class="okline">Run <code>aienvmp status --write</code> to include AI contract metadata.</div>';
|
|
409
436
|
const intentTargets=manifest.preflight?.intentTargets||[];
|
|
410
437
|
const intentTargetsHtml=intentTargets.length?'<div class="timeline">'+intentTargets.slice(0,5).map(t=>\`<div class="event"><time>\${esc(t.target)}</time><div><b>\${esc(t.target)}</b> \${esc(t.reason||'Record this target before environment changes.')}\${t.command?\`<div class="path">\${esc(t.command)}</div>\`:''}</div></div>\`).join('')+'</div>':'<div class="okline">No specific target recommendation. Use <code>aienvmp intent --actor agent:id --action planned-change</code>.</div>';
|
|
411
438
|
const dependencyReadSet=manifest.preflight?.dependencyReadSet||[];
|
|
@@ -462,6 +489,8 @@ document.getElementById('app').innerHTML=\`
|
|
|
462
489
|
<div style="height:14px"></div>
|
|
463
490
|
\${card('AI Intent Targets','<span class="pill">'+intentTargets.length+' targets</span>',intentTargetsHtml)}
|
|
464
491
|
<div style="height:14px"></div>
|
|
492
|
+
\${card('AI Contract','<span class="pill">'+(contract.stability||'additive')+'</span>',contractHtml)}
|
|
493
|
+
<div style="height:14px"></div>
|
|
465
494
|
\${card('Dependency Read Set','<span class="pill">'+dependencyReadSet.length+' files</span>',dependencyReadSetHtml)}
|
|
466
495
|
<div style="height:14px"></div>
|
|
467
496
|
\${card('Dependency Protocol','<span class="pill">'+(dependencyProtocol.mode||'advisory')+'</span>',dependencyProtocolHtml)}
|
|
@@ -569,6 +598,10 @@ function lightSbomLines(lightSbom = {}) {
|
|
|
569
598
|
`- Transitive or unmatched vulnerable packages: ${summary.transitiveOrUnmatchedVulnerablePackages || 0}`,
|
|
570
599
|
`- Lockfiles: ${(summary.lockfiles || []).map((item) => item.file).join(", ") || "none"}`
|
|
571
600
|
];
|
|
601
|
+
if (lightSbom.source || lightSbom.confidence) {
|
|
602
|
+
lines.push(`- Source: ${lightSbom.source?.dependencies || "project manifests"}; vulnerabilities: ${lightSbom.source?.vulnerabilities || "not scanned"}`);
|
|
603
|
+
lines.push(`- Confidence: direct ${lightSbom.confidence?.directDependencies || "unknown"}; transitive ${lightSbom.confidence?.transitiveDependencies || "unknown"}`);
|
|
604
|
+
}
|
|
572
605
|
if (lightSbom.packageManagerPolicy) {
|
|
573
606
|
lines.push(`- Package manager policy: ${lightSbom.packageManagerPolicy.status}`);
|
|
574
607
|
lines.push(`- Package manager guidance: ${lightSbom.packageManagerPolicy.guidance}`);
|