aienvmp 0.1.41 → 0.1.43

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 CHANGED
@@ -34,6 +34,18 @@ 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
+
43
+ ### Record follow-up loop needed platform verification
44
+
45
+ - Issue: dependency/security records need to guide the next AI back through sync, status, and handoff without forcing operations.
46
+ - Fix: `record` timeline entries now include follow-up metadata, and status/context/dashboard surface unresolved follow-ups.
47
+ - Verification: Windows and macOS candidate smoke checks confirmed `record --target dependency` appears in `status --json` followUps and the dashboard Follow-ups card.
48
+
37
49
  ## Template
38
50
 
39
51
  ### Title
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.43
4
+
5
+ - Added follow-up metadata to `record` timeline entries so dependency/security changes point agents back to sync, status, and handoff.
6
+ - Added timeline follow-up summarization so unresolved dependency/security records can be surfaced consistently.
7
+ - Surfaced pending follow-ups in status/preflight and context outputs.
8
+ - Added a dashboard Follow-ups card for unresolved env/SBOM record refresh work.
9
+ - Updated the README change loop to show record, sync, status, and handoff as one simple continuation flow.
10
+ - Documented Windows/macOS candidate verification for the record follow-up loop.
11
+
12
+ ## 0.1.42
13
+
14
+ - Added explicit enforcement gate metadata so AI and CI consumers know local checks are warn-only unless `--strict` or `--ci` is requested.
15
+ - Added `doctor --json` exit behavior metadata to distinguish advisory warnings from strict failure conditions.
16
+ - Surfaced enforcement gate details in context, plan, and dashboard outputs.
17
+ - Extended the GitHub Action to write `.aienvmp/schema.json` and `.aienvmp/doctor.json` artifacts by default.
18
+ - Clarified README guidance for advisory-by-default behavior and opt-in strict/CI failure.
19
+ - Documented advisory doctor exit behavior and strict verification steps in troubleshooting and bugfix notes.
20
+
3
21
  ## 0.1.41
4
22
 
5
23
  - Added preflight contract metadata so AI and CI consumers can rely on stable entry fields while ignoring additive changes.
package/README.md CHANGED
@@ -24,10 +24,13 @@ Before environment changes:
24
24
  ```bash
25
25
  npx aienvmp intent --actor agent:codex --action "change dependency" --target dependency
26
26
  npx aienvmp record --actor agent:codex --summary "dependency-change" --target dependency
27
+ npx aienvmp sync
28
+ npx aienvmp status --write
27
29
  npx aienvmp handoff --record --actor agent:codex
28
30
  ```
29
31
 
30
32
  Use `--dir <workspace>` when AI or CI runs outside the target project.
33
+ Warnings are advisory by default. Use `doctor --strict <scope>` only when you want CI-style failure.
31
34
 
32
35
  ## What It Creates
33
36
 
@@ -48,9 +51,11 @@ AIENV.md # Markdown env map for AI agents
48
51
  - `status.json.nextAgent` tells the next AI what to read and whether to review first.
49
52
  - `dependencyReadSet` lists manifests and lockfiles before package or security changes.
50
53
  - `coordination.conflictTargets` shows where multiple agents are planning changes.
54
+ - `followUps` shows records that still need `sync`, `status`, or `handoff`.
51
55
  - `handoff` carries dependency read-set and protocol guidance for the next AI.
52
56
  - Light SBOM includes source/confidence hints; verify security claims with dedicated scanners.
53
- - Everything is advisory by default; strict failure is opt-in with `doctor --strict`.
57
+ - `enforcementProfile.gate` explains when checks warn, fail, and set exit codes.
58
+ - Everything is advisory by default; strict failure is opt-in with `doctor --strict` or `--ci`.
54
59
 
55
60
  ## Agent Files
56
61
 
@@ -78,6 +83,8 @@ aienvmp doctor --strict security|policy|coordination|all
78
83
 
79
84
  ## CI
80
85
 
