diff --git a/.changes/adhd/work-resolve-issue-issue-13080-41774896462-1-4bce75c3.md b/.changes/adhd/work-resolve-issue-issue-13080-41774896462-1-4bce75c3.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/adhd/work-resolve-issue-issue-13080-41774896462-1-4bce75c3.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/.changes/ai-briefing/work-resolve-issue-issue-13080-41774896462-1-6e3aeb2f.md b/.changes/ai-briefing/work-resolve-issue-issue-13080-41774896462-1-6e3aeb2f.md new file mode 100644 index 00000000000..6fb2d7acd0d --- /dev/null +++ b/.changes/ai-briefing/work-resolve-issue-issue-13080-41774896462-1-6e3aeb2f.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Fixed + +- **The retired `~/.claude/rendered-views.md` notice no longer points at a `medium` option this plugin does not ship.** The shared medium resolver adds "; per user, the plugin's medium option also works" only for a lane whose plugin ships a `medium` userConfig option, which each lane registration in `lib/view-registry.mjs` now records as `hasMediumOption`. `generate` now prints every notice as the resolver gives it, in place of rewriting that notice by hand. diff --git a/.changes/architecture/work-resolve-issue-issue-13080-41774896462-1-8298b4cc.md b/.changes/architecture/work-resolve-issue-issue-13080-41774896462-1-8298b4cc.md new file mode 100644 index 00000000000..83628ebcbcf --- /dev/null +++ b/.changes/architecture/work-resolve-issue-issue-13080-41774896462-1-8298b4cc.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Fixed + +- **The retired `~/.claude/rendered-views.md` notice no longer points at a `medium` option this plugin does not ship.** The shared medium resolver adds "; per user, the plugin's medium option also works" only for a lane whose plugin ships a `medium` userConfig option, which each lane registration in `lib/view-registry.mjs` now records as `hasMediumOption`. diff --git a/.changes/codebase-health/work-resolve-issue-issue-13080-41774896462-1-f4d20642.md b/.changes/codebase-health/work-resolve-issue-issue-13080-41774896462-1-f4d20642.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/codebase-health/work-resolve-issue-issue-13080-41774896462-1-f4d20642.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/.changes/debugging/work-resolve-issue-issue-13080-41774896462-1-6d96af98.md b/.changes/debugging/work-resolve-issue-issue-13080-41774896462-1-6d96af98.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/debugging/work-resolve-issue-issue-13080-41774896462-1-6d96af98.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/.changes/discovery/work-resolve-issue-issue-13080-41774896462-1-50387251.md b/.changes/discovery/work-resolve-issue-issue-13080-41774896462-1-50387251.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/discovery/work-resolve-issue-issue-13080-41774896462-1-50387251.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/.changes/education/work-resolve-issue-issue-13080-41774896462-1-6421e514.md b/.changes/education/work-resolve-issue-issue-13080-41774896462-1-6421e514.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/education/work-resolve-issue-issue-13080-41774896462-1-6421e514.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/.changes/harness-ops/work-resolve-issue-issue-13080-41774896462-1-aa932a83.md b/.changes/harness-ops/work-resolve-issue-issue-13080-41774896462-1-aa932a83.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/harness-ops/work-resolve-issue-issue-13080-41774896462-1-aa932a83.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/.changes/machine-health/work-resolve-issue-issue-13080-41774896462-1-00b80f20.md b/.changes/machine-health/work-resolve-issue-issue-13080-41774896462-1-00b80f20.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/machine-health/work-resolve-issue-issue-13080-41774896462-1-00b80f20.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/.changes/overengineering/work-resolve-issue-issue-13080-41774896462-1-5d846b7e.md b/.changes/overengineering/work-resolve-issue-issue-13080-41774896462-1-5d846b7e.md new file mode 100644 index 00000000000..a98220251a7 --- /dev/null +++ b/.changes/overengineering/work-resolve-issue-issue-13080-41774896462-1-5d846b7e.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Fixed + +- **The retired `~/.claude/rendered-views.md` notice no longer points at a `medium` option this plugin does not ship.** The shared medium resolver adds "; per user, the plugin's medium option also works" only for a lane whose plugin ships a `medium` userConfig option, which each lane registration in `lib/view-registry.mjs` now records as `hasMediumOption`. The audit's retired-file eval no longer expects the clause. diff --git a/.changes/planning/work-resolve-issue-issue-13080-41774896462-1-48336f48.md b/.changes/planning/work-resolve-issue-issue-13080-41774896462-1-48336f48.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/planning/work-resolve-issue-issue-13080-41774896462-1-48336f48.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/.changes/prototype/work-resolve-issue-issue-13080-41774896462-1-a979137d.md b/.changes/prototype/work-resolve-issue-issue-13080-41774896462-1-a979137d.md new file mode 100644 index 00000000000..f819514c02e --- /dev/null +++ b/.changes/prototype/work-resolve-issue-issue-13080-41774896462-1-a979137d.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Fixed + +- **The retired `~/.claude/rendered-views.md` notice no longer points at a `medium` option this plugin does not ship.** The shared medium resolver adds "; per user, the plugin's medium option also works" only for a lane whose plugin ships a `medium` userConfig option, which each lane registration in `lib/view-registry.mjs` now records as `hasMediumOption`. `explore-directions` and `pressure-test` now print every notice as it stands, with no clause to drop by hand. diff --git a/.changes/review/work-resolve-issue-issue-13080-41774896462-1-1cb05545.md b/.changes/review/work-resolve-issue-issue-13080-41774896462-1-1cb05545.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/review/work-resolve-issue-issue-13080-41774896462-1-1cb05545.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/.changes/source-control/work-resolve-issue-issue-13080-41774896462-1-09538c3f.md b/.changes/source-control/work-resolve-issue-issue-13080-41774896462-1-09538c3f.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/source-control/work-resolve-issue-issue-13080-41774896462-1-09538c3f.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/.changes/visualization/work-resolve-issue-issue-13080-41774896462-1-c88effd8.md b/.changes/visualization/work-resolve-issue-issue-13080-41774896462-1-c88effd8.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/visualization/work-resolve-issue-issue-13080-41774896462-1-c88effd8.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/.changes/work-items/work-resolve-issue-issue-13080-41774896462-1-862a445b.md b/.changes/work-items/work-resolve-issue-issue-13080-41774896462-1-862a445b.md new file mode 100644 index 00000000000..d019ab5d6d1 --- /dev/null +++ b/.changes/work-items/work-resolve-issue-issue-13080-41774896462-1-862a445b.md @@ -0,0 +1,7 @@ +--- +bump: patch +--- + +### Changed + +- **The shared view registry records whether each lane's plugin ships a `medium` option.** `lib/view-registry.mjs` gains `hasMediumOption` on every lane registration, and the retired `~/.claude/rendered-views.md` notice names the plugin's `medium` option only for a lane where it is set; this plugin's notices are unchanged. diff --git a/lib/view-registry.mjs b/lib/view-registry.mjs index a74d5f92dfa..9b724c94017 100644 --- a/lib/view-registry.mjs +++ b/lib/view-registry.mjs @@ -3,12 +3,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -40,28 +41,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/lib/view-settings.mjs b/lib/view-settings.mjs index b5b137e61b5..91563132708 100755 --- a/lib/view-settings.mjs +++ b/lib/view-settings.mjs @@ -181,7 +181,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -213,7 +213,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -243,7 +243,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/lib/view-settings.test.mjs b/lib/view-settings.test.mjs index 429034f0020..dfc18e624d6 100644 --- a/lib/view-settings.test.mjs +++ b/lib/view-settings.test.mjs @@ -15,7 +15,7 @@ import { tempDir } from "./test-tmp.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); const CLI = join(HERE, "view-settings.mjs"); const { resolveMedium, gateExplicit } = await import("./view-settings.mjs"); -const { reportLines } = await import("./view-registry.mjs"); +const { LANES, reportLines } = await import("./view-registry.mjs"); const TEAM = "docs/conventions/rendered-views.yaml"; const PERSONAL = "docs/conventions/rendered-views.local.yaml"; @@ -224,6 +224,18 @@ describe("retired rendered-views files", () => { assert.equal(run(tempDir("view-settings-other-plugin-")).length, 3); assert.ok(readdirSync(join(dataDir, "view-settings")).length > 0, "state is keyed under the plugin data directory"); }); + test("the retired user file's notice names the plugin's medium option only when the plugin ships one", () => { + const home = userHome({ ".claude/rendered-views.md": "medium: file\n" }); + const userNotice = (lane) => resolve({ lane }, { userHome: home }).notices.find((n) => n.path === "~/.claude/rendered-views.md").text; + assert.ok(userNotice("planning").endsWith("; per user, the planning plugin's medium option also works")); + assert.doesNotMatch(userNotice("overengineering:audit"), /per user/); + }); + test("each lane's hasMediumOption matches whether its plugin ships a medium option", () => { + for (const lane of Object.values(LANES)) { + const manifest = JSON.parse(readFileSync(join(HERE, "..", "plugins", lane.laneId.split(":")[0], ".claude-plugin", "plugin.json"), "utf8")); + assert.equal(lane.hasMediumOption, Object.hasOwn(manifest.userConfig ?? {}, "medium"), lane.laneId); + } + }); }); describe("a retired file's terminal or file value keeps the view local", () => { diff --git a/plugins/adhd/lib/view-registry.mjs b/plugins/adhd/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/adhd/lib/view-registry.mjs +++ b/plugins/adhd/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/adhd/lib/view-settings.mjs b/plugins/adhd/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/adhd/lib/view-settings.mjs +++ b/plugins/adhd/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/ai-briefing/lib/view-registry.mjs b/plugins/ai-briefing/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/ai-briefing/lib/view-registry.mjs +++ b/plugins/ai-briefing/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/ai-briefing/lib/view-settings.mjs b/plugins/ai-briefing/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/ai-briefing/lib/view-settings.mjs +++ b/plugins/ai-briefing/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/ai-briefing/skills/generate/SKILL.md b/plugins/ai-briefing/skills/generate/SKILL.md index 7dcb50dc7cb..933f7a617ff 100644 --- a/plugins/ai-briefing/skills/generate/SKILL.md +++ b/plugins/ai-briefing/skills/generate/SKILL.md @@ -29,17 +29,9 @@ CLAUDE_PLUGIN_DATA="${CLAUDE_PLUGIN_DATA}" node "${CLAUDE_PLUGIN_ROOT}/lib/view- It prints one JSON line, then the report line naming the medium and what decided it, then one line per notice. Print the report line and the notices in the report as they stand (step 9), -next to the format's source, which alone says what the briefing was emitted as. One exception: -the notice for a retired `~/.claude/rendered-views.md` ends "set it in the ai-briefing plugin's -medium option instead", and this plugin has no such option. Print that notice with -`docs/conventions/rendered-views.local.yaml` (the personal override) in place of "the -ai-briefing plugin's medium option". When that notice names a value (`(it sets medium: …)`), -also append `; a publishing value there applies only while that file is untracked and -gitignored`, since the resolver ignores a publishing value in a committed personal override. -Which values publish is the resolver's to know, so this caveat holds whatever the value is. Leave -every other notice as it stands. Do -not read or weigh the rendered-views convention files yourself. When `node` is missing, say so in -one line and carry on. +next to the format's source, which alone says what the briefing was emitted as. Do not read or +weigh the rendered-views convention files yourself. When `node` is missing, say so in one line and +carry on. ## Source and access policy diff --git a/plugins/ai-briefing/skills/generate/evals/evals.json b/plugins/ai-briefing/skills/generate/evals/evals.json index 1c97548e497..fb6bac10ac9 100644 --- a/plugins/ai-briefing/skills/generate/evals/evals.json +++ b/plugins/ai-briefing/skills/generate/evals/evals.json @@ -140,7 +140,7 @@ "id": 11, "name": "retired-rendered-views-file-gives-notice", "prompt": "Catch me up on AI from the last week. (Fixture: ~/.claude/rendered-views.md exists and sets medium: artifact, and its notice has not been shown in this project before; the repository has no rendered-views convention file; no --format argument was given.)", - "expected_output": "A markdown briefing covering roughly the last 7 days. The skill takes the medium from the shared resolver, not from the retired file, and prints the resolver's report line plus its notice that ~/.claude/rendered-views.md is no longer read, with docs/conventions/rendered-views.local.yaml as the place to set it, since this plugin has no medium option, and the caveat that a publishing value there applies only while that file is untracked and gitignored.", + "expected_output": "A markdown briefing covering roughly the last 7 days. The skill takes the medium from the shared resolver, not from the retired file, and prints the resolver's report line plus its notice that ~/.claude/rendered-views.md is no longer read, with docs/conventions/rendered-views.local.yaml as the place to set it, since this plugin has no medium option, and the resolver's caveat that a publishing value there applies only while that file is untracked and gitignored.", "files": [], "narration": true, "expectations": [ @@ -148,7 +148,7 @@ "The skill does not read ~/.claude/rendered-views.md itself or take a value from it", "The report carries the resolver's 'view:' report line and its notice that ~/.claude/rendered-views.md is no longer read", "The notice points the user at docs/conventions/rendered-views.local.yaml, not at an ai-briefing plugin medium option, which does not exist", - "Because the retired file's notice names a value, the notice ends with '; a publishing value there applies only while that file is untracked and gitignored'" + "The notice is printed as the resolver gives it, ending with '; it publishes only while that file is untracked and gitignored'" ] } ] diff --git a/plugins/architecture/lib/view-registry.mjs b/plugins/architecture/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/architecture/lib/view-registry.mjs +++ b/plugins/architecture/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/architecture/lib/view-settings.mjs b/plugins/architecture/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/architecture/lib/view-settings.mjs +++ b/plugins/architecture/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/codebase-health/lib/view-registry.mjs b/plugins/codebase-health/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/codebase-health/lib/view-registry.mjs +++ b/plugins/codebase-health/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/codebase-health/lib/view-settings.mjs b/plugins/codebase-health/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/codebase-health/lib/view-settings.mjs +++ b/plugins/codebase-health/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/debugging/lib/view-registry.mjs b/plugins/debugging/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/debugging/lib/view-registry.mjs +++ b/plugins/debugging/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/debugging/lib/view-settings.mjs b/plugins/debugging/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/debugging/lib/view-settings.mjs +++ b/plugins/debugging/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/discovery/lib/view-registry.mjs b/plugins/discovery/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/discovery/lib/view-registry.mjs +++ b/plugins/discovery/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/discovery/lib/view-settings.mjs b/plugins/discovery/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/discovery/lib/view-settings.mjs +++ b/plugins/discovery/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/education/lib/view-registry.mjs b/plugins/education/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/education/lib/view-registry.mjs +++ b/plugins/education/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/education/lib/view-settings.mjs b/plugins/education/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/education/lib/view-settings.mjs +++ b/plugins/education/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/harness-ops/lib/view-registry.mjs b/plugins/harness-ops/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/harness-ops/lib/view-registry.mjs +++ b/plugins/harness-ops/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/harness-ops/lib/view-settings.mjs b/plugins/harness-ops/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/harness-ops/lib/view-settings.mjs +++ b/plugins/harness-ops/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/machine-health/lib/view-registry.mjs b/plugins/machine-health/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/machine-health/lib/view-registry.mjs +++ b/plugins/machine-health/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/overengineering/lib/view-registry.mjs b/plugins/overengineering/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/overengineering/lib/view-registry.mjs +++ b/plugins/overengineering/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/overengineering/lib/view-settings.mjs b/plugins/overengineering/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/overengineering/lib/view-settings.mjs +++ b/plugins/overengineering/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/overengineering/skills/audit/evals/evals.json b/plugins/overengineering/skills/audit/evals/evals.json index 3d75f979445..2259cca2e59 100644 --- a/plugins/overengineering/skills/audit/evals/evals.json +++ b/plugins/overengineering/skills/audit/evals/evals.json @@ -204,7 +204,7 @@ { "id": 15, "name": "retired-file-local-value-drops-rendered-view-offer", - "prompt": "/overengineering:audit ci-lanes (Fixture: on branch feat/ci-trim; interactive session; the visualization plugin is installed; first run in this project; ~/.claude/rendered-views.md contains only 'medium: terminal'; no other rendered-views scope sets medium; no --medium argument was given. The resolver prints a JSON line with medium terminal and page false, the report line 'view: terminal (retired ~/.claude/rendered-views.md)' and the notice 'notice: ~/.claude/rendered-views.md is retired but still keeps the view local (it sets medium: terminal); set it in docs/conventions/rendered-views.local.yaml instead; per user, the overengineering plugin's medium option also works'. The walk finds two CI lanes that duplicate each other's checks.)", + "prompt": "/overengineering:audit ci-lanes (Fixture: on branch feat/ci-trim; interactive session; the visualization plugin is installed; first run in this project; ~/.claude/rendered-views.md contains only 'medium: terminal'; no other rendered-views scope sets medium; no --medium argument was given. The resolver prints a JSON line with medium terminal and page false, the report line 'view: terminal (retired ~/.claude/rendered-views.md)' and the notice 'notice: ~/.claude/rendered-views.md is retired but still keeps the view local (it sets medium: terminal); set it in docs/conventions/rendered-views.local.yaml instead'. The walk finds two CI lanes that duplicate each other's checks.)", "expected_output": "Writes the findings artifact and the inline terminal summary as usual. Runs the shared medium resolver with --lane overengineering:audit before the rendered-view offer, prints its report line and retired-file notice in the terminal summary as they stand, and makes no offer to render the artifact through visualization:visualize, because the JSON's page is false. It does not open or read ~/.claude/rendered-views.md itself.", "files": [], "narration": true, diff --git a/plugins/planning/lib/view-registry.mjs b/plugins/planning/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/planning/lib/view-registry.mjs +++ b/plugins/planning/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/planning/lib/view-settings.mjs b/plugins/planning/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/planning/lib/view-settings.mjs +++ b/plugins/planning/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/prototype/lib/view-registry.mjs b/plugins/prototype/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/prototype/lib/view-registry.mjs +++ b/plugins/prototype/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/prototype/lib/view-settings.mjs b/plugins/prototype/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/prototype/lib/view-settings.mjs +++ b/plugins/prototype/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/prototype/skills/explore-directions/reference/medium.md b/plugins/prototype/skills/explore-directions/reference/medium.md index c734a0bdc01..71ef1df59a9 100644 --- a/plugins/prototype/skills/explore-directions/reference/medium.md +++ b/plugins/prototype/skills/explore-directions/reference/medium.md @@ -9,11 +9,9 @@ node "/lib/view-settings.mjs" resolve --lane prototype:explore-dire ``` It prints one JSON line, then the report line naming the medium and what decided it, then one line -per notice. Print the report line and each notice in the handover as they stand, except that a -notice's closing "; per user, the prototype plugin's medium option also works" clause is dropped, -since this plugin has no such option. Leave every settings file to the resolver. A usage error -(exit 2) means the argument named no medium: say so in one line and run it again without -`--argument`. +per notice. Print the report line and each notice in the handover as they stand. Leave every +settings file to the resolver. A usage error (exit 2) means the argument named no medium: say so in +one line and run it again without `--argument`. - **The JSON's `publishes` is `false`**: the medium keeps the view on this machine. Build the mockup and make no design-canvas offer, since the canvas is published. When `page` is also diff --git a/plugins/prototype/skills/pressure-test/SKILL.md b/plugins/prototype/skills/pressure-test/SKILL.md index 272697b4ecb..391ef972a8e 100644 --- a/plugins/prototype/skills/pressure-test/SKILL.md +++ b/plugins/prototype/skills/pressure-test/SKILL.md @@ -95,12 +95,10 @@ swaps. Route by audience: ``` It prints one JSON line, then the report line naming the medium and what decided it, then one - line per notice. Print the report line and each notice in the handoff as they stand, except that - a notice's closing "; per user, the prototype plugin's medium option also works" clause is - dropped, since this plugin has no such option. Leave every settings file to the resolver. A usage - error (exit 2) means the argument named no medium: say so in one line and run it again without - `--argument`. When the JSON's `page` is `false`, the medium shows no page: build the TUI. Any - other result, a `null` medium, or a resolver that fails (say so in one line) overrides nothing: + line per notice. Print the report line and each notice in the handoff as they stand. Leave every + settings file to the resolver. A usage error (exit 2) means the argument named no medium: say so + in one line and run it again without `--argument`. When the JSON's `page` is `false`, the medium + shows no page: build the TUI. Any other result, a `null` medium, or a resolver that fails (say so in one line) overrides nothing: the medium says where a view goes, not which shell an audience needs, so the audience routing above still decides, and when it picks the HTML demo the page stays a local file, since this skill publishes nothing. Name the source of the shell choice in the handoff: the request, the diff --git a/plugins/review/lib/view-registry.mjs b/plugins/review/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/review/lib/view-registry.mjs +++ b/plugins/review/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/review/lib/view-settings.mjs b/plugins/review/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/review/lib/view-settings.mjs +++ b/plugins/review/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/source-control/lib/view-registry.mjs b/plugins/source-control/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/source-control/lib/view-registry.mjs +++ b/plugins/source-control/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/source-control/lib/view-settings.mjs b/plugins/source-control/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/source-control/lib/view-settings.mjs +++ b/plugins/source-control/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/visualization/lib/view-registry.mjs b/plugins/visualization/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/visualization/lib/view-registry.mjs +++ b/plugins/visualization/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/visualization/lib/view-settings.mjs b/plugins/visualization/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/visualization/lib/view-settings.mjs +++ b/plugins/visualization/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive }); diff --git a/plugins/work-items/lib/view-registry.mjs b/plugins/work-items/lib/view-registry.mjs index 1dd99753c9c..eb5492be8b4 100644 --- a/plugins/work-items/lib/view-registry.mjs +++ b/plugins/work-items/lib/view-registry.mjs @@ -5,12 +5,13 @@ // lib/view-settings.mjs. Adding a medium or a lane is an addition here, never a skill edit. // // MediumEntry { id, publishes, reachable(context) -> {ok, reason}, deliver(view, context) -> {location}, reportName } -// Lane { laneId, kinds, hostedCapable, default } +// Lane { laneId, kinds, hostedCapable, default, hasMediumOption } // // `context.interactive` is false in a CI or other non-interactive run, where only the terminal is // reachable (docs/contracts/rendered-views/README.md, "Reachability and preference"). A lane's // `default` is its shipped medium, applied when no scope decides; null leaves the choice to the -// skill's own content rule. +// skill's own content rule. `hasMediumOption` is true when the lane's plugin ships a `medium` +// userConfig option, so a notice may point the user at it. const interactiveOnly = (context = {}) => (context.interactive === false ? { ok: false, reason: "non-interactive run" } : { ok: true }); @@ -42,28 +43,28 @@ export const SCHEMA = Object.freeze({ properties: { medium: { type: "string", enum: [NO_PREFERENCE, ...Object.keys(MEDIUMS)] } }, }); -const lane = (laneId, kinds, hostedCapable, medium) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium }); +const lane = (laneId, kinds, hostedCapable, medium, hasMediumOption) => Object.freeze({ laneId, kinds: Object.freeze(kinds), hostedCapable, default: medium, hasMediumOption }); export const LANES = Object.freeze( Object.fromEntries( [ - lane("review:explain-change", ["digest"], true, "artifact"), - lane("visualization:present", ["deck"], false, "artifact"), - lane("visualization:visualize", [], false, null), - lane("planning", ["plan", "brainstorm", "design"], false, "artifact"), - lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file"), - lane("discovery:blindspot", ["blindspot"], false, "file"), - lane("debugging:debug", ["post-mortem"], false, "file"), - lane("education:illustrate", [], false, "file"), - lane("work-items:morning-brief", ["morning-brief"], false, "file"), - lane("work-items:triage", ["board"], false, "file"), - lane("adhd:clarify", [], false, null), - lane("codebase-health:audit", [], false, null), - lane("overengineering:audit", [], false, null), - lane("education:teach", [], false, null), - lane("prototype:explore-directions", [], false, null), - lane("prototype:pressure-test", [], false, null), - lane("ai-briefing:generate", [], false, null), + lane("review:explain-change", ["digest"], true, "artifact", true), + lane("visualization:present", ["deck"], false, "artifact", true), + lane("visualization:visualize", [], false, null, true), + lane("planning", ["plan", "brainstorm", "design"], false, "artifact", true), + lane("architecture", ["landscape", "containers", "components", "dependencies", "data", "events", "flow", "context", "deployment"], false, "file", false), + lane("discovery:blindspot", ["blindspot"], false, "file", true), + lane("debugging:debug", ["post-mortem"], false, "file", true), + lane("education:illustrate", [], false, "file", true), + lane("work-items:morning-brief", ["morning-brief"], false, "file", true), + lane("work-items:triage", ["board"], false, "file", true), + lane("adhd:clarify", [], false, null, true), + lane("codebase-health:audit", [], false, null, true), + lane("overengineering:audit", [], false, null, false), + lane("education:teach", [], false, null, true), + lane("prototype:explore-directions", [], false, null, false), + lane("prototype:pressure-test", [], false, null, false), + lane("ai-briefing:generate", [], false, null, false), ].map((l) => [l.laneId, l]), ), ); diff --git a/plugins/work-items/lib/view-settings.mjs b/plugins/work-items/lib/view-settings.mjs index d268b37da9a..f792ca62f40 100755 --- a/plugins/work-items/lib/view-settings.mjs +++ b/plugins/work-items/lib/view-settings.mjs @@ -183,7 +183,7 @@ function retiredLocal(legacy) { } /** One notice per retired file, unless this plugin already showed it for this project with these bytes. */ -function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { +function retiredNotices(legacy, { plugin, hasMediumOption, root, userHome, dataDir }) { const where = (path) => retiredWhere(path, root, userHome); const destination = { "claude-user": PERSONAL_FILE, "claude-project": TEAM_FILE, "claude-local": PERSONAL_FILE, "fenced-block": TEAM_FILE }; const key = dataDir ? stateKey(root) : null; @@ -215,7 +215,7 @@ function retiredNotices(legacy, { plugin, root, userHome, dataDir }) { : "" : ""; const status = rank({ path, kind }) >= 0 && isLocal(value) ? "is retired but still keeps the view local" : "is no longer read"; - const perUser = kind === "claude-user" ? `; per user, the ${plugin} plugin's medium option also works` : ""; + const perUser = kind === "claude-user" && hasMediumOption ? `; per user, the ${plugin} plugin's medium option also works` : ""; notices.push({ kind: "retired", path: shown, value, destination: to, text: `${shown} ${status}${value ? ` (it sets medium: ${value})` : ""}; set it in ${to} instead${caveat}${perUser}` }); } if (state && changed) { @@ -245,7 +245,7 @@ export function resolveMedium({ lane, argument = null, userConfig = null, projec const root = repoScopes(projectRoot, userHome) ? projectRoot : null; const team = root && typeof teamRef === "string" ? teamAt(root, teamRef, notices) : undefined; const { layers, legacy } = readLayers({ plugin: CONCERN, projectRoot: root, schema: SCHEMA, userConfig: { medium: userConfig ?? "" }, team, userHome }); - notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], root, userHome, dataDir })); + notices.unshift(...retiredNotices(legacy, { plugin: lane.split(":")[0], hasMediumOption: laneEntry.hasMediumOption, root, userHome, dataDir })); const finish = (preferred, decidedBy) => { if (!preferred) return { preferred: null, medium: null, decidedBy, notices }; const { medium, fell } = fallBack(preferred, laneEntry, { interactive });