@event4u/agent-config 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent-src/commands/agent-handoff.md +4 -1
- package/.agent-src/commands/agent-status.md +3 -0
- package/.agent-src/commands/agents-audit.md +4 -0
- package/.agent-src/commands/agents-cleanup.md +6 -1
- package/.agent-src/commands/agents-prepare.md +3 -0
- package/.agent-src/commands/analyze-reference-repo.md +4 -0
- package/.agent-src/commands/bug-fix.md +7 -3
- package/.agent-src/commands/bug-investigate.md +4 -0
- package/.agent-src/commands/chat-history-checkpoint.md +126 -0
- package/.agent-src/commands/chat-history-clear.md +6 -1
- package/.agent-src/commands/chat-history-resume.md +7 -2
- package/.agent-src/commands/chat-history.md +7 -2
- package/.agent-src/commands/check-current-md.md +137 -0
- package/.agent-src/commands/commit-in-chunks.md +118 -0
- package/.agent-src/commands/commit.md +4 -0
- package/.agent-src/commands/compress.md +37 -2
- package/.agent-src/commands/context-create.md +4 -0
- package/.agent-src/commands/context-refactor.md +4 -0
- package/.agent-src/commands/copilot-agents-init.md +3 -0
- package/.agent-src/commands/copilot-agents-optimize.md +3 -0
- package/.agent-src/commands/create-pr-description.md +4 -0
- package/.agent-src/commands/create-pr.md +4 -0
- package/.agent-src/commands/do-and-judge.md +4 -1
- package/.agent-src/commands/do-in-steps.md +3 -0
- package/.agent-src/commands/e2e-heal.md +4 -0
- package/.agent-src/commands/e2e-plan.md +4 -0
- package/.agent-src/commands/estimate-ticket.md +4 -1
- package/.agent-src/commands/feature-dev.md +4 -0
- package/.agent-src/commands/feature-explore.md +4 -0
- package/.agent-src/commands/feature-plan.md +4 -0
- package/.agent-src/commands/feature-refactor.md +4 -0
- package/.agent-src/commands/feature-roadmap.md +6 -0
- package/.agent-src/commands/fix-ci.md +4 -0
- package/.agent-src/commands/fix-portability.md +5 -2
- package/.agent-src/commands/fix-pr-bot-comments.md +4 -0
- package/.agent-src/commands/fix-pr-comments.md +4 -0
- package/.agent-src/commands/fix-pr-developer-comments.md +4 -0
- package/.agent-src/commands/fix-references.md +3 -0
- package/.agent-src/commands/fix-seeder.md +4 -0
- package/.agent-src/commands/implement-ticket.md +39 -13
- package/.agent-src/commands/jira-ticket.md +4 -0
- package/.agent-src/commands/judge.md +3 -0
- package/.agent-src/commands/memory-add.md +5 -3
- package/.agent-src/commands/memory-full.md +5 -2
- package/.agent-src/commands/memory-promote.md +7 -6
- package/.agent-src/commands/mode.md +3 -0
- package/.agent-src/commands/module-create.md +4 -0
- package/.agent-src/commands/module-explore.md +4 -0
- package/.agent-src/commands/onboard.md +33 -0
- package/.agent-src/commands/optimize-agents.md +4 -0
- package/.agent-src/commands/optimize-augmentignore.md +12 -0
- package/.agent-src/commands/optimize-rtk-filters.md +3 -0
- package/.agent-src/commands/optimize-skills.md +4 -0
- package/.agent-src/commands/override-create.md +4 -0
- package/.agent-src/commands/override-manage.md +4 -0
- package/.agent-src/commands/package-reset.md +3 -0
- package/.agent-src/commands/package-test.md +3 -0
- package/.agent-src/commands/prepare-for-review.md +4 -0
- package/.agent-src/commands/project-analyze.md +4 -0
- package/.agent-src/commands/project-health.md +4 -0
- package/.agent-src/commands/propose-memory.md +6 -8
- package/.agent-src/commands/quality-fix.md +4 -0
- package/.agent-src/commands/refine-ticket.md +12 -7
- package/.agent-src/commands/review-changes.md +39 -8
- package/.agent-src/commands/review-routing.md +4 -0
- package/.agent-src/commands/roadmap-create.md +18 -0
- package/.agent-src/commands/roadmap-execute.md +14 -1
- package/.agent-src/commands/rule-compliance-audit.md +4 -0
- package/.agent-src/commands/set-cost-profile.md +11 -0
- package/.agent-src/commands/sync-agent-settings.md +12 -0
- package/.agent-src/commands/sync-gitignore.md +3 -0
- package/.agent-src/commands/tests-create.md +4 -0
- package/.agent-src/commands/tests-execute.md +6 -3
- package/.agent-src/commands/threat-model.md +4 -0
- package/.agent-src/commands/update-form-request-messages.md +4 -0
- package/.agent-src/commands/upstream-contribute.md +4 -0
- package/.agent-src/commands/work.md +161 -0
- package/.agent-src/guidelines/agent-infra/engineering-memory-data-format.md +2 -6
- package/.agent-src/guidelines/agent-infra/layered-settings.md +0 -1
- package/.agent-src/guidelines/agent-infra/memory-access.md +0 -7
- package/.agent-src/guidelines/agent-infra/role-contracts.md +2 -4
- package/.agent-src/guidelines/agent-infra/self-improvement-pipeline.md +0 -1
- package/.agent-src/guidelines/php/patterns/strategy.md +180 -2
- package/.agent-src/personas/README.md +0 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +7 -2
- package/.agent-src/rules/artifact-engagement-recording.md +133 -0
- package/.agent-src/rules/ask-when-uncertain.md +18 -13
- package/.agent-src/rules/augment-portability.md +64 -37
- package/.agent-src/rules/autonomous-execution.md +158 -0
- package/.agent-src/rules/chat-history-cadence.md +109 -0
- package/.agent-src/rules/chat-history-ownership.md +123 -0
- package/.agent-src/rules/chat-history-visibility.md +96 -0
- package/.agent-src/rules/cli-output-handling.md +27 -4
- package/.agent-src/rules/command-suggestion.md +134 -0
- package/.agent-src/rules/commit-policy.md +109 -0
- package/.agent-src/rules/direct-answers.md +114 -0
- package/.agent-src/rules/docs-sync.md +36 -0
- package/.agent-src/rules/downstream-changes.md +10 -9
- package/.agent-src/rules/improve-before-implement.md +9 -6
- package/.agent-src/rules/language-and-tone.md +85 -6
- package/.agent-src/rules/non-destructive-by-default.md +117 -0
- package/.agent-src/rules/package-ci-checks.md +4 -0
- package/.agent-src/rules/preservation-guard.md +20 -0
- package/.agent-src/rules/roadmap-progress-sync.md +159 -27
- package/.agent-src/rules/role-mode-adherence.md +1 -1
- package/.agent-src/rules/scope-control.md +42 -1
- package/.agent-src/rules/size-enforcement.md +2 -3
- package/.agent-src/rules/skill-quality.md +3 -8
- package/.agent-src/rules/ui-audit-before-build.md +106 -0
- package/.agent-src/rules/user-interaction.md +107 -51
- package/.agent-src/scripts/update_roadmap_progress.py +73 -9
- package/.agent-src/skills/blade-ui/SKILL.md +47 -3
- package/.agent-src/skills/command-routing/SKILL.md +32 -0
- package/.agent-src/skills/command-writing/SKILL.md +52 -2
- package/.agent-src/skills/description-assist/SKILL.md +21 -0
- package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
- package/.agent-src/skills/existing-ui-audit/SKILL.md +202 -0
- package/.agent-src/skills/fe-design/SKILL.md +78 -61
- package/.agent-src/skills/file-editor/SKILL.md +9 -0
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
- package/.agent-src/skills/flux/SKILL.md +31 -4
- package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
- package/.agent-src/skills/livewire/SKILL.md +49 -4
- package/.agent-src/skills/md-language-check/SKILL.md +103 -0
- package/.agent-src/skills/php-coder/SKILL.md +24 -0
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
- package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
- package/.agent-src/skills/refine-ticket/SKILL.md +32 -28
- package/.agent-src/skills/roadmap-management/SKILL.md +24 -11
- package/.agent-src/skills/rule-writing/SKILL.md +23 -1
- package/.agent-src/skills/skill-writing/SKILL.md +3 -5
- package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
- package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
- package/.agent-src/templates/AGENTS.md +24 -6
- package/.agent-src/templates/agent-settings.md +149 -0
- package/.agent-src/templates/roadmaps.md +11 -4
- package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
- package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
- package/.agent-src/templates/scripts/memory_lookup.py +1 -1
- package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
- package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
- package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
- package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
- package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
- package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
- package/.agent-src/templates/scripts/telemetry_record.py +166 -0
- package/.agent-src/templates/scripts/telemetry_report.py +161 -0
- package/.agent-src/templates/scripts/telemetry_status.py +142 -0
- package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
- package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
- package/.agent-src/templates/scripts/work_engine/cli.py +195 -0
- package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +10 -3
- package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
- package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +3 -3
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +2 -2
- package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +37 -5
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
- package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
- package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
- package/.agent-src/templates/scripts/work_engine/emitters.py +43 -0
- package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -0
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
- package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
- package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
- package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
- package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
- package/.agent-src/templates/scripts/work_engine/input_builders.py +163 -0
- package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
- package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
- package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
- package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +231 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +1 -1
- package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
- package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
- package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
- package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
- package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
- package/.agent-src/templates/scripts/work_engine/state.py +641 -0
- package/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
- package/.claude-plugin/marketplace.json +105 -2
- package/AGENTS.md +38 -8
- package/CHANGELOG.md +609 -0
- package/README.md +136 -14
- package/config/agent-settings.template.yml +45 -0
- package/config/gitignore-block.txt +4 -0
- package/docs/MIGRATION.md +122 -0
- package/docs/architecture.md +111 -35
- package/docs/contracts/STABILITY.md +95 -0
- package/docs/contracts/adr-chat-history-split.md +132 -0
- package/docs/contracts/adr-command-suggestion.md +146 -0
- package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
- package/docs/contracts/adr-product-ui-track.md +384 -0
- package/docs/contracts/adr-prompt-driven-execution.md +187 -0
- package/docs/contracts/agent-memory-contract.md +149 -0
- package/docs/contracts/artifact-engagement-flow.md +262 -0
- package/docs/contracts/command-clusters.md +126 -0
- package/docs/contracts/command-suggestion-flow.md +148 -0
- package/docs/contracts/implement-ticket-flow.md +628 -0
- package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
- package/docs/contracts/linear-ai-three-layers.md +131 -0
- package/docs/contracts/rule-interactions.md +107 -0
- package/docs/contracts/rule-interactions.yml +142 -0
- package/docs/contracts/ui-stack-extension.md +236 -0
- package/docs/contracts/ui-track-flow.md +338 -0
- package/docs/development.md +1 -1
- package/docs/getting-started.md +3 -3
- package/docs/installation.md +124 -2
- package/docs/migrations/commands-1.15.0.md +112 -0
- package/docs/showcase.md +204 -0
- package/docs/ui-track-mental-model.md +121 -0
- package/package.json +1 -1
- package/scripts/agent-config +199 -0
- package/scripts/audit_cloud_compatibility.py +288 -0
- package/scripts/build_cloud_bundle.py +458 -0
- package/scripts/build_linear_digest.py +263 -0
- package/scripts/chat_history.py +796 -7
- package/scripts/check_compression.py +139 -0
- package/scripts/check_iron_law_prominence.py +143 -0
- package/scripts/check_md_language.py +159 -0
- package/scripts/check_portability.py +38 -0
- package/scripts/check_public_links.py +185 -0
- package/scripts/check_references.py +1 -0
- package/scripts/check_reply_consistency.py +140 -0
- package/scripts/command_suggester/__init__.py +51 -0
- package/scripts/command_suggester/cooldown.py +132 -0
- package/scripts/command_suggester/loader.py +70 -0
- package/scripts/command_suggester/match.py +180 -0
- package/scripts/command_suggester/rank.py +120 -0
- package/scripts/command_suggester/render.py +86 -0
- package/scripts/command_suggester/sanitize.py +113 -0
- package/scripts/command_suggester/settings.py +125 -0
- package/scripts/command_suggester/types.py +78 -0
- package/scripts/hooks/augment-chat-history.sh +56 -0
- package/scripts/install-hooks.sh +67 -0
- package/scripts/install.py +150 -33
- package/scripts/lint_marketplace.py +27 -0
- package/scripts/lint_no_new_atomic_commands.py +179 -0
- package/scripts/lint_rule_interactions.py +149 -0
- package/scripts/memory_lookup.py +1 -1
- package/scripts/migrate_command_suggestions.py +151 -0
- package/scripts/release.py +297 -64
- package/scripts/schemas/command.schema.json +41 -0
- package/scripts/skill_linter.py +81 -0
- package/scripts/sync_agent_settings.py +42 -12
- package/scripts/update_counts.py +10 -0
- package/templates/consumer-settings/augment-cli-hooks.json +54 -0
- package/templates/consumer-settings/claude-settings.json +55 -1
- package/.agent-src/rules/chat-history.md +0 -171
- package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
- package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
- package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
- package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
package/scripts/release.py
CHANGED
|
@@ -20,10 +20,13 @@ Pipeline:
|
|
|
20
20
|
push the tag (this triggers publish-npm.yml).
|
|
21
21
|
9. GitHub Release — `gh release create X.Y.Z --notes <changelog>`.
|
|
22
22
|
|
|
23
|
-
Idempotency
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
Idempotency: pass `--resume` to recover from a partial failure. Each
|
|
24
|
+
step then probes existing state (branch, commit, PR, tag, GitHub
|
|
25
|
+
Release) and skips work that is already done, instead of erroring out.
|
|
26
|
+
Without `--resume` the pipeline still mutates git/network state, so
|
|
27
|
+
re-running on a dirty tree needs `--resume` (or a manual cleanup).
|
|
28
|
+
Each step prints what it's about to do before doing it, so a crash
|
|
29
|
+
leaves a recoverable trail.
|
|
27
30
|
|
|
28
31
|
Stdlib-only (Python 3.10+). No third-party runtime dependencies.
|
|
29
32
|
"""
|
|
@@ -183,6 +186,61 @@ def have(bin: str) -> bool:
|
|
|
183
186
|
)
|
|
184
187
|
|
|
185
188
|
|
|
189
|
+
# ─── resume-mode state probes ────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _branch_exists_local(branch: str) -> bool:
|
|
193
|
+
r = run(
|
|
194
|
+
"git", "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}",
|
|
195
|
+
check=False, capture=True,
|
|
196
|
+
)
|
|
197
|
+
return r.returncode == 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _branch_exists_remote(branch: str) -> bool:
|
|
201
|
+
r = run(
|
|
202
|
+
"git", "ls-remote", "--exit-code", "--heads", REMOTE, branch,
|
|
203
|
+
check=False, capture=True,
|
|
204
|
+
)
|
|
205
|
+
return r.returncode == 0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _tag_exists_local(tag: str) -> bool:
|
|
209
|
+
return tag in git("tag", "-l", tag, capture=True).splitlines()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _tag_exists_remote(tag: str) -> bool:
|
|
213
|
+
r = run(
|
|
214
|
+
"git", "ls-remote", "--exit-code", "--tags", REMOTE, tag,
|
|
215
|
+
check=False, capture=True,
|
|
216
|
+
)
|
|
217
|
+
return r.returncode == 0
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _pr_for_branch(branch: str) -> dict | None:
|
|
221
|
+
"""Most recent PR (any state) with `release/X.Y.Z` as head, or None."""
|
|
222
|
+
r = run(
|
|
223
|
+
"gh", "pr", "list",
|
|
224
|
+
"--head", branch,
|
|
225
|
+
"--state", "all",
|
|
226
|
+
"--json", "number,state,url",
|
|
227
|
+
"--limit", "1",
|
|
228
|
+
check=False, capture=True,
|
|
229
|
+
)
|
|
230
|
+
if r.returncode != 0:
|
|
231
|
+
return None
|
|
232
|
+
try:
|
|
233
|
+
items = json.loads(r.stdout or "[]")
|
|
234
|
+
except json.JSONDecodeError:
|
|
235
|
+
return None
|
|
236
|
+
return items[0] if items else None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _release_exists(tag: str) -> bool:
|
|
240
|
+
r = run("gh", "release", "view", tag, check=False, capture=True)
|
|
241
|
+
return r.returncode == 0
|
|
242
|
+
|
|
243
|
+
|
|
186
244
|
# ─── version math ─────────────────────────────────────────────────────────────
|
|
187
245
|
|
|
188
246
|
|
|
@@ -353,8 +411,21 @@ def set_marketplace_version(path: Path, version: str) -> None:
|
|
|
353
411
|
# ─── preflight ────────────────────────────────────────────────────────────────
|
|
354
412
|
|
|
355
413
|
|
|
356
|
-
def preflight(target: str) -> None:
|
|
357
|
-
"""Fail fast on conditions that would break the release mid-flight.
|
|
414
|
+
def preflight(target: str, *, resume: bool = False) -> None:
|
|
415
|
+
"""Fail fast on conditions that would break the release mid-flight.
|
|
416
|
+
|
|
417
|
+
In ``--resume`` mode two invariants are relaxed:
|
|
418
|
+
|
|
419
|
+
* The starting branch may be ``release/{target}`` in addition to
|
|
420
|
+
``main`` — both are valid resume positions (mid-pipeline crash
|
|
421
|
+
after step 1 leaves you on the release branch).
|
|
422
|
+
* The target-tag-exists check is dropped — execute() probes for
|
|
423
|
+
existing tags/releases and skips them.
|
|
424
|
+
|
|
425
|
+
Tree cleanliness, gh auth, and ``main`` in-sync with origin are
|
|
426
|
+
still enforced, so resuming has the same starting posture as a
|
|
427
|
+
fresh run; only step-level outcomes differ.
|
|
428
|
+
"""
|
|
358
429
|
for b in ("git", "gh"):
|
|
359
430
|
if not have(b):
|
|
360
431
|
die(f"{b!r} not found on PATH")
|
|
@@ -368,7 +439,14 @@ def preflight(target: str) -> None:
|
|
|
368
439
|
die("gh is not authenticated; run `gh auth login` first")
|
|
369
440
|
|
|
370
441
|
branch = git("rev-parse", "--abbrev-ref", "HEAD", capture=True)
|
|
371
|
-
|
|
442
|
+
release_branch = f"release/{target}"
|
|
443
|
+
allowed = {MAIN_BRANCH, release_branch} if resume else {MAIN_BRANCH}
|
|
444
|
+
if branch not in allowed:
|
|
445
|
+
if resume:
|
|
446
|
+
die(
|
|
447
|
+
f"resume must run from {MAIN_BRANCH!r} or {release_branch!r}, "
|
|
448
|
+
f"currently on {branch!r}"
|
|
449
|
+
)
|
|
372
450
|
die(f"release must run from {MAIN_BRANCH!r}, currently on {branch!r}")
|
|
373
451
|
|
|
374
452
|
porcelain = git("status", "--porcelain", capture=True)
|
|
@@ -380,17 +458,24 @@ def preflight(target: str) -> None:
|
|
|
380
458
|
# about to create a new tag anyway — local drift (e.g. from renamed
|
|
381
459
|
# release-please tags) should not block the fetch.
|
|
382
460
|
run("git", "fetch", REMOTE, "--tags", "--prune", "--force", capture=True)
|
|
383
|
-
local = git("rev-parse", "HEAD", capture=True)
|
|
384
|
-
remote = git("rev-parse", f"{REMOTE}/{MAIN_BRANCH}", capture=True)
|
|
385
|
-
if local != remote:
|
|
386
|
-
die(
|
|
387
|
-
f"local {MAIN_BRANCH} is not in sync with {REMOTE}/{MAIN_BRANCH}; "
|
|
388
|
-
"pull or push first"
|
|
389
|
-
)
|
|
390
461
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
462
|
+
# The local-in-sync-with-origin check only applies to main; if we're
|
|
463
|
+
# already on the release branch in resume mode, the relevant invariant
|
|
464
|
+
# is "main hasn't moved beyond what release/X.Y.Z branched off", which
|
|
465
|
+
# `git pull --ff-only` enforces in step 8 anyway.
|
|
466
|
+
if branch == MAIN_BRANCH:
|
|
467
|
+
local = git("rev-parse", "HEAD", capture=True)
|
|
468
|
+
remote = git("rev-parse", f"{REMOTE}/{MAIN_BRANCH}", capture=True)
|
|
469
|
+
if local != remote:
|
|
470
|
+
die(
|
|
471
|
+
f"local {MAIN_BRANCH} is not in sync with "
|
|
472
|
+
f"{REMOTE}/{MAIN_BRANCH}; pull or push first"
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if not resume:
|
|
476
|
+
tags = git("tag", "-l", target, capture=True).splitlines()
|
|
477
|
+
if target in tags:
|
|
478
|
+
die(f"tag {target!r} already exists; nothing to release")
|
|
394
479
|
|
|
395
480
|
|
|
396
481
|
# ─── plan ─────────────────────────────────────────────────────────────────────
|
|
@@ -436,7 +521,13 @@ def _step(n: int, total: int, msg: str) -> None:
|
|
|
436
521
|
print(f"[{n}/{total}] {msg}")
|
|
437
522
|
|
|
438
523
|
|
|
439
|
-
def execute(
|
|
524
|
+
def execute(
|
|
525
|
+
plan: Plan,
|
|
526
|
+
*,
|
|
527
|
+
wait_for_checks: bool,
|
|
528
|
+
dry_run: bool,
|
|
529
|
+
resume: bool = False,
|
|
530
|
+
) -> None:
|
|
440
531
|
branch = f"release/{plan.target}"
|
|
441
532
|
total = 9
|
|
442
533
|
|
|
@@ -444,57 +535,129 @@ def execute(plan: Plan, *, wait_for_checks: bool, dry_run: bool) -> None:
|
|
|
444
535
|
print("(dry-run) no git/gh mutations will be performed.")
|
|
445
536
|
return
|
|
446
537
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
538
|
+
# Probe the world once at the top so each step skip-decision is cheap.
|
|
539
|
+
pr_info = _pr_for_branch(branch) if resume else None
|
|
540
|
+
pr_state = (pr_info or {}).get("state")
|
|
541
|
+
pr_merged = pr_state == "MERGED"
|
|
542
|
+
|
|
543
|
+
# ─── 1. branch ──────────────────────────────────────────────────────────
|
|
544
|
+
if pr_merged:
|
|
545
|
+
_step(1, total, f"PR for {branch} already merged — staying on {MAIN_BRANCH}")
|
|
546
|
+
if git("rev-parse", "--abbrev-ref", "HEAD", capture=True) != MAIN_BRANCH:
|
|
547
|
+
run("git", "checkout", MAIN_BRANCH)
|
|
548
|
+
run("git", "pull", "--ff-only", REMOTE, MAIN_BRANCH)
|
|
549
|
+
elif resume and _branch_exists_local(branch):
|
|
550
|
+
_step(1, total, f"Branch {branch} exists locally — checkout")
|
|
551
|
+
run("git", "checkout", branch)
|
|
552
|
+
elif resume and _branch_exists_remote(branch):
|
|
553
|
+
_step(1, total, f"Branch {branch} exists on {REMOTE} — fetch + checkout")
|
|
554
|
+
run("git", "fetch", REMOTE, branch)
|
|
555
|
+
run("git", "checkout", "-b", branch, f"{REMOTE}/{branch}")
|
|
556
|
+
else:
|
|
557
|
+
_step(1, total, f"Create branch {branch}")
|
|
558
|
+
run("git", "checkout", "-b", branch)
|
|
461
559
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
560
|
+
# ─── 2. file mutations ──────────────────────────────────────────────────
|
|
561
|
+
if pr_merged:
|
|
562
|
+
_step(2, total, "PR already merged — skip file bumps")
|
|
563
|
+
else:
|
|
564
|
+
current_pkg = json.loads(PACKAGE_JSON.read_text(encoding="utf-8")).get("version")
|
|
565
|
+
if resume and current_pkg == plan.target:
|
|
566
|
+
_step(2, total, f"Files already at {plan.target} — skip bump")
|
|
567
|
+
else:
|
|
568
|
+
_step(2, total, "Bump package.json + marketplace.json, prepend CHANGELOG")
|
|
569
|
+
set_package_version(PACKAGE_JSON, plan.target)
|
|
570
|
+
set_marketplace_version(MARKETPLACE_JSON, plan.target)
|
|
571
|
+
prepend_changelog(CHANGELOG, plan.changelog_entry)
|
|
572
|
+
|
|
573
|
+
# ─── 3. commit ──────────────────────────────────────────────────────────
|
|
574
|
+
if pr_merged:
|
|
575
|
+
_step(3, total, "PR already merged — skip commit")
|
|
576
|
+
else:
|
|
577
|
+
last_msg = git("log", "-1", "--format=%s", capture=True)
|
|
578
|
+
porcelain = git("status", "--porcelain", capture=True)
|
|
579
|
+
if resume and last_msg == f"release: {plan.target}" and not porcelain:
|
|
580
|
+
_step(3, total, f"Last commit already `release: {plan.target}` and tree clean — skip")
|
|
581
|
+
else:
|
|
582
|
+
_step(3, total, f"Commit `release: {plan.target}`")
|
|
583
|
+
run("git", "add", str(PACKAGE_JSON), str(MARKETPLACE_JSON), str(CHANGELOG))
|
|
584
|
+
run("git", "commit", "-m", f"release: {plan.target}")
|
|
585
|
+
|
|
586
|
+
# ─── 4. push ────────────────────────────────────────────────────────────
|
|
587
|
+
if pr_merged:
|
|
588
|
+
_step(4, total, "PR already merged — skip push")
|
|
589
|
+
else:
|
|
590
|
+
# `git push -u` is naturally idempotent — it prints "Everything
|
|
591
|
+
# up-to-date" when remote already matches. No probe needed.
|
|
592
|
+
_step(4, total, f"Push {branch} to {REMOTE}")
|
|
593
|
+
run("git", "push", "-u", REMOTE, branch)
|
|
594
|
+
|
|
595
|
+
# ─── 5. PR ──────────────────────────────────────────────────────────────
|
|
596
|
+
if pr_merged:
|
|
597
|
+
_step(5, total, f"PR #{pr_info.get('number')} already merged — skip")
|
|
598
|
+
elif resume and pr_state == "OPEN":
|
|
599
|
+
_step(5, total, f"PR already open: {pr_info.get('url')}")
|
|
600
|
+
else:
|
|
601
|
+
_step(5, total, "Open pull request")
|
|
602
|
+
pr_body = (
|
|
603
|
+
f"Release {plan.target}.\n\n"
|
|
604
|
+
f"{plan.changelog_body}\n\n"
|
|
605
|
+
"Created by `scripts/release.py`."
|
|
606
|
+
)
|
|
607
|
+
run(
|
|
608
|
+
"gh", "pr", "create",
|
|
609
|
+
"--base", MAIN_BRANCH,
|
|
610
|
+
"--head", branch,
|
|
611
|
+
"--title", f"release: {plan.target}",
|
|
612
|
+
"--body", pr_body,
|
|
613
|
+
)
|
|
475
614
|
|
|
476
|
-
|
|
615
|
+
# ─── 6. wait for checks ─────────────────────────────────────────────────
|
|
616
|
+
if pr_merged:
|
|
617
|
+
_step(6, total, "PR already merged — skip checks wait")
|
|
618
|
+
elif wait_for_checks:
|
|
477
619
|
_step(6, total, "Wait for PR checks")
|
|
478
620
|
watch_pr_checks()
|
|
479
621
|
else:
|
|
480
622
|
_step(6, total, "Skip waiting for checks (--no-wait)")
|
|
481
623
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
624
|
+
# ─── 7. merge ───────────────────────────────────────────────────────────
|
|
625
|
+
if pr_merged:
|
|
626
|
+
_step(7, total, f"PR #{pr_info.get('number')} already merged — skip")
|
|
627
|
+
else:
|
|
628
|
+
_step(7, total, "Merge pull request (merge commit) and delete branch")
|
|
629
|
+
run("gh", "pr", "merge", "--merge", "--delete-branch")
|
|
630
|
+
|
|
631
|
+
# ─── 8. tag main + push tag ─────────────────────────────────────────────
|
|
632
|
+
# Always idempotent — even outside resume mode this prevents a mid-flight
|
|
633
|
+
# crash on step 9 from leaving a half-tagged release that subsequent
|
|
634
|
+
# `task release` invocations can't recover from without `--resume`.
|
|
635
|
+
if git("rev-parse", "--abbrev-ref", "HEAD", capture=True) != MAIN_BRANCH:
|
|
636
|
+
run("git", "checkout", MAIN_BRANCH)
|
|
487
637
|
run("git", "pull", "--ff-only", REMOTE, MAIN_BRANCH)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
638
|
+
|
|
639
|
+
if _tag_exists_local(plan.target):
|
|
640
|
+
if _tag_exists_remote(plan.target):
|
|
641
|
+
_step(8, total, f"Tag {plan.target} already on {REMOTE} — skip")
|
|
642
|
+
else:
|
|
643
|
+
_step(8, total, f"Tag {plan.target} exists locally — push only")
|
|
644
|
+
run("git", "push", REMOTE, plan.target)
|
|
645
|
+
else:
|
|
646
|
+
_step(8, total, f"Tag merge commit and push {plan.target}")
|
|
647
|
+
run("git", "tag", plan.target)
|
|
648
|
+
run("git", "push", REMOTE, plan.target)
|
|
649
|
+
|
|
650
|
+
# ─── 9. GitHub Release ──────────────────────────────────────────────────
|
|
651
|
+
if _release_exists(plan.target):
|
|
652
|
+
_step(9, total, f"GitHub Release {plan.target} already exists — skip")
|
|
653
|
+
else:
|
|
654
|
+
_step(9, total, "Create GitHub Release (triggers publish-npm on the tag)")
|
|
655
|
+
notes = plan.changelog_body or f"Release {plan.target}"
|
|
656
|
+
run(
|
|
657
|
+
"gh", "release", "create", plan.target,
|
|
658
|
+
"--title", plan.target,
|
|
659
|
+
"--notes", notes,
|
|
660
|
+
)
|
|
498
661
|
|
|
499
662
|
print()
|
|
500
663
|
print(f"✅ Released {plan.target}")
|
|
@@ -535,6 +698,15 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
|
|
|
535
698
|
"--no-wait", action="store_true",
|
|
536
699
|
help="Merge immediately without waiting for PR checks to pass.",
|
|
537
700
|
)
|
|
701
|
+
p.add_argument(
|
|
702
|
+
"--resume", action="store_true",
|
|
703
|
+
help=(
|
|
704
|
+
"Recover from a partial run. Each step probes existing state "
|
|
705
|
+
"(branch, commit, PR, tag, GitHub Release) and skips work that "
|
|
706
|
+
"is already done. Use this when an earlier `task release` "
|
|
707
|
+
"crashed mid-pipeline."
|
|
708
|
+
),
|
|
709
|
+
)
|
|
538
710
|
return p.parse_args(argv)
|
|
539
711
|
|
|
540
712
|
|
|
@@ -545,6 +717,49 @@ def resolve_bump(override: str | None, commits: list[Commit]) -> str:
|
|
|
545
717
|
return infer_bump(commits)
|
|
546
718
|
|
|
547
719
|
|
|
720
|
+
_RELEASE_BRANCH_RE = re.compile(r"^release/(\d+\.\d+\.\d+)$")
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _detect_in_flight_target() -> str | None:
|
|
724
|
+
"""Find the in-flight release target from existing release branches.
|
|
725
|
+
|
|
726
|
+
Resume mode needs to know which `release/X.Y.Z` is being recovered,
|
|
727
|
+
not what the next bump would be. The release branch name is the
|
|
728
|
+
canonical anchor: it was committed by step 1 of an earlier run and
|
|
729
|
+
is the only state guaranteed to survive a partial pipeline.
|
|
730
|
+
|
|
731
|
+
Local branches win over remote, current-branch wins over both — if
|
|
732
|
+
you ran `git checkout release/1.15.0`, that's the target. Returns
|
|
733
|
+
None if no release branch exists; caller falls back to the regular
|
|
734
|
+
bump-inference path.
|
|
735
|
+
"""
|
|
736
|
+
head = git("rev-parse", "--abbrev-ref", "HEAD", capture=True)
|
|
737
|
+
m = _RELEASE_BRANCH_RE.match(head)
|
|
738
|
+
if m:
|
|
739
|
+
return m.group(1)
|
|
740
|
+
|
|
741
|
+
local_raw = git("for-each-ref", "--format=%(refname:short)", "refs/heads/release/", capture=True)
|
|
742
|
+
candidates = [
|
|
743
|
+
m.group(1)
|
|
744
|
+
for line in local_raw.splitlines()
|
|
745
|
+
if (m := _RELEASE_BRANCH_RE.match(line.strip()))
|
|
746
|
+
]
|
|
747
|
+
remote_raw = git(
|
|
748
|
+
"for-each-ref", "--format=%(refname:short)",
|
|
749
|
+
f"refs/remotes/{REMOTE}/release/", capture=True,
|
|
750
|
+
)
|
|
751
|
+
for line in remote_raw.splitlines():
|
|
752
|
+
bare = line.strip().removeprefix(f"{REMOTE}/")
|
|
753
|
+
if (m := _RELEASE_BRANCH_RE.match(bare)):
|
|
754
|
+
candidates.append(m.group(1))
|
|
755
|
+
|
|
756
|
+
if not candidates:
|
|
757
|
+
return None
|
|
758
|
+
# Sort semver-aware so 1.10.0 > 1.9.0 (lexicographic would lose).
|
|
759
|
+
candidates.sort(key=parse_version)
|
|
760
|
+
return candidates[-1]
|
|
761
|
+
|
|
762
|
+
|
|
548
763
|
def main(argv: list[str] | None = None) -> int:
|
|
549
764
|
args = _parse_args(list(sys.argv[1:] if argv is None else argv))
|
|
550
765
|
|
|
@@ -554,11 +769,22 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
554
769
|
prev = latest_tag()
|
|
555
770
|
commits = commits_since(prev)
|
|
556
771
|
bump = resolve_bump(args.bump_override, commits)
|
|
557
|
-
|
|
772
|
+
|
|
773
|
+
# Resume mode: prefer an existing `release/X.Y.Z` over computed bump,
|
|
774
|
+
# so we don't accidentally start a 1.16.0 release while 1.15.0 is
|
|
775
|
+
# still in flight. Explicit --version still wins.
|
|
776
|
+
in_flight = _detect_in_flight_target() if args.resume else None
|
|
777
|
+
if args.explicit:
|
|
778
|
+
target = args.explicit
|
|
779
|
+
elif in_flight:
|
|
780
|
+
target = in_flight
|
|
781
|
+
print(f"(resume) detected in-flight release branch release/{in_flight}")
|
|
782
|
+
else:
|
|
783
|
+
target = bump_version(current, bump)
|
|
558
784
|
parse_version(target)
|
|
559
785
|
|
|
560
786
|
if not args.dry_run:
|
|
561
|
-
preflight(target)
|
|
787
|
+
preflight(target, resume=args.resume)
|
|
562
788
|
|
|
563
789
|
today = _date.today().isoformat()
|
|
564
790
|
full, body = render_changelog_entry(target, prev, commits, today)
|
|
@@ -572,6 +798,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
572
798
|
changelog_entry=full,
|
|
573
799
|
)
|
|
574
800
|
print_preview(plan)
|
|
801
|
+
if args.resume:
|
|
802
|
+
print("(resume) probing existing state — completed steps will be skipped.")
|
|
575
803
|
|
|
576
804
|
if args.dry_run:
|
|
577
805
|
return 0
|
|
@@ -580,7 +808,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
580
808
|
print("aborted.")
|
|
581
809
|
return 1
|
|
582
810
|
|
|
583
|
-
execute(
|
|
811
|
+
execute(
|
|
812
|
+
plan,
|
|
813
|
+
wait_for_checks=not args.no_wait,
|
|
814
|
+
dry_run=False,
|
|
815
|
+
resume=args.resume,
|
|
816
|
+
)
|
|
584
817
|
return 0
|
|
585
818
|
|
|
586
819
|
|
|
@@ -27,6 +27,47 @@
|
|
|
27
27
|
"type": "string",
|
|
28
28
|
"pattern": "^[a-z][a-z0-9-]*$"
|
|
29
29
|
}
|
|
30
|
+
},
|
|
31
|
+
"suggestion": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"required": ["eligible"],
|
|
35
|
+
"description": "Context-aware command-suggestion metadata. The suggestion layer is read-only and never auto-executes; see road-to-context-aware-command-suggestion.",
|
|
36
|
+
"properties": {
|
|
37
|
+
"eligible": {
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"description": "true → suggestion layer may surface this command; false → never suggest (still works when typed explicitly)."
|
|
40
|
+
},
|
|
41
|
+
"rationale": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"minLength": 0,
|
|
44
|
+
"maxLength": 240,
|
|
45
|
+
"description": "Why the command is ineligible. Required when eligible: false (enforced by linter)."
|
|
46
|
+
},
|
|
47
|
+
"trigger_description": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"minLength": 0,
|
|
50
|
+
"maxLength": 240,
|
|
51
|
+
"description": "Natural-language pattern trigger. Required when eligible: true (enforced by linter)."
|
|
52
|
+
},
|
|
53
|
+
"trigger_context": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"minLength": 0,
|
|
56
|
+
"maxLength": 240,
|
|
57
|
+
"description": "Concrete signal trigger (branch name, file pattern, recent tool output). Required when eligible: true (enforced by linter)."
|
|
58
|
+
},
|
|
59
|
+
"confidence_floor": {
|
|
60
|
+
"type": "number",
|
|
61
|
+
"minimum": 0,
|
|
62
|
+
"description": "Optional per-command override of the global confidence floor (0.0–1.0). Defaults to settings."
|
|
63
|
+
},
|
|
64
|
+
"cooldown": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"minLength": 0,
|
|
67
|
+
"maxLength": 16,
|
|
68
|
+
"description": "Optional per-command override of the global cooldown duration (e.g. '10m'). Defaults to settings."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
30
71
|
}
|
|
31
72
|
}
|
|
32
73
|
}
|
package/scripts/skill_linter.py
CHANGED
|
@@ -772,6 +772,70 @@ def lint_rule(path: Path, text: str) -> LintResult:
|
|
|
772
772
|
)
|
|
773
773
|
|
|
774
774
|
|
|
775
|
+
def _lint_command_suggestion_block(text: str) -> List[Issue]:
|
|
776
|
+
"""Validate the suggestion frontmatter block (road-to-context-aware-command-suggestion).
|
|
777
|
+
|
|
778
|
+
Schema-shape is enforced upstream by validate_frontmatter; this function adds the
|
|
779
|
+
*conditional* content rules that JSON Schema (Draft-07 subset used here) cannot
|
|
780
|
+
express: trigger fields must be non-empty when eligible, rationale must be
|
|
781
|
+
non-empty when ineligible.
|
|
782
|
+
"""
|
|
783
|
+
issues: List[Issue] = []
|
|
784
|
+
data, _offset = parse_frontmatter_for_schema(text)
|
|
785
|
+
if data is None:
|
|
786
|
+
return issues
|
|
787
|
+
suggestion = data.get("suggestion")
|
|
788
|
+
if suggestion is None:
|
|
789
|
+
issues.append(Issue(
|
|
790
|
+
"warning", "missing_suggestion_block",
|
|
791
|
+
"Command frontmatter is missing the 'suggestion' block — required by "
|
|
792
|
+
"road-to-context-aware-command-suggestion Phase 2.",
|
|
793
|
+
))
|
|
794
|
+
return issues
|
|
795
|
+
if not isinstance(suggestion, dict):
|
|
796
|
+
issues.append(Issue("error", "invalid_suggestion_block", "'suggestion' must be a mapping"))
|
|
797
|
+
return issues
|
|
798
|
+
eligible = suggestion.get("eligible")
|
|
799
|
+
if eligible is True:
|
|
800
|
+
td = (suggestion.get("trigger_description") or "").strip()
|
|
801
|
+
tc = (suggestion.get("trigger_context") or "").strip()
|
|
802
|
+
if not td:
|
|
803
|
+
issues.append(Issue(
|
|
804
|
+
"error", "missing_trigger_description",
|
|
805
|
+
"suggestion.eligible=true requires a non-empty 'trigger_description'.",
|
|
806
|
+
))
|
|
807
|
+
elif len(td) < 10:
|
|
808
|
+
issues.append(Issue(
|
|
809
|
+
"warning", "trigger_description_too_short",
|
|
810
|
+
"suggestion.trigger_description is suspiciously short (<10 chars); "
|
|
811
|
+
"linter rejects empty or overly generic patterns.",
|
|
812
|
+
))
|
|
813
|
+
if not tc:
|
|
814
|
+
issues.append(Issue(
|
|
815
|
+
"error", "missing_trigger_context",
|
|
816
|
+
"suggestion.eligible=true requires a non-empty 'trigger_context'.",
|
|
817
|
+
))
|
|
818
|
+
elif len(tc) < 10:
|
|
819
|
+
issues.append(Issue(
|
|
820
|
+
"warning", "trigger_context_too_short",
|
|
821
|
+
"suggestion.trigger_context is suspiciously short (<10 chars); "
|
|
822
|
+
"linter rejects empty or overly generic patterns.",
|
|
823
|
+
))
|
|
824
|
+
elif eligible is False:
|
|
825
|
+
rationale = (suggestion.get("rationale") or "").strip()
|
|
826
|
+
if not rationale:
|
|
827
|
+
issues.append(Issue(
|
|
828
|
+
"error", "missing_suggestion_rationale",
|
|
829
|
+
"suggestion.eligible=false requires a non-empty 'rationale'.",
|
|
830
|
+
))
|
|
831
|
+
else:
|
|
832
|
+
issues.append(Issue(
|
|
833
|
+
"error", "invalid_suggestion_eligible",
|
|
834
|
+
"suggestion.eligible must be true or false.",
|
|
835
|
+
))
|
|
836
|
+
return issues
|
|
837
|
+
|
|
838
|
+
|
|
775
839
|
def lint_command(path: Path, text: str) -> LintResult:
|
|
776
840
|
issues: List[Issue] = []
|
|
777
841
|
suggestions: List[str] = []
|
|
@@ -800,6 +864,23 @@ def lint_command(path: Path, text: str) -> LintResult:
|
|
|
800
864
|
if not description:
|
|
801
865
|
issues.append(Issue("warning", "missing_description", "Frontmatter description is missing"))
|
|
802
866
|
|
|
867
|
+
# suggestion block (road-to-context-aware-command-suggestion Phase 2)
|
|
868
|
+
issues.extend(_lint_command_suggestion_block(text))
|
|
869
|
+
|
|
870
|
+
# deprecation-shim warning line (P0.8b — command-clusters contract)
|
|
871
|
+
if "superseded_by:" in frontmatter:
|
|
872
|
+
shim_warning = re.search(
|
|
873
|
+
r"⚠️\s+/[a-z][a-z0-9-]*\s+is deprecated;\s+use\s+/[a-z][a-z0-9 -]+\s+instead",
|
|
874
|
+
text,
|
|
875
|
+
)
|
|
876
|
+
if not shim_warning:
|
|
877
|
+
issues.append(Issue(
|
|
878
|
+
"error", "shim_missing_warning",
|
|
879
|
+
"Deprecation shim must contain a one-line warning matching "
|
|
880
|
+
"'⚠️ /<old-name> is deprecated; use /<cluster> <sub> instead.'"
|
|
881
|
+
" (see docs/contracts/command-clusters.md § Deprecation shim contract)"
|
|
882
|
+
))
|
|
883
|
+
|
|
803
884
|
# --- Structure checks ---
|
|
804
885
|
if not H1_PATTERN.search(text):
|
|
805
886
|
issues.append(Issue("error", "missing_h1", "Command is missing an H1 heading (# Title)"))
|