86
+ The GitHub Action writes status, schema, doctor, plan, and dashboard artifacts. `strict: "off"` reports warnings without failing the job.
87
+
81
88
  ```yaml
82
89
  - uses: soovwv/aienvmp@main
83
90
  with:
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,12 +21,13 @@ export async function contextWorkspace(args) {
21
21
  const decision = aiDecision(warnings, intents);
22
22
  const actions = recommendedActions(manifest, { warnings, intents });
23
23
  const stepSummary = compactStepSummary(buildPlan(manifest, warnings, intents, policy));
24
- const preflight = buildPreflight(manifest, warnings, intents);
24
+ const preflight = buildPreflight(manifest, warnings, intents, timeline);
25
25
  if (args.json) {
26
26
  console.log(JSON.stringify({
27
27
  status: warnings.length ? "review-required" : "clear",
28
28
  preflight,
29
29
  coordination: preflight.coordination,
30
+ followUps: preflight.followUps,
30
31
  decision,
31
32
  enforcement: enforcementAdvice(warnings),
32
33
  recommendedActions: actions,
@@ -50,7 +51,7 @@ export async function contextWorkspace(args) {
50
51
  }, null, 2));
51
52
  return;
52
53
  }
53
- console.log(renderContext(manifest, timeline, warnings, intents, policy, actions));
54
+ console.log(renderContext({ ...manifest, preflight }, timeline, warnings, intents, policy, actions));
54
55
  }
55
56
 
56
57
  function lightSbomSummary(lightSbom = {}) {
@@ -24,7 +24,7 @@ export async function dashWorkspace(args) {
24
24
  const planEnvironment = await detectedPlanEnvironment(dir);
25
25
  const html = renderDashboard({
26
26
  ...manifest,
27
- preflight: buildPreflight(manifest, warnings, intents),
27
+ preflight: buildPreflight(manifest, warnings, intents, timeline),
28
28
  recommendedActions: recommendedActions(manifest, { warnings, intents }),
29
29
  planArtifacts,
30
30
  planRemediation,
@@ -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,
@@ -46,7 +46,7 @@ async function recordHandoff(file, handoff, actor) {
46
46
  export function buildHandoff(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
47
47
  const reviewRequired = warnings.length > 0 || intents.length > 0;
48
48
  const actions = recommendedActions(manifest, { warnings, intents });
49
- const preflight = buildPreflight(manifest, warnings, intents);
49
+ const preflight = buildPreflight(manifest, warnings, intents, timeline);
50
50
  return {
51
51
  status: reviewRequired ? "review-required" : "clear",
52
52
  trust: manifest.trust || {},
@@ -18,7 +18,7 @@ export async function planWorkspace(args) {
18
18
  const intents = openIntents(await readJsonl(intentsPath(dir)));
19
19
  const policy = await loadPolicy(dir);
20
20
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
21
- const plan = buildPlan(manifest, warnings, intents, policy);
21
+ const plan = buildPlan(manifest, warnings, intents, policy, timeline);
22
22
 
23
23
  if (args.write) {
24
24
  await writeJson(planJsonPath(dir), plan);
@@ -33,14 +33,14 @@ export async function planWorkspace(args) {
33
33
  return plan;
34
34
  }
35
35
 
36
- export function buildPlan(manifest, warnings = [], intents = [], policy = {}) {
36
+ export function buildPlan(manifest, warnings = [], intents = [], policy = {}, timeline = []) {
37
37
  const actions = recommendedActions(manifest, { warnings, intents });
38
38
  const status = warnings.length || intents.length ? "review-required" : "clear";
39
39
  return {
40
40
  schemaVersion: 1,
41
41
  generatedAt: new Date().toISOString(),
42
42
  status,
43
- preflight: buildPreflight(manifest, warnings, intents),
43
+ preflight: buildPreflight(manifest, warnings, intents, timeline),
44
44
  workspace: manifest.workspace || {},
45
45
  trust: manifest.trust || {},
46
46
  decision: aiDecision(warnings, intents),
@@ -18,12 +18,37 @@ export async function recordWorkspace(args) {
18
18
  after: args.after || "",
19
19
  evidence: args.evidence || "",
20
20
  requiresReview,
21
+ followUp: followUpForRecord({ target: args.target, summary }),
21
22
  trust: changedTrust(now, requiresReview)
22
23
  };
23
24
  await appendJsonLine(timelinePath(dir), entry);
24
25
  console.log(`recorded ${entry.type} by ${actor}`);
25
26
  }
26
27
 
28
+ export function followUpForRecord(record = {}) {
29
+ const target = String(record.target || "").toLowerCase();
30
+ const text = `${record.summary || ""} ${target}`.toLowerCase();
31
+ const isDependency = ["dependency", "package", "lockfile", "vulnerab", "security", "npm", "pnpm", "yarn", "pip", "uv"].some((item) => text.includes(item));
32
+ const isEnvironment = isDependency || ["node", "python", "docker", "runtime", "package-manager", "global"].some((item) => text.includes(item));
33
+ if (!isEnvironment) return {
34
+ required: false,
35
+ reason: "No environment follow-up detected.",
36
+ commands: []
37
+ };
38
+ return {
39
+ required: true,
40
+ target: isDependency ? "dependency" : target || "environment",
41
+ reason: isDependency
42
+ ? "Dependency or security records should refresh the env map and handoff context."
43
+ : "Environment records should refresh the env map and handoff context.",
44
+ commands: [
45
+ "aienvmp sync",
46
+ "aienvmp status --write",
47
+ "aienvmp handoff --record --actor agent:id"
48
+ ]
49
+ };
50
+ }
51
+
27
52
  function required(value, name) {
28
53
  if (!value) throw new Error(`record: --${name} is required`);
29
54
  return String(value);
@@ -15,7 +15,7 @@ export async function statusWorkspace(args) {
15
15
  const timeline = await readTimeline(timelinePath(dir));
16
16
  const intents = openIntents(await readJsonl(intentsPath(dir)));
17
17
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
18
- const status = buildStatus(manifest, warnings, intents);
18
+ const status = buildStatus(manifest, warnings, intents, timeline);
19
19
  const artifact = args.write ? await writeStatusArtifact(dir, status) : "";
20
20
  const output = artifact ? { ...status, artifact } : status;
21
21
  if (args.json) {
@@ -39,6 +39,6 @@ export async function writeStatusArtifact(dir, status) {
39
39
  return out;
40
40
  }
41
41
 
42
- export function buildStatus(manifest = {}, warnings = [], intents = []) {
43
- return buildPreflight(manifest, warnings, intents);
42
+ export function buildStatus(manifest = {}, warnings = [], intents = [], timeline = []) {
43
+ return buildPreflight(manifest, warnings, intents, timeline);
44
44
  }
package/src/contract.js CHANGED
@@ -4,7 +4,7 @@ export function preflightContract() {
4
4
  version: 1,
5
5
  stability: "additive",
6
6
  requiredFields: ["schemaVersion", "state", "decision", "quickstart", "commands", "artifacts"],
7
- aiEntryFields: ["state", "summary", "nextAgent", "coordination", "dependencyReadSet", "dependencyChangeProtocol"],
7
+ aiEntryFields: ["state", "summary", "nextAgent", "coordination", "followUps", "dependencyReadSet", "dependencyChangeProtocol"],
8
8
  rule: "Consumers should ignore unknown fields and treat missing optional fields as unavailable."
9
9
  };
10
10
  }
@@ -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,9 +1,10 @@
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
4
  import { preflightContract } from "./contract.js";
5
+ import { pendingFollowUps } from "./timeline.js";
5
6
 
6
- export function buildPreflight(manifest = {}, warnings = [], intents = []) {
7
+ export function buildPreflight(manifest = {}, warnings = [], intents = [], timeline = []) {
7
8
  const decision = aiDecision(warnings, intents);
8
9
  const enforcement = enforcementAdvice(warnings);
9
10
  const actions = recommendedActions(manifest, { warnings, intents });
@@ -13,6 +14,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
13
14
  const dependencyReadSet = dependencyPreflightReadSet(manifest);
14
15
  const dependencyChangeProtocol = dependencyProtocol(manifest, dependencyReadSet);
15
16
  const coordination = coordinationSummary(intents);
17
+ const followUps = pendingFollowUps(timeline);
16
18
  return {
17
19
  schemaVersion: 1,
18
20
  contract: preflightContract(),
@@ -27,6 +29,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
27
29
  localOperation: "non-blocking",
28
30
  strictMode: "optional",
29
31
  strictUse: "CI or explicit human-requested checks only",
32
+ gate: enforcementGate(""),
30
33
  reason: "Avoid disrupting shared servers or developer machines while still making drift visible.",
31
34
  recommendedStrictCommand: enforcement.recommendedCommand,
32
35
  strictCommands: [
@@ -52,6 +55,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = []) {
52
55
  quickstart: agentQuickstart(decision.reviewRequired),
53
56
  nextAgent: nextAgentHint(state, dependencyReadSet, dependencyChangeProtocol),
54
57
  coordination,
58
+ followUps,
55
59
  intentTargets,
56
60
  dependencyReadSet,
57
61
  dependencyChangeProtocol,
package/src/render.js CHANGED
@@ -170,6 +170,12 @@ 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
+ "",
176
+ "Follow-ups:",
177
+ ...followUpLines(manifest.preflight?.followUps),
178
+ "",
173
179
  "Open intents:",
174
180
  ...(intents.length ? intents.slice(-5).reverse().map((i) => `- ${i.actor}: ${i.action}`) : ["- none"]),
175
181
  "",
@@ -267,6 +273,9 @@ export function renderPlan(plan) {
267
273
  "Review gates:",
268
274
  ...plan.reviewGates.map((item) => `- ${item}`),
269
275
  "",
276
+ "Enforcement gate:",
277
+ ...enforcementGateLines(plan.preflight?.enforcementProfile?.gate),
278
+ "",
270
279
  "Dependency protocol:",
271
280
  ...dependencyProtocolPlanLines(plan.preflight?.dependencyChangeProtocol),
272
281
  "",
@@ -294,6 +303,23 @@ function dependencyProtocolPlanLines(protocol = {}) {
294
303
  ];
295
304
  }
296
305
 
306
+ function enforcementGateLines(gate = {}) {
307
+ return [
308
+ `- Default: ${gate.defaultMode || "advisory"} (${gate.localDefault || "warn-only"})`,
309
+ `- Strict: ${gate.strictMode || "off"}`,
310
+ `- Fail condition: ${gate.failCondition || "never in default mode"}`,
311
+ `- Exit code: ${gate.exitCode || "0 unless the command itself errors"}`
312
+ ];
313
+ }
314
+
315
+ function followUpLines(followUps = []) {
316
+ if (!followUps.length) return ["- none"];
317
+ return followUps.slice(0, 5).map((item) => {
318
+ const command = item.commands?.[0] ? ` (${item.commands[0]})` : "";
319
+ return `- ${item.target || "environment"}: ${item.summary || item.reason || "follow-up required"}${command}`;
320
+ });
321
+ }
322
+
297
323
  function environmentLines(item) {
298
324
  return [
299
325
  `- ${item.category}: ${item.summary}`,
@@ -414,11 +440,14 @@ const ciHasFailure=ciReadiness.some(s=>s.status==='fail');
414
440
  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>';
415
441
  const enforcementProfile=manifest.preflight?.enforcementProfile||{};
416
442
  const strictCommands=enforcementProfile.strictCommands||[];
417
- const enforcementHtml=\`<table><tr><th>Default</th><td><code>\${esc(enforcementProfile.defaultMode||'advisory')}</code></td></tr><tr><th>Local</th><td>\${esc(enforcementProfile.localOperation||'non-blocking')}</td></tr><tr><th>Strict</th><td>\${esc(enforcementProfile.strictUse||'CI or explicit checks only')}</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(enforcementProfile.reason||'Warnings stay advisory unless strict mode is requested.')}</div>\`;
443
+ const gate=enforcementProfile.gate||{};
444
+ 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>\`;
418
445
  const contract=manifest.preflight?.contract||{};
419
446
  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>';
420
447
  const intentTargets=manifest.preflight?.intentTargets||[];
421
448
  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>';
449
+ const followUps=manifest.preflight?.followUps||[];
450
+ const followUpsHtml=followUps.length?'<div class="timeline">'+followUps.slice(0,5).map(f=>\`<div class="event"><time>\${esc(f.target||'env')}</time><div><b>\${esc(f.summary||'follow-up')}</b> \${esc(f.reason||'Refresh shared context.')}\${f.commands?.length?\`<div class="path">\${esc(f.commands.join(' -> '))}</div>\`:''}</div></div>\`).join('')+'</div>':'<div class="okline">No pending follow-ups after environment records.</div>';
422
451
  const dependencyReadSet=manifest.preflight?.dependencyReadSet||[];
423
452
  const dependencyReadSetHtml=dependencyReadSet.length?'<div class="timeline">'+dependencyReadSet.slice(0,5).map(d=>\`<div class="event"><time>\${esc(d.ecosystem||'deps')}</time><div><b>\${esc(d.manifest||'dependency files')}</b> <code>\${esc(d.manager||'unknown')}</code><div class="path">\${esc([d.manifest,...(d.lockfiles||[])].filter(Boolean).join(', '))}</div>\${d.riskPackages?.length?\`<div class="path">risk: \${esc(d.riskPackages.join(', '))}</div>\`:''}</div></div>\`).join('')+'</div>':'<div class="okline">No dependency files detected.</div>';
424
453
  const dependencyProtocol=manifest.preflight?.dependencyChangeProtocol||{};
@@ -473,6 +502,8 @@ document.getElementById('app').innerHTML=\`
473
502
  <div style="height:14px"></div>
474
503
  \${card('AI Intent Targets','<span class="pill">'+intentTargets.length+' targets</span>',intentTargetsHtml)}
475
504
  <div style="height:14px"></div>
505
+ \${card('Follow-ups',followUps.length?'<span class="pill warn">'+followUps.length+' pending</span>':'<span class="pill">clear</span>',followUpsHtml)}
506
+ <div style="height:14px"></div>
476
507
  \${card('AI Contract','<span class="pill">'+(contract.stability||'additive')+'</span>',contractHtml)}
477
508
  <div style="height:14px"></div>
478
509
  \${card('Dependency Read Set','<span class="pill">'+dependencyReadSet.length+' files</span>',dependencyReadSetHtml)}
package/src/timeline.js CHANGED
@@ -44,3 +44,26 @@ export function newIntentID(now = new Date()) {
44
44
  const entropy = Math.random().toString(36).slice(2, 8);
45
45
  return `int_${time}_${entropy}`;
46
46
  }
47
+
48
+ export function pendingFollowUps(timeline = []) {
49
+ const lastSync = [...timeline].reverse().find((item) => item.type === "sync" || item.type === "detected-change");
50
+ const lastHandoff = [...timeline].reverse().find((item) => item.type === "agent-handoff");
51
+ const lastSyncAt = lastSync ? new Date(lastSync.at).getTime() : 0;
52
+ const lastHandoffAt = lastHandoff ? new Date(lastHandoff.at).getTime() : 0;
53
+ return timeline
54
+ .filter((item) => item.followUp?.required)
55
+ .filter((item) => {
56
+ const at = new Date(item.at).getTime();
57
+ return at > lastSyncAt || at > lastHandoffAt;
58
+ })
59
+ .slice(-5)
60
+ .reverse()
61
+ .map((item) => ({
62
+ at: item.at,
63
+ actor: item.actor || "unknown",
64
+ target: item.followUp.target || item.target || "environment",
65
+ summary: item.summary || item.type || "environment record",
66
+ reason: item.followUp.reason || "Follow-up is required.",
67
+ commands: item.followUp.commands || []
68
+ }));
69
+ }