aienvmp 0.1.14 → 0.1.16

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.16
4
+
5
+ - Added scoped strict checks with `doctor --strict security|policy|coordination|all`.
6
+ - Kept `doctor --ci` compatible as strict `all`.
7
+ - Updated the GitHub Action `strict` input to support scoped enforcement while keeping advisory mode by default.
8
+
9
+ ## 0.1.15
10
+
11
+ - Added dashboard links for written AI plan artifacts.
12
+ - Added `write-plan` support to the GitHub Action so CI can emit read-only plan outputs.
13
+ - Updated the GitHub Action example to show the sync, plan, and doctor flow.
14
+
3
15
  ## 0.1.14
4
16
 
5
17
  - Added `aienvmp plan` for read-only AI environment action plans.
package/README.md CHANGED
@@ -36,6 +36,8 @@ AIENV.md
36
36
  .aienvmp/manifest.json
37
37
  .aienvmp/intents.jsonl
38
38
  .aienvmp/timeline.jsonl
39
+ .aienvmp/plan.json
40
+ .aienvmp/plan.md
39
41
  .aienvmp/dashboard.html
40
42
  ```
41
43
 
@@ -61,7 +63,8 @@ aienvmp plan # read-only AI action plan, no automatic fixes
61
63
  aienvmp handoff # next-agent handoff summary + recommended actions
62
64
  aienvmp intent # record a planned env change
63
65
  aienvmp record # record what changed
64
- aienvmp doctor --ci # strict CI check
66
+ aienvmp doctor --ci # strict CI check for all warnings
67
+ aienvmp doctor --strict security # fail only scoped warnings
65
68
  ```
66
69
 
67
70
  ## Principles
@@ -70,6 +73,7 @@ aienvmp doctor --ci # strict CI check
70
73
  - AI-first
71
74
  - lightweight
72
75
  - one advisory engine, optional enforcement with `doctor --ci`
76
+ - scoped enforcement with `doctor --strict security|policy|coordination|all`
73
77
  - non-blocking unless strict mode is explicitly requested
74
78
  - security checks are opt-in and read-only
75
79
 
package/action.yml CHANGED
@@ -8,9 +8,13 @@ inputs:
8
8
  required: false
9
9
  default: "."
10
10
  strict:
11
- description: Fail when doctor reports warnings
11
+ description: Fail when doctor reports scoped warnings
12
12
  required: false
13
- default: "false"
13
+ default: "off"
14
+ write-plan:
15
+ description: Write read-only AI plan artifacts
16
+ required: false
17
+ default: "true"
14
18
 
15
19
  runs:
16
20
  using: composite
@@ -19,11 +23,20 @@ runs:
19
23
  shell: bash
20
24
  run: node "$GITHUB_ACTION_PATH/bin/aienvmp.js" sync --dir "${{ inputs.directory }}" --quiet
21
25
 
26
+ - name: Write AI plan
27
+ shell: bash
28
+ run: |
29
+ if [ "${{ inputs.write-plan }}" = "true" ]; then
30
+ node "$GITHUB_ACTION_PATH/bin/aienvmp.js" plan --dir "${{ inputs.directory }}" --write --json
31
+ fi
32
+
22
33
  - name: Doctor
23
34
  shell: bash
24
35
  run: |
25
36
  if [ "${{ inputs.strict }}" = "true" ]; then
26
37
  node "$GITHUB_ACTION_PATH/bin/aienvmp.js" doctor --dir "${{ inputs.directory }}" --ci
38
+ elif [ "${{ inputs.strict }}" != "false" ] && [ "${{ inputs.strict }}" != "off" ]; then
39
+ node "$GITHUB_ACTION_PATH/bin/aienvmp.js" doctor --dir "${{ inputs.directory }}" --strict "${{ inputs.strict }}"
27
40
  else
28
41
  node "$GITHUB_ACTION_PATH/bin/aienvmp.js" doctor --dir "${{ inputs.directory }}"
29
42
  fi
@@ -13,4 +13,5 @@ jobs:
13
13
  - uses: soovwv/aienvmp@main
14
14
  with:
15
15
  directory: "."
16
- strict: "false"
16
+ write-plan: "true"
17
+ strict: "off" # security, policy, coordination, all, or off
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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
@@ -98,7 +98,7 @@ Advanced:
98
98
  aienvmp snippet [agents|claude|gemini] [--write AGENTS.md]
99
99
  aienvmp compile [--dir .]
100
100
  aienvmp diff [--dir .]
101
- aienvmp doctor [--dir .] [--json] [--ci]
101
+ aienvmp doctor [--dir .] [--json] [--ci] [--strict security|policy|coordination|all]
102
102
  aienvmp dash [--dir .] [--open]
103
103
  `);
104
104
  }
@@ -2,9 +2,9 @@ import fs from "node:fs/promises";
2
2
  import { execFile } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import { diagnose } from "../doctor.js";
5
- import { readJson } from "../fsutil.js";
5
+ import { exists, readJson } from "../fsutil.js";
6
6
  import { openIntents, readJsonl, readTimeline } from "../timeline.js";
7
- import { dashboardPath, intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
7
+ import { dashboardPath, intentsPath, manifestPath, planJsonPath, planMdPath, timelinePath, workspaceDir } from "../paths.js";
8
8
  import { renderDashboard } from "../render.js";
9
9
  import { loadPolicy, policyWarnings } from "../policy.js";
10
10
  import { recommendedActions } from "../actions.js";
@@ -17,7 +17,8 @@ export async function dashWorkspace(args) {
17
17
  const intents = openIntents(await readJsonl(intentsPath(dir)));
18
18
  const policy = await loadPolicy(dir);
19
19
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
20
- const html = renderDashboard({ ...manifest, recommendedActions: recommendedActions(manifest, { warnings, intents }) }, timeline, warnings, intents, policy);
20
+ const planArtifacts = await detectedPlanArtifacts(dir);
21
+ const html = renderDashboard({ ...manifest, recommendedActions: recommendedActions(manifest, { warnings, intents }), planArtifacts }, timeline, warnings, intents, policy);
21
22
  const out = dashboardPath(dir);
22
23
  await fs.mkdir(path.dirname(out), { recursive: true });
23
24
  await fs.writeFile(out, html, "utf8");
@@ -26,6 +27,15 @@ export async function dashWorkspace(args) {
26
27
  return { dashboard: out };
27
28
  }
28
29
 
30
+ async function detectedPlanArtifacts(dir) {
31
+ const json = planJsonPath(dir);
32
+ const markdown = planMdPath(dir);
33
+ return {
34
+ json: await exists(json) ? ".aienvmp/plan.json" : "",
35
+ markdown: await exists(markdown) ? ".aienvmp/plan.md" : ""
36
+ };
37
+ }
38
+
29
39
  function openFile(file) {
30
40
  if (process.platform === "win32") {
31
41
  execFile("cmd", ["/c", "start", "", file], { windowsHide: true });
@@ -14,16 +14,18 @@ export async function doctorWorkspace(args) {
14
14
  const intents = openIntents(await readJsonl(intentsPath(dir)));
15
15
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
16
16
  const actions = recommendedActions(manifest, { warnings, intents });
17
+ const strict = strictResult(warnings, args);
17
18
  if (args.json) {
18
19
  console.log(JSON.stringify({
19
- status: warnings.length ? "warning" : "ok",
20
+ status: strict.fail ? "fail" : warnings.length ? "warning" : "ok",
20
21
  trust: manifest.trust || {},
21
22
  policy,
22
23
  openIntentCount: intents.length,
23
24
  warnings,
24
- recommendedActions: actions
25
+ recommendedActions: actions,
26
+ strict
25
27
  }, null, 2));
26
- if (args.ci && warnings.length) {
28
+ if (strict.fail) {
27
29
  process.exitCode = 1;
28
30
  }
29
31
  return;
@@ -39,8 +41,39 @@ export async function doctorWorkspace(args) {
39
41
  for (const item of actions) {
40
42
  console.log(`- [${item.priority}] ${item.summary}${item.command ? ` (${item.command})` : ""}`);
41
43
  }
42
- console.log("doctor: warnings are non-blocking by default; pass --ci to fail automation.");
43
- if (args.ci) {
44
+ console.log(`doctor: warnings are non-blocking by default; pass --ci or --strict ${strict.availableScopes.join("|")} to fail automation.`);
45
+ if (strict.fail) {
44
46
  process.exitCode = 1;
45
47
  }
46
48
  }
49
+
50
+ export function strictResult(warnings = [], args = {}) {
51
+ const scope = normalizeStrictScope(args.strict || (args.ci ? "all" : ""));
52
+ const matchedWarnings = scope ? warnings.filter((warning) => warningMatchesScope(warning, scope)) : [];
53
+ return {
54
+ enabled: Boolean(scope),
55
+ scope: scope || "off",
56
+ fail: matchedWarnings.length > 0,
57
+ matchedWarningCodes: matchedWarnings.map((warning) => warning.code),
58
+ availableScopes: ["security", "policy", "coordination", "all"]
59
+ };
60
+ }
61
+
62
+ function normalizeStrictScope(value) {
63
+ if (value === true) return "all";
64
+ const scope = String(value || "").trim().toLowerCase();
65
+ if (!scope || scope === "false" || scope === "off") return "";
66
+ if (["security", "policy", "coordination", "all"].includes(scope)) return scope;
67
+ return "all";
68
+ }
69
+
70
+ function warningMatchesScope(warning, scope) {
71
+ if (scope === "all") return true;
72
+ return warningScope(warning.code) === scope;
73
+ }
74
+
75
+ function warningScope(code = "") {
76
+ if (code === "security-vulnerabilities") return "security";
77
+ if (["conflicting-open-intents", "stale-open-intent", "handoff-stale"].includes(code)) return "coordination";
78
+ return "policy";
79
+ }
package/src/render.js CHANGED
@@ -252,7 +252,7 @@ const sec=manifest.security||{};
252
252
  const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
253
253
  const secPackages=sec.topPackages||[];
254
254
  const securityFix=p=>p.fixVersions?.length?\`fix \${p.fixVersions.slice(0,3).join(', ')}\`:(p.fixAvailable?'fix available':'review required');
255
- const securityRefs=p=>p.advisories?.length?\` · \${p.advisories.map(a=>a.id||a.title).filter(Boolean).slice(0,2).join(', ')}\`:'';
255
+ const securityRefs=p=>p.advisories?.length?\` - \${p.advisories.map(a=>a.id||a.title).filter(Boolean).slice(0,2).join(', ')}\`:'';
256
256
  const securityHtml=sec.enabled?\`<table><tr><th>Total</th><td><code>\${esc(secSummary.total||0)}</code></td></tr><tr><th>Critical</th><td><code>\${esc(secSummary.critical||0)}</code></td></tr><tr><th>High</th><td><code>\${esc(secSummary.high||0)}</code></td></tr><tr><th>Moderate</th><td><code>\${esc(secSummary.moderate||0)}</code></td></tr><tr><th>Low</th><td><code>\${esc(secSummary.low||0)}</code></td></tr></table>\${secPackages.length?'<div class="timeline">'+secPackages.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.severity)}</time><div><b>\${esc(p.name)}</b> \${esc(securityFix(p))}\${esc(securityRefs(p))}</div></div>\`).join('')+'</div>':'<div class="okline" style="margin-top:10px">No vulnerable packages reported.</div>'}\`:'<div class="okline">Security scan is off. Run <code>aienvmp sync --security</code> for read-only vulnerability summary.</div>';
257
257
  const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
258
258
  const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
@@ -264,6 +264,8 @@ const intentsHtml=intents.length?'<div class="timeline">'+intents.slice(-6).reve
264
264
  const policyHtml=entries(policy).length?\`<table>\${rows(policy)}</table>\`:'<div class="okline">No explicit version policy set.</div>';
265
265
  const actions=manifest.recommendedActions||[];
266
266
  const actionsHtml=actions.length?'<div class="timeline">'+actions.slice(0,6).map(a=>\`<div class="event"><time>\${esc(a.priority)}</time><div><b>\${esc(a.category)}</b> \${esc(a.summary)}\${a.command?\`<div class="path">\${esc(a.command)}</div>\`:''}</div></div>\`).join('')+'</div>':'<div class="okline">No recommended actions. Continue project-local work.</div>';
267
+ const plan=manifest.planArtifacts||{};
268
+ const planHtml=plan.markdown||plan.json?\`<table><tr><th>Markdown</th><td>\${plan.markdown?'<a href="plan.md">plan.md</a>':'not written'}</td></tr><tr><th>JSON</th><td>\${plan.json?'<a href="plan.json">plan.json</a>':'not written'}</td></tr></table>\`:'<div class="okline">No plan artifacts yet. Run <code>aienvmp plan --write</code>.</div>';
267
269
  const card=(title,badge,body)=>\`<section class="card"><div class="card-head"><h2>\${title}</h2>\${badge||''}</div>\${body}</section>\`;
268
270
  const reviewRequired=warnings.length>0||intents.length>0;
269
271
  const recentChanges=timeline.slice(-8).length;
@@ -305,6 +307,8 @@ document.getElementById('app').innerHTML=\`
305
307
  <aside>
306
308
  \${card('Recommended Actions','<span class="pill">'+actions.length+' actions</span>',actionsHtml)}
307
309
  <div style="height:14px"></div>
310
+ \${card('AI Plan Artifacts',plan.markdown||plan.json?'<span class="pill">written</span>':'<span class="pill off">not written</span>',planHtml)}
311
+ <div style="height:14px"></div>
308
312
  \${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
309
313
  <div style="height:14px"></div>
310
314
  \${card('Version Policy','<span class="pill">'+entries(policy).length+' rules</span>',policyHtml)}