diff --git a/.changeset/eager-rams-click.md b/.changeset/eager-rams-click.md new file mode 100644 index 000000000..832eca188 --- /dev/null +++ b/.changeset/eager-rams-click.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3425 +--- +**GSD skills no longer override the caller's effort level** (#3151) — invoking `/gsd-plan-phase`, `/gsd-execute-phase`, `/gsd-autonomous`, `/gsd-next`, `/gsd-progress`, or `/gsd-stats` previously set `output_config.effort` to a static value baked into the skill frontmatter; when that differed from the session's effort (which it did ~76% of the time), it invalidated the entire prompt cache at both scope boundaries (skill entry and exit). These skills now run at the session's existing effort level (no `effort:` emitted into SKILL.md). The elevated-effort intent is preserved on the source command files; only the skill-frontmatter emission is dropped. The separate agent-effort surface is unaffected. diff --git a/bin/install.js b/bin/install.js index 7a9bfbb29..bd5e98a95 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2091,12 +2091,6 @@ function skillFrontmatterName(skillDirName) { return skillDirName; } -function normalizeClaudeSkillEffort(effort) { - // #3039: `max` is rejected by Anthropic models when extended thinking is disabled. - if (effort === 'xhigh' || effort === 'max') return 'high'; - return effort; -} - /** * Qwen Code skills accept an optional numeric `priority` frontmatter field. * Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified @@ -2153,10 +2147,11 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c const description = extractFrontmatterField(frontmatter, 'description') || ''; const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint'); const agent = extractFrontmatterField(frontmatter, 'agent'); - // #769: preserve context: and effort: from source command files so they - // are emitted into the installed SKILL.md frontmatter unchanged. + // #769: preserve context: from source command files so it is emitted into + // the installed SKILL.md frontmatter unchanged. (#3151: effort: is no longer + // emitted into skill frontmatter — a static effort value invalidates the + // caller's prompt cache at both scope boundaries.) const context = extractFrontmatterField(frontmatter, 'context'); - const effort = extractFrontmatterField(frontmatter, 'effort'); // Preserve allowed-tools as YAML multiline list (Claude native format) const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m); @@ -2190,12 +2185,13 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c } if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`; if (agent) fm += `agent: ${agent}\n`; - // #769: emit context: and effort: when present so the runtime can honour - // them natively (context: fork = isolated subagent window; effort: = - // token-budget tier). Fields are Claude-specific; unknown frontmatter - // fields are silently ignored by other runtimes (backward-compatible). + // #769: emit context: when present so the runtime can honour it natively + // (context: fork = isolated subagent window). Claude-specific; unknown + // frontmatter fields are silently ignored by other runtimes (backward-compatible). + // (#3151: effort: is intentionally NOT emitted into skill frontmatter — a + // static effort value changes output_config.effort on invocation and + // invalidates the caller's prompt cache at both scope boundaries.) if (context) fm += `context: ${context}\n`; - if (effort) fm += `effort: ${normalizeClaudeSkillEffort(effort)}\n`; if (toolsBlock) fm += toolsBlock; fm += '---'; diff --git a/skills/gsd-autonomous/SKILL.md b/skills/gsd-autonomous/SKILL.md index 9d1c2932e..6cc9de4d8 100644 --- a/skills/gsd-autonomous/SKILL.md +++ b/skills/gsd-autonomous/SKILL.md @@ -2,7 +2,6 @@ name: gsd-autonomous description: "Run all remaining phases autonomously — discuss→plan→execute per phase" argument-hint: "[--from N] [--to N] [--only N] [--interactive] [--converge]" -effort: high allowed-tools: - Read - Write diff --git a/skills/gsd-execute-phase/SKILL.md b/skills/gsd-execute-phase/SKILL.md index 34f9b2393..27ed6b394 100644 --- a/skills/gsd-execute-phase/SKILL.md +++ b/skills/gsd-execute-phase/SKILL.md @@ -2,7 +2,6 @@ name: gsd-execute-phase description: "Execute all plans in a phase with wave-based parallelization" argument-hint: " [--wave N] [--gaps-only] [--interactive] [--tdd]" -effort: high allowed-tools: - Read - Write diff --git a/skills/gsd-next/SKILL.md b/skills/gsd-next/SKILL.md index ebef741bd..fcd0b05b2 100644 --- a/skills/gsd-next/SKILL.md +++ b/skills/gsd-next/SKILL.md @@ -1,7 +1,6 @@ --- name: gsd-next description: "Smart entry — detect project state and route to the right next GSD action." -effort: low allowed-tools: - Read - Bash diff --git a/skills/gsd-plan-phase/SKILL.md b/skills/gsd-plan-phase/SKILL.md index 4f7d151a8..2ed44f1b0 100644 --- a/skills/gsd-plan-phase/SKILL.md +++ b/skills/gsd-plan-phase/SKILL.md @@ -2,7 +2,6 @@ name: gsd-plan-phase description: "Create detailed phase plan (PLAN.md) with verification loop" argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase ] [--view] [--gaps] [--skip-verify] [--skip-ui] [--prd ] [--ingest ] [--ingest-format ] [--reviews] [--text] [--bounce] [--skip-bounce] [--chunked] [--tdd] [--mvp] [--granularity ] [--no-tracer] [--no-reversibility-gates]" -effort: high allowed-tools: - Read - Write diff --git a/skills/gsd-progress/SKILL.md b/skills/gsd-progress/SKILL.md index a9199229e..d24715a90 100644 --- a/skills/gsd-progress/SKILL.md +++ b/skills/gsd-progress/SKILL.md @@ -2,7 +2,6 @@ name: gsd-progress description: "Check progress, advance workflow, or dispatch freeform intent — the unified GSD situational command" argument-hint: "[--forensic | --next [--auto] [--converge] | --do \\\"task description\\\"]" -effort: low allowed-tools: - Read - Bash diff --git a/skills/gsd-stats/SKILL.md b/skills/gsd-stats/SKILL.md index f481087f1..57d0db82f 100644 --- a/skills/gsd-stats/SKILL.md +++ b/skills/gsd-stats/SKILL.md @@ -1,7 +1,6 @@ --- name: gsd-stats description: "Display project statistics — phases, plans, requirements, git metrics, and timeline" -effort: low allowed-tools: - Read - Bash diff --git a/src/runtime-artifact-conversion.cts b/src/runtime-artifact-conversion.cts index 8ee05dc62..03e7bcf36 100644 --- a/src/runtime-artifact-conversion.cts +++ b/src/runtime-artifact-conversion.cts @@ -392,16 +392,6 @@ function skillFrontmatterName(skillDirName) { return skillDirName; } -function normalizeClaudeSkillEffort(effort) { - // #3039: `max` is rejected by Anthropic models when extended thinking is - // disabled (400: output_config.effort 'max' is not supported when thinking - // is disabled). The frontmatter is static at install time and the installer - // cannot know whether thinking will be on or off at invocation. `high` is the - // maximum value that works in both states on all supported models. - if (effort === 'xhigh' || effort === 'max') return 'high'; - return effort; -} - /** * Qwen Code skills accept an optional numeric `priority` frontmatter field. * Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified @@ -458,10 +448,13 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c const description = extractFrontmatterField(frontmatter, 'description') || ''; const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint'); const agent = extractFrontmatterField(frontmatter, 'agent'); - // #769: preserve context: and effort: from source command files so they - // are emitted into the installed SKILL.md frontmatter unchanged. + // #769: preserve context: from source command files so it is emitted into + // the installed SKILL.md frontmatter unchanged. (#3151: effort: is no longer + // emitted into skill frontmatter — a static effort value changes + // output_config.effort on invocation and invalidates the caller's prompt + // cache at both scope boundaries; the reporter's owned measurement confirms + // the mechanism. The separate agent-effort surface is tracked by #3160.) const context = extractFrontmatterField(frontmatter, 'context'); - const effort = extractFrontmatterField(frontmatter, 'effort'); // Preserve allowed-tools as YAML multiline list (Claude native format) const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m); @@ -498,12 +491,13 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c } if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`; if (agent) fm += `agent: ${agent}\n`; - // #769: emit context: and effort: when present so the runtime can honour - // them natively (context: fork = isolated subagent window; effort: = - // token-budget tier). Fields are Claude-specific; unknown frontmatter - // fields are silently ignored by other runtimes (backward-compatible). + // #769: emit context: when present so the runtime can honour it natively + // (context: fork = isolated subagent window). Claude-specific; unknown + // frontmatter fields are silently ignored by other runtimes (backward-compatible). + // (#3151: effort: is intentionally NOT emitted into skill frontmatter — a + // static effort value changes output_config.effort on invocation and + // invalidates the caller's prompt cache at both scope boundaries.) if (context) fm += `context: ${context}\n`; - if (effort) fm += `effort: ${normalizeClaudeSkillEffort(effort)}\n`; if (toolsBlock) fm += toolsBlock; fm += '---'; diff --git a/tests/emitted-drift-acks/2812-cursor-single-menu-surface.json b/tests/emitted-drift-acks/2812-cursor-single-menu-surface.json deleted file mode 100644 index 72964b303..000000000 --- a/tests/emitted-drift-acks/2812-cursor-single-menu-surface.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "version": 1, - "paths": { - "commands/gsd-add-tests.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ai-integration-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-audit-fix.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-audit-milestone.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-audit-uat.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-autonomous.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-capture.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-cleanup.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-code-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-complete-milestone.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-config.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-debug.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-discuss-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-docs-update.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-eval-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-execute-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-explore.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-extract-learnings.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-fast.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-forensics.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-graphify.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-health.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-help.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-import.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-inbox.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ingest-docs.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-manager.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-map-codebase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-mempalace-capture.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-mempalace-recall.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-milestone-summary.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-mvp-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-new-milestone.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-new-project.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-next.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ns-context.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ns-ideate.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ns-manage.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ns-project.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ns-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ns-workflow.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-onboard.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-pause-work.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-plan-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-plan-review-convergence.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-pr-branch.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-profile-user.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-progress.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-quick.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-resume-work.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-review-backlog.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-secure-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-settings.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ship.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-sketch.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-spec-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-spike.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-stats.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-surface.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-thread.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ui-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ui-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-ultraplan-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-undo.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-update.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-validate-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-verify-work.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-workspace.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "commands/gsd-workstreams.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).", - "skills/gsd-add-tests/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ai-integration-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-audit-fix/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-audit-milestone/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-audit-uat/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-autonomous/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644). #3039: normalizeClaudeSkillEffort clamps max to high \u2014 this Claude-runtime SKILL.md now carries effort: high instead of effort: max.", - "skills/gsd-capture/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-cleanup/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-code-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-complete-milestone/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-config/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-debug/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-discuss-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-docs-update/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-eval-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-execute-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644). #3039: normalizeClaudeSkillEffort clamps max to high \u2014 this Claude-runtime SKILL.md now carries effort: high instead of effort: max.", - "skills/gsd-explore/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-extract-learnings/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-fast/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-forensics/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-graphify/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-health/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-help/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-import/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-inbox/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ingest-docs/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-manager/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-map-codebase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-mempalace-capture/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-mempalace-recall/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-milestone-summary/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-mvp-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-new-milestone/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-new-project/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-next/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ns-context/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ns-ideate/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ns-manage/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ns-project/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ns-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ns-workflow/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-onboard/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-pause-work/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-plan-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644). #3039: normalizeClaudeSkillEffort clamps max to high \u2014 this Claude-runtime SKILL.md now carries effort: high instead of effort: max.", - "skills/gsd-plan-review-convergence/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-pr-branch/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-profile-user/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-progress/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-quick/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-resume-work/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-review-backlog/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-secure-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-settings/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ship/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-sketch/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-spec-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-spike/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-stats/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-surface/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-thread/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ui-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ui-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-ultraplan-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-undo/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-update/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-validate-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-verify-work/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-workspace/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).", - "skills/gsd-workstreams/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644)." - } -} diff --git a/tests/emitted-drift-acks/3039-effort-clamp-max-to-high.json b/tests/emitted-drift-acks/3039-effort-clamp-max-to-high.json deleted file mode 100644 index 03d199ae8..000000000 --- a/tests/emitted-drift-acks/3039-effort-clamp-max-to-high.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": 1, - "paths": { - "skills/gsd-ns-workflow/skills/autonomous/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled).", - "skills/gsd-ns-workflow/skills/execute-phase/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled).", - "skills/gsd-ns-workflow/skills/plan-phase/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled).", - "skills/gsd/gsd-ns-workflow/skills/autonomous/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled).", - "skills/gsd/gsd-ns-workflow/skills/execute-phase/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled).", - "skills/gsd/gsd-ns-workflow/skills/plan-phase/SKILL.md": "#3039: normalizeClaudeSkillEffort now clamps max/xhigh to high \u2014 every emitted SKILL.md that carried effort: max now carries effort: high (max rejected by Anthropic when thinking disabled)." - } -} diff --git a/tests/emitted-drift-acks/3151-skill-effort-cache-invalidation.json b/tests/emitted-drift-acks/3151-skill-effort-cache-invalidation.json new file mode 100644 index 000000000..d4ac14c4c --- /dev/null +++ b/tests/emitted-drift-acks/3151-skill-effort-cache-invalidation.json @@ -0,0 +1,59 @@ +{ + "version": 1, + "paths": { + "skills/gsd-autonomous/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd-execute-phase/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd-next/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd-ns-manage/skills/stats/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd-ns-workflow/skills/autonomous/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd-ns-workflow/skills/execute-phase/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd-ns-workflow/skills/next/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd-ns-workflow/skills/plan-phase/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd-ns-workflow/skills/progress/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd-plan-phase/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd-progress/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd-stats/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd/gsd-ns-manage/skills/stats/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd/gsd-ns-workflow/skills/autonomous/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd/gsd-ns-workflow/skills/execute-phase/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd/gsd-ns-workflow/skills/next/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd/gsd-ns-workflow/skills/plan-phase/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + }, + "skills/gsd/gsd-ns-workflow/skills/progress/SKILL.md": { + "reason": "#3151: convertClaudeCommandToClaudeSkill no longer emits effort: into skill frontmatter (a static effort value invalidates the caller's prompt cache at both scope boundaries). The converter (src/runtime-artifact-conversion.cts + duplicate bin/install.js) changed, moving these emitted skill hashes without a source-command change; this ack covers that converter-driven emission drop across all runtime layouts (per ADR-2719/#3039 precedent)." + } + } +} \ No newline at end of file diff --git a/tests/install-runtime-artifacts.test.cjs b/tests/install-runtime-artifacts.test.cjs index 21e81efb2..180b463ae 100644 --- a/tests/install-runtime-artifacts.test.cjs +++ b/tests/install-runtime-artifacts.test.cjs @@ -3388,50 +3388,22 @@ describe('#769/#1319 convertClaudeCommandToClaudeSkill: preserves context and em `SKILL.md frontmatter must include context: fork\nActual frontmatter:\n${fm}`); }); - test('normalizes effort: xhigh/max to effort: high in emitted SKILL.md frontmatter (#1319/#3039)', () => { - const input = [ - '---', - 'name: gsd:test-heavy', - 'description: Test heavy skill', - 'context: fork', - 'effort: xhigh', - 'allowed-tools:', - ' - Read', - ' - Bash', - '---', - '', - 'Heavy skill body.', - ].join('\n'); - - const result = convertClaudeCommandToClaudeSkill(input, 'test-heavy'); - const end = result.indexOf('---', 3); - const fm = result.substring(3, end); - - assert.match(fm, /^effort:[ \t]*high$/m, - `SKILL.md frontmatter must include portable effort: high (#3039)\nActual frontmatter:\n${fm}`); - assert.doesNotMatch(fm, /^effort:[ \t]*(xhigh|max)$/m, - `SKILL.md frontmatter must not include rejected effort: xhigh or max (#1319/#3039)\nActual frontmatter:\n${fm}`); - }); - - test('preserves effort: low in emitted SKILL.md frontmatter', () => { - const input = [ - '---', - 'name: gsd:test-light', - 'description: Test light skill', - 'effort: low', - 'allowed-tools:', - ' - Read', - '---', - '', - 'Light skill body.', - ].join('\n'); - - const result = convertClaudeCommandToClaudeSkill(input, 'test-light'); - const end = result.indexOf('---', 3); - const fm = result.substring(3, end); - - assert.match(fm, /^effort:[ \t]*low$/m, - `SKILL.md frontmatter must include effort: low\nActual frontmatter:\n${fm}`); + test('#3151: does NOT emit effort: into SKILL.md frontmatter (a static effort value invalidates the caller\'s prompt cache)', () => { + // Whatever effort the source command declares, the installed SKILL.md must + // NOT carry it: Claude Code applies SKILL.md `effort:` as output_config.effort, + // and any change from the session baseline invalidates the prompt cache at both + // scope boundaries (entry + exit). Verified by the reporter's owned measurement. + const inputs = [ + ['xhigh source', '---\nname: gsd:test-heavy\ndescription: Heavy\ncontext: fork\neffort: xhigh\nallowed-tools:\n - Read\n---\n\nBody.\n'], + ['low source', '---\nname: gsd:test-light\ndescription: Light\neffort: low\nallowed-tools:\n - Read\n---\n\nBody.\n'], + ]; + for (const [label, input] of inputs) { + const result = convertClaudeCommandToClaudeSkill(input, label === 'xhigh source' ? 'test-heavy' : 'test-light'); + const end = result.indexOf('---', 3); + const fm = result.substring(3, end); + assert.doesNotMatch(fm, /^effort:/m, + `SKILL.md frontmatter must NOT include effort: for ${label} (#3151)\nActual frontmatter:\n${fm}`); + } }); test('does NOT emit context: or effort: when absent from source', () => { @@ -3461,7 +3433,7 @@ describe('#769/#1319 convertClaudeCommandToClaudeSkill: preserves context and em // #921/#922: after install, spawning orchestrators must NOT carry context: fork // in their emitted SKILL.md. #1319: heavyweight skills must use portable max effort. -describe('#769/#921/#1319 Claude global install: spawning-orchestrator SKILL.md files have effort: high (clamped from max) but NOT context: fork', () => { +describe('#769/#921/#3151 Claude global install: SKILL.md files emit NO effort (#3151 — static effort invalidates caller cache) and NOT context: fork', () => { let tmpDir; let claudeHome; @@ -3483,14 +3455,12 @@ describe('#769/#921/#1319 Claude global install: spawning-orchestrator SKILL.md `gsd-autonomous is a spawning orchestrator; its SKILL.md must NOT have context: fork (#921)\nActual:\n${fm}`); }); - test('gsd-autonomous SKILL.md has effort: max after global install (#1319)', () => { + test('gsd-autonomous SKILL.md does NOT emit effort: after global install (#3151)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'autonomous'); const fm = readFrontmatter(skillPath); - assert.match(fm, /^effort:[ \t]*high$/m, - `gsd-autonomous SKILL.md must have effort: high\nActual:\n${fm}`); - assert.doesNotMatch(fm, /^effort:[ \t]*xhigh$/m, - `gsd-autonomous SKILL.md must not have rejected effort: xhigh or max (#1319/#3039)\nActual:\n${fm}`); + assert.doesNotMatch(fm, /^effort:/m, + `gsd-autonomous SKILL.md must NOT emit effort: (#3151 — a static value invalidates the caller's prompt cache)\nActual:\n${fm}`); }); test('gsd-execute-phase SKILL.md does NOT have context: fork after global install (#921)', () => { @@ -3501,14 +3471,12 @@ describe('#769/#921/#1319 Claude global install: spawning-orchestrator SKILL.md `gsd-execute-phase is a spawning orchestrator; its SKILL.md must NOT have context: fork (#921)\nActual:\n${fm}`); }); - test('gsd-execute-phase SKILL.md has effort: max after global install (#1319)', () => { + test('gsd-execute-phase SKILL.md does NOT emit effort: after global install (#3151)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'execute-phase'); const fm = readFrontmatter(skillPath); - assert.match(fm, /^effort:[ \t]*high$/m, - `gsd-execute-phase SKILL.md must have effort: high\nActual:\n${fm}`); - assert.doesNotMatch(fm, /^effort:[ \t]*xhigh$/m, - `gsd-execute-phase SKILL.md must not have rejected effort: xhigh or max (#1319/#3039)\nActual:\n${fm}`); + assert.doesNotMatch(fm, /^effort:/m, + `gsd-execute-phase SKILL.md must NOT emit effort: (#3151)\nActual:\n${fm}`); }); test('gsd-plan-phase SKILL.md does NOT have context: fork after global install (#921)', () => { @@ -3519,30 +3487,28 @@ describe('#769/#921/#1319 Claude global install: spawning-orchestrator SKILL.md `gsd-plan-phase is a spawning orchestrator; its SKILL.md must NOT have context: fork (#921)\nActual:\n${fm}`); }); - test('gsd-plan-phase SKILL.md has effort: max after global install (#1319)', () => { + test('gsd-plan-phase SKILL.md does NOT emit effort: after global install (#3151)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'plan-phase'); const fm = readFrontmatter(skillPath); - assert.match(fm, /^effort:[ \t]*high$/m, - `gsd-plan-phase SKILL.md must have effort: high\nActual:\n${fm}`); - assert.doesNotMatch(fm, /^effort:[ \t]*xhigh$/m, - `gsd-plan-phase SKILL.md must not have rejected effort: xhigh or max (#1319/#3039)\nActual:\n${fm}`); + assert.doesNotMatch(fm, /^effort:/m, + `gsd-plan-phase SKILL.md must NOT emit effort: (#3151)\nActual:\n${fm}`); }); - test('gsd-progress SKILL.md has effort: low after global install', () => { + test('gsd-progress SKILL.md does NOT emit effort: after global install (#3151)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'progress'); const fm = readFrontmatter(skillPath); - assert.match(fm, /^effort:[ \t]*low$/m, - `gsd-progress SKILL.md must have effort: low\nActual:\n${fm}`); + assert.doesNotMatch(fm, /^effort:/m, + `gsd-progress SKILL.md must NOT emit effort: (#3151)\nActual:\n${fm}`); }); - test('gsd-stats SKILL.md has effort: low after global install', () => { + test('gsd-stats SKILL.md does NOT emit effort: after global install (#3151)', () => { runClaudeGlobalInstall(claudeHome); const skillPath = flatSkillPath(path.join(claudeHome, 'skills'),'stats'); const fm = readFrontmatter(skillPath); - assert.match(fm, /^effort:[ \t]*low$/m, - `gsd-stats SKILL.md must have effort: low\nActual:\n${fm}`); + assert.doesNotMatch(fm, /^effort:/m, + `gsd-stats SKILL.md must NOT emit effort: (#3151)\nActual:\n${fm}`); }); }); });