From 998162e3b49a3a32133601952c1b39edf7914c96 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 4 Jul 2026 01:55:27 -0400 Subject: [PATCH 1/6] docs: plan wfctl repair lifecycle --- decisions/0053-top-level-wfctl-repair.md | 31 +++++ ...2026-07-04-wfctl-repair-alignment-check.md | 28 +++++ .../2026-07-04-wfctl-repair-design-review.md | 42 +++++++ docs/plans/2026-07-04-wfctl-repair-design.md | 77 +++++++++++++ .../2026-07-04-wfctl-repair-plan-review.md | 55 +++++++++ docs/plans/2026-07-04-wfctl-repair.md | 109 ++++++++++++++++++ 6 files changed, 342 insertions(+) create mode 100644 decisions/0053-top-level-wfctl-repair.md create mode 100644 docs/plans/2026-07-04-wfctl-repair-alignment-check.md create mode 100644 docs/plans/2026-07-04-wfctl-repair-design-review.md create mode 100644 docs/plans/2026-07-04-wfctl-repair-design.md create mode 100644 docs/plans/2026-07-04-wfctl-repair-plan-review.md create mode 100644 docs/plans/2026-07-04-wfctl-repair.md diff --git a/decisions/0053-top-level-wfctl-repair.md b/decisions/0053-top-level-wfctl-repair.md new file mode 100644 index 00000000..e0089e19 --- /dev/null +++ b/decisions/0053-top-level-wfctl-repair.md @@ -0,0 +1,31 @@ +# 0053. Add guarded wfctl repair + +**Status:** Accepted +**Date:** 2026-07-04 +**Decision-makers:** Codex +**Related:** `decisions/0050-plugin-lock-install-modes.md`, `decisions/0052-top-level-wfctl-doctor.md`, `docs/plans/2026-07-04-wfctl-repair-design.md`, `docs/plans/2026-07-04-wfctl-repair.md` + +## Context + +`wfctl doctor` now identifies project/plugin lifecycle drift and prints concrete +fix commands. Repeating those commands manually is useful but still leaves a +partial-checkout footgun: users must infer ordering when a manifest, lockfile, +and installed plugin directory disagree. Provider-specific repair remains out +of scope for core per the plugin boundary. + +## Decision + +Add top-level `wfctl repair` as a guarded orchestrator over existing lifecycle +commands. Default mode is dry-run and emits the planned commands. `--apply` +executes only safe project plugin repairs: regenerate `.wfctl-lock.yaml` with +`wfctl plugin lock` when manifest/lock provenance is stale or incomplete, then +sync installed project plugins with `wfctl plugin install`. Binary self-update, +workflow generation, parse fixes, global plugin mutation, and provider-specific +repairs stay as suggestions. + +## Consequences + +Users get one command for the common doctor fix path while existing commands +remain authoritative for mutation. The CLI adds a small orchestration layer and +tests must guard dry-run/apply behavior. Rollback is simple: remove the command, +docs, and tests; `doctor`, `plugin lock`, and `plugin install` continue to work. diff --git a/docs/plans/2026-07-04-wfctl-repair-alignment-check.md b/docs/plans/2026-07-04-wfctl-repair-alignment-check.md new file mode 100644 index 00000000..7b92b34e --- /dev/null +++ b/docs/plans/2026-07-04-wfctl-repair-alignment-check.md @@ -0,0 +1,28 @@ +### Alignment Report + +**Status:** PASS + +**Coverage:** + +| Design Requirement | Plan Task(s) | Status | +|---|---|---| +| Add top-level `wfctl repair` | Task 2 | Covered | +| Dry-run default and `--apply` mutation gate | Task 1, Task 2, Task 4 | Covered | +| Delegate mutation to `plugin lock` / `plugin install` | Task 1, Task 2 | Covered | +| Support doctor-like project/plugin flags and text/json output | Task 1, Task 2 | Covered | +| Keep unsupported fixes suggest-only | Task 1, Task 2, Task 3 | Covered | +| Docs and command wiring | Task 2, Task 3, Task 4 | Covered | +| Runtime/help/package verification | Task 4 | Covered | + +**Scope Check:** + +| Plan Task | Design Requirement | Status | +|---|---|---| +| Task 1 | Planner/apply behavior must be tested before implementation | Justified | +| Task 2 | Command implementation and CLI wiring | Justified | +| Task 3 | Docs update for behavior/CLI change | Justified | +| Task 4 | Multi-component validation and PR readiness | Justified | + +**Manifest Check:** `/Users//.codex/plugins/cache/autodev-marketplace/autodev/6.5.11/tests/plan-scope-check.sh --plan docs/plans/2026-07-04-wfctl-repair.md` -> PASS. + +**Drift Items:** none. diff --git a/docs/plans/2026-07-04-wfctl-repair-design-review.md b/docs/plans/2026-07-04-wfctl-repair-design-review.md new file mode 100644 index 00000000..80c99b8a --- /dev/null +++ b/docs/plans/2026-07-04-wfctl-repair-design-review.md @@ -0,0 +1,42 @@ +### Adversarial Review Report + +**Phase:** design +**Artifact:** `docs/plans/2026-07-04-wfctl-repair-design.md` +**Status:** PASS + +**Findings (Critical):** +- None. + +**Findings (Important):** +- None. + +**Findings (Minor):** +- `D1` [YAGNI] Design could grow into a general repair registry. Recommendation: keep first PR to project plugin lock/install only. _Resolution: design out-of-scope excludes typed repair registry/provider repair._ +- `D2` [Failure modes] Relock succeeds then install fails leaves a newer lock with old/missing binaries. Recommendation: document rerun/idempotency and stop on first failing action. _Resolution: architecture/executor and assumptions state stop/rerun behavior._ +- `D3` [User intent] User also asked for downstream editor/IDE validation. Recommendation: keep this PR focused and run editor/IDE as a separate downstream phase. _Resolution: design scope is Workflow CLI repair only; downstream validation follows after merge/release._ + +**Bug-class scan transcript:** + +| Class | Result | Note | +|---|---|---| +| Project-guidance conflicts | Clean | Reuses core lifecycle orchestration and keeps provider behavior in plugins. | +| Assumptions under attack | Clean | A1-A3 list command-authority, idempotency, and dry-run assumptions. | +| Repo-precedent conflicts | Clean | Follows `cmd/wfctl` top-level command pattern and ADR 0050/0052. | +| Artifact-class precedent | Clean | New command touches `cmd/wfctl/*.go`, `wfctl.yaml`, docs, tests like `doctor`. | +| YAGNI violations | Minor | General repair registry explicitly rejected. | +| Missing failure modes | Minor | Partial relock/install addressed via ordered stop and rerun. | +| Security / privacy | Clean | No new secrets; downloads flow through existing checksum paths. | +| Infrastructure impact | Clean | Local filesystem writes only; no cloud resources. | +| Multi-component validation | Clean | CLI wiring smoke and package tests planned. | +| Declared integration proof | Clean | Command integration is embedded CLI workflow; help smoke proves consumer path. | +| Contributed UI rendering proof | Clean | No UI. | +| Rollback story | Clean | Remove command/docs/tests; no runtime migration. | +| Simpler alternative | Clean | Docs-only and `doctor --repair` considered/rejected. | +| User-intent drift | Minor | Downstream editor/IDE is separate phase, not dropped. | +| Existence / runtime-validity | Clean | `doctor`, `plugin lock`, `plugin install`, and `wfctl.yaml` surfaces exist. | + +**Options the author may not have considered:** +1. `wfctl doctor --fix`: compact UX but violates ADR 0052's read-only promise. +2. `wfctl plugin repair`: narrower but hides project/binary diagnostics behind plugin subcommand. + +**Verdict reasoning:** PASS. The design is intentionally narrow, preserves the plugin boundary, and delegates mutation to established lifecycle commands. diff --git a/docs/plans/2026-07-04-wfctl-repair-design.md b/docs/plans/2026-07-04-wfctl-repair-design.md new file mode 100644 index 00000000..85dcecbb --- /dev/null +++ b/docs/plans/2026-07-04-wfctl-repair-design.md @@ -0,0 +1,77 @@ +# wfctl Repair Design + +## Global Design Guidance + +Source: `AGENTS.md`, `docs/AGENT_GUIDE.md`, `docs/REPO_LAYOUT.md`, `decisions/0050-plugin-lock-install-modes.md`, `decisions/0052-top-level-wfctl-doctor.md`, workspace `docs/PORTFOLIO.md`. + +| guidance | design response | +|---|---| +| `wfctl` owns lifecycle orchestration; plugins own provider-specific behavior | `repair` only runs `plugin lock` and `plugin install`; no provider-specific fixes. | +| Update docs/tests with CLI behavior | Add `cmd/wfctl/*_test.go`, README, `docs/WFCTL.md`, embedded `wfctl.yaml`. | +| Use `GOWORK=off` | Verification commands use `GOWORK=off`. | +| Reuse existing tooling inventory | Existing `wfctl doctor`, `plugin lock`, `plugin install`, `plugin ci`, and `update` cover needed capabilities; no new resolver. | + +## Goal + +Add `wfctl repair`: a guarded, dry-run-by-default lifecycle repair command that turns actionable `doctor` plugin diagnostics into ordered existing commands. + +## Approaches + +| option | summary | trade-off | +|---|---|---| +| A recommended | Top-level `repair`, dry-run default, `--apply` delegates to `plugin lock`/`plugin install` | Clear UX, low mutation surface, reuses tested paths. | +| B | Add `doctor --repair` | Overloads read-only command promised by ADR 0052. | +| C | Add full repair framework with typed fix IDs | More extensible but premature; larger public contract. | + +## Architecture + +- New `cmd/wfctl/repair.go`. +- Flags mirror doctor project/plugin paths: `--workflow`, `--manifest`, `--lock-file`, `--plugin-dir`, `--include-global`, `--online`, `--format`. +- Mutation requires `--apply`; dry-run prints planned commands. +- Planner reads the `doctorReport` plus manifest/lock/install state and emits ordered actions: + 1. `plugin lock` when manifest exists and lockfile missing/stale/incomplete. + 2. `plugin install` when a project plugin is missing/stale or after relock. + 3. Suggest-only items for binary update, workflow creation/parse failures, missing manifest, global plugin state. +- Executor calls injected function hooks in tests; production hooks call existing `runPluginLock` and `runPluginInstall`. + +## Assumptions + +| id | assumption | challenge | fallback | +|---|---|---|---| +| A1 | Existing `plugin lock`/`plugin install` are correct mutation authority | If they are wrong, repair repeats the bug | Keep repair as orchestration only; fix authoritative commands separately. | +| A2 | Reinstalling after relock is safe/idempotent | Registry/network failure may leave lock updated but install incomplete | Ordered output shows failure; rerun `wfctl repair --apply`. | +| A3 | Default dry-run avoids surprising file/network mutation | Users may expect repair to mutate by default | Docs and usage show `--apply` explicitly. | + +## Self-Challenge + +1. Laziest solution: docs-only "run doctor then fixes." Rejected: user asked to address repair follow-up and repeated lifecycle drift now justifies a command. +2. Fragile assumption: relock+install idempotency. Mitigation: delegate to package-manager-like install semantics from ADR 0050 and do not delete existing plugins first. +3. YAGNI sweep: no typed repair registry, no plugin/provider repair API, no binary self-update, no global mutation. + +## Security Review + +- No new secrets/auth. +- `--apply` may download plugins through existing checksum/compatibility enforcement; repair does not add `--skip-checksum`. +- Dry-run default limits accidental supply-chain/network mutation. +- Online release check remains opt-in via `--online`. + +## Infrastructure Impact + +- No cloud resources, migrations, services, or deployment config. +- Local filesystem writes only when `--apply`: `.wfctl-lock.yaml` and `data/plugins`. +- Rollback: revert command changes; user can restore prior lockfile/plugins from VCS/cache or rerun prior pinned install. + +## Multi-Component Validation + +- Unit tests cover planner and apply ordering with injected runners. +- CLI smoke proves `wfctl repair --help` loads through embedded command wiring. +- Dry-run smoke against a temp project verifies no file mutation. +- Package test verifies doctor/repair/plugin lifecycle command compatibility. + +## Out Of Scope + +- Provider-specific repair. +- Binary self-update. +- Creating new workflow projects or plugin manifests. +- Global plugin mutation. +- Changing plugin resolution/install semantics. diff --git a/docs/plans/2026-07-04-wfctl-repair-plan-review.md b/docs/plans/2026-07-04-wfctl-repair-plan-review.md new file mode 100644 index 00000000..0d11efb0 --- /dev/null +++ b/docs/plans/2026-07-04-wfctl-repair-plan-review.md @@ -0,0 +1,55 @@ +### Adversarial Review Report + +**Phase:** plan +**Artifact:** `docs/plans/2026-07-04-wfctl-repair.md` +**Status:** PASS + +**Findings (Critical):** +- None. + +**Findings (Important):** +- None. + +**Findings (Minor):** +- `P1` [Verification-class mismatch] Docs task uses `rg`, not render preview. Recommendation: acceptable for CLI docs in this repo if package/help tests cover command truth. _Resolution: Task 4 includes help/runtime smoke._ +- `P2` [Hidden dependency] Task 3 docs depend on Task 2 flag names. Recommendation: execute serially in one PR. _Resolution: PR grouping is one serial PR._ +- `P3` [Rollback wiring] Local plugin install rollback cannot recover deleted/corrupt plugin cache. Recommendation: avoid delete-first behavior. _Resolution: implementation delegates to existing install and does not remove plugins._ + +**Bug-class scan transcript:** + +| Class | Result | Note | +|---|---|---| +| Project-guidance conflicts | Clean | Plan uses `GOWORK=off`, docs/tests, command layout. | +| Assumptions under attack | Clean | Plan tests dry-run/apply and does not depend on provider specifics. | +| Repo-precedent conflicts | Clean | Mirrors doctor command additions. | +| Artifact-class precedent | Clean | CLI command + embedded `wfctl.yaml` + docs/test path matches existing commands. | +| YAGNI violations | Clean | One command, no registry/framework. | +| Missing failure modes | Clean | Stop-on-first-error and no delete-first behavior planned. | +| Security / privacy | Clean | Existing install checksum paths only. | +| Infrastructure impact | Clean | No infra. | +| Multi-component validation | Clean | CLI help and embedded command wiring are checked. | +| Declared integration proof | Clean | Embedded CLI workflow is validated by runtime help. | +| Contributed UI rendering proof | Clean | No UI. | +| Rollback story | Minor | Cache recovery not guaranteed; acceptable because no destructive delete is planned. | +| Simpler alternative | Clean | Covered by design. | +| User-intent drift | Clean | Implements repair follow-up; downstream editor/IDE handled after. | +| Existence / runtime-validity | Clean | Existing target command names verified in repo. | +| Over/under-decomposition | Clean | Four tasks are reasonable for one CLI command. | +| Verification-class mismatch | Minor | Docs render preview substituted with grep/help/package tests. | +| Auth/authz chain composition | Clean | No auth. | +| Hidden serial dependencies | Minor | One PR serial execution avoids collision. | +| Missing rollback wiring | Minor | Revert PR is sufficient for CLI command. | +| Missing integration proof | Clean | Runtime command help exercises embedded CLI. | +| Missing declared integration matrix | Clean | No external integration added. | +| Missing contributed UI route proof | Clean | No UI. | +| Infrastructure verification mismatch | Clean | No infra. | +| Plugin-loader runtime layout | Clean | Does not spawn plugins directly. | +| Config-validation schema rules | Clean | Embedded `wfctl.yaml` command wiring mirrors existing command entries. | +| Identifier / naming-convention match | Clean | Flags use existing hyphenated CLI convention. | +| Planned-code compile-validity | Clean | Plan embeds no production Go snippets. | + +**Options the author may not have considered:** +1. Full test of real `plugin install` with local fake registry: stronger proof but high setup cost; injected runner plus package tests is enough because install itself is already covered. +2. Make `--apply` the default: faster UX but unsafe for network/filesystem mutation. + +**Verdict reasoning:** PASS. The plan traces to the design, keeps one reviewable PR, and includes focused tests plus CLI runtime validation. diff --git a/docs/plans/2026-07-04-wfctl-repair.md b/docs/plans/2026-07-04-wfctl-repair.md new file mode 100644 index 00000000..f494df54 --- /dev/null +++ b/docs/plans/2026-07-04-wfctl-repair.md @@ -0,0 +1,109 @@ +# wfctl Repair Implementation Plan + +> **For the implementing agent:** REQUIRED SUB-SKILL: Use autodev:executing-plans to implement this plan task-by-task. + +**Goal:** Add dry-run-by-default `wfctl repair` for project plugin lifecycle drift. + +**Architecture:** Reuse `doctor` diagnostics and existing `plugin lock` / `plugin install` mutation paths. `repair` plans ordered actions, requires `--apply` to mutate, and keeps unsupported/provider-specific fixes as suggestions. + +**Tech Stack:** Go stdlib CLI, existing `cmd/wfctl` helpers, Workflow embedded `wfctl.yaml`. + +**Base branch:** main + +--- + +## Scope Manifest + +**PR Count:** 1 +**Tasks:** 4 +**Estimated Lines of Change:** ~420 + +**Out of scope:** +- Provider-specific repair APIs or plugin-owned fixes. +- Binary self-update execution. +- Global plugin mutation. +- New plugin resolution/install behavior. + +**PR Grouping:** + +| PR # | Title | Tasks | Branch | +|------|-------|-------|--------| +| 1 | Add guarded wfctl repair | Task 1, Task 2, Task 3, Task 4 | `feat/wfctl-repair-lifecycle` | + +**Status:** Draft + +### Task 1: Repair Planner Tests + +**Files:** +- Create: `cmd/wfctl/repair_test.go` + +**Steps:** +1. Add test fixture reusing `writeDoctorFixture`. +2. Test stale lock + missing install dry-run returns ordered `plugin lock` then `plugin install`. +3. Test missing manifest yields suggestion-only and no executable actions. +4. Test `--apply` uses injected runner and records lock/install args. +5. Run: `GOWORK=off go test ./cmd/wfctl -run 'TestRepair' -count=1` +6. Expected: FAIL with undefined `runRepairWithOutput` / repair types. +7. Commit after Task 2 green. + +### Task 2: Repair Command Implementation + +**Files:** +- Create: `cmd/wfctl/repair.go` +- Modify: `cmd/wfctl/main.go` +- Modify: `cmd/wfctl/wfctl.yaml` + +**Steps:** +1. Implement flags: `--workflow`, `--manifest`, `--lock-file`, `--plugin-dir`, `--include-global`, `--online`, `--format text|json`, `--apply`. +2. Implement `repairReport`, `repairAction`, `repairRunner`. +3. Planner: + - call `buildDoctorReport`. + - inspect manifest/lock/install state. + - emit `plugin lock` action when lock missing/stale/incomplete. + - emit `plugin install` action when relock is planned or installed plugins mismatch. + - emit suggest-only entries for missing workflow/manifest, parse errors, update hints, global info. +4. Executor: + - dry-run prints `DRY-RUN` and commands. + - `--apply` runs lock before install; stop on first error. +5. Wire `commands["repair"]` and `cmd-repair` in `wfctl.yaml`. +6. Run: `GOWORK=off go test ./cmd/wfctl -run 'TestRepair|TestDoctorCommandWiring' -count=1` +7. Expected: PASS. +8. Regression proof: temporarily remove command wiring; `TestRepairCommandWiring` fails; restore; test passes. +9. Commit: `feat(wfctl): add guarded repair command`. + +Rollback: revert this commit; existing `doctor`, `plugin lock`, and `plugin install` remain. + +### Task 3: Docs + +**Files:** +- Modify: `README.md` +- Modify: `docs/WFCTL.md` +- Modify: `docs/WFCTL_INSTALLATION.md` + +**Steps:** +1. Add README command list entry for `wfctl repair`. +2. Add `docs/WFCTL.md` section after `doctor`: flags, dry-run/apply examples, mutation boundaries. +3. Add install lifecycle doc note: `doctor` diagnoses, `repair --apply` relocks/reinstalls project plugins. +4. Run: `rg -n "wfctl repair|repair --apply|cmd-repair" README.md docs/WFCTL.md docs/WFCTL_INSTALLATION.md cmd/wfctl/wfctl.yaml` +5. Expected: command documented and wired. +6. Commit: `docs(wfctl): document repair lifecycle`. + +### Task 4: Verification And PR + +**Files:** +- No new source unless verification finds defects. + +**Steps:** +1. Run focused tests: `GOWORK=off go test ./cmd/wfctl -run 'TestRepair|TestDoctor|TestMain' -count=1` +2. Expected: PASS. +3. Run package tests: `GOWORK=off go test ./cmd/wfctl` +4. Expected: PASS. +5. Run CLI help: `GOWORK=off go run ./cmd/wfctl repair --help` +6. Expected: usage text includes `--apply`. +7. Run dry-run smoke against temp fixture. +8. Expected: output contains `DRY-RUN`, `wfctl plugin lock`, `wfctl plugin install`; no mutation without `--apply`. +9. Run `git diff --check origin/main...HEAD`. +10. Expected: no whitespace errors. +11. Open PR, monitor CI/review, admin-merge once green and review addressed. + +Rollback: revert PR; no runtime migration or deployment rollback. From f50a97ece7b785a6ff58bcd3d0a48a7d68d4ae59 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 4 Jul 2026 01:56:23 -0400 Subject: [PATCH 2/6] chore: lock scope for wfctl repair --- docs/plans/2026-07-04-wfctl-repair.md | 2 +- docs/plans/2026-07-04-wfctl-repair.md.scope-lock | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 docs/plans/2026-07-04-wfctl-repair.md.scope-lock diff --git a/docs/plans/2026-07-04-wfctl-repair.md b/docs/plans/2026-07-04-wfctl-repair.md index f494df54..4270f0c4 100644 --- a/docs/plans/2026-07-04-wfctl-repair.md +++ b/docs/plans/2026-07-04-wfctl-repair.md @@ -30,7 +30,7 @@ |------|-------|-------|--------| | 1 | Add guarded wfctl repair | Task 1, Task 2, Task 3, Task 4 | `feat/wfctl-repair-lifecycle` | -**Status:** Draft +**Status:** Locked 2026-07-04T05:55:40Z ### Task 1: Repair Planner Tests diff --git a/docs/plans/2026-07-04-wfctl-repair.md.scope-lock b/docs/plans/2026-07-04-wfctl-repair.md.scope-lock new file mode 100644 index 00000000..e5d4eb0e --- /dev/null +++ b/docs/plans/2026-07-04-wfctl-repair.md.scope-lock @@ -0,0 +1 @@ +2a22706c2ab6769b484ea609ed49872a2ef195ff6166e2ddc9f1a27e1f07104b From decb4b1b8435ed48d78c76a0a0ebed82e97b0490 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 4 Jul 2026 01:59:30 -0400 Subject: [PATCH 3/6] feat(wfctl): add guarded repair command --- cmd/wfctl/main.go | 1 + cmd/wfctl/repair.go | 309 +++++++++++++++++++++++++++++++++++++++ cmd/wfctl/repair_test.go | 157 ++++++++++++++++++++ cmd/wfctl/wfctl.yaml | 6 + 4 files changed, 473 insertions(+) create mode 100644 cmd/wfctl/repair.go create mode 100644 cmd/wfctl/repair_test.go diff --git a/cmd/wfctl/main.go b/cmd/wfctl/main.go index 21f5ce72..51928797 100644 --- a/cmd/wfctl/main.go +++ b/cmd/wfctl/main.go @@ -137,6 +137,7 @@ var commands = map[string]func([]string) error{ "registry": runRegistry, "plugin-registry": runPluginRegistry, "update": runUpdate, + "repair": runRepair, "mcp": runMCP, "modernize": runModernize, "expr-migrate": runExprMigrate, diff --git a/cmd/wfctl/repair.go b/cmd/wfctl/repair.go new file mode 100644 index 00000000..f6d56d60 --- /dev/null +++ b/cmd/wfctl/repair.go @@ -0,0 +1,309 @@ +package main + +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/GoCodeAlone/workflow/config" +) + +type repairStatus string + +const ( + repairStatusNoop repairStatus = "noop" + repairStatusPlanned repairStatus = "planned" + repairStatusApplied repairStatus = "applied" + repairStatusManual repairStatus = "manual" +) + +type repairReport struct { + Mode string `json:"mode"` + Status repairStatus `json:"status"` + Doctor doctorStatus `json:"doctorStatus"` + Actions []repairAction `json:"actions"` + Suggestions []repairAction `json:"suggestions,omitempty"` +} + +type repairAction struct { + Kind string `json:"kind"` + Description string `json:"description"` + Command string `json:"command,omitempty"` + Args []string `json:"args,omitempty"` +} + +type repairRunner interface { + PluginLock(args []string) error + PluginInstall(args []string) error +} + +type defaultRepairRunner struct{} + +func (defaultRepairRunner) PluginLock(args []string) error { + return runPluginLock(args) +} + +func (defaultRepairRunner) PluginInstall(args []string) error { + return runPluginInstall(args) +} + +func runRepair(args []string) error { + return runRepairWithOutput(args, os.Stdout) +} + +func runRepairWithOutput(args []string, out io.Writer) error { + return runRepairWithRunner(args, out, defaultRepairRunner{}) +} + +func runRepairWithRunner(args []string, out io.Writer, runner repairRunner) error { + fs := flag.NewFlagSet("repair", flag.ContinueOnError) + fs.SetOutput(out) + workflowPath := fs.String("workflow", "workflow.yaml", "Workflow config path") + manifestPath := fs.String("manifest", wfctlManifestPath, "wfctl plugin manifest path") + lockPath := fs.String("lock-file", wfctlLockPath, "wfctl plugin lockfile path") + pluginDir := fs.String("plugin-dir", defaultDataDir, "Project plugin install directory") + includeGlobal := fs.Bool("include-global", false, "Include global plugin diagnostics as suggestions") + online := fs.Bool("online", false, "Check latest GitHub release for suggestions") + format := fs.String("format", "text", "Output format: text or json") + apply := fs.Bool("apply", false, "Apply automatic repairs") + fs.Usage = func() { + fmt.Fprintf(fs.Output(), `Usage: wfctl repair [options] + +Plan or apply safe wfctl lifecycle repairs. By default this command is a dry-run. +Use --apply to regenerate a stale plugin lockfile and install locked project plugins. + +Options: +`) + fs.PrintDefaults() + } + if err := fs.Parse(args); err != nil { + return err + } + + report := buildRepairReport(repairOptions{ + Doctor: doctorOptions{ + WorkflowPath: *workflowPath, + ManifestPath: *manifestPath, + LockPath: *lockPath, + PluginDir: *pluginDir, + IncludeGlobal: *includeGlobal, + Online: *online, + }, + Apply: *apply, + }) + + if *apply { + if err := applyRepairActions(report.Actions, runner); err != nil { + return err + } + if len(report.Actions) > 0 { + report.Status = repairStatusApplied + } + } + + switch *format { + case "text": + renderRepairText(out, report) + case "json": + enc := json.NewEncoder(out) + enc.SetIndent("", " ") + if err := enc.Encode(report); err != nil { + return fmt.Errorf("encode repair report: %w", err) + } + default: + return fmt.Errorf("--format must be text or json, got %q", *format) + } + return nil +} + +type repairOptions struct { + Doctor doctorOptions + Apply bool +} + +func buildRepairReport(opts repairOptions) repairReport { + doctor := buildDoctorReport(opts.Doctor) + actions, automatedKinds := planAutomaticRepairActions(opts.Doctor) + suggestions := planManualRepairSuggestions(doctor, automatedKinds) + status := repairStatusNoop + if len(actions) > 0 { + status = repairStatusPlanned + } else if len(suggestions) > 0 { + status = repairStatusManual + } + mode := "dry-run" + if opts.Apply { + mode = "apply" + } + return repairReport{ + Mode: mode, + Status: status, + Doctor: doctor.Status, + Actions: actions, + Suggestions: suggestions, + } +} + +func planAutomaticRepairActions(opts doctorOptions) ([]repairAction, map[string]bool) { + automatedKinds := make(map[string]bool) + manifest, err := config.LoadWfctlManifest(opts.ManifestPath) + if err != nil || manifest == nil || len(manifest.Plugins) == 0 { + return nil, automatedKinds + } + + lockNeedsRepair := false + lock, err := config.LoadWfctlLockfile(opts.LockPath) + if errors.Is(err, os.ErrNotExist) { + lockNeedsRepair = true + } else if err != nil { + return nil, automatedKinds + } else if err := config.ValidateWfctlLockfileProvenance(manifest, lock); err != nil { + lockNeedsRepair = true + } else { + for _, pluginEntry := range manifest.Plugins { + if _, ok := findLockedPlugin(lock, pluginEntry.Name); !ok { + lockNeedsRepair = true + break + } + } + } + + installNeedsRepair := lockNeedsRepair + if !installNeedsRepair && lock != nil { + for _, pluginEntry := range manifest.Plugins { + lockEntry, ok := findLockedPlugin(lock, pluginEntry.Name) + if !ok { + installNeedsRepair = true + break + } + if doctorInstalledPluginCheck(opts.PluginDir, pluginEntry.Name, lockEntry.Version).Status != doctorStatusOK { + installNeedsRepair = true + break + } + } + } + + var actions []repairAction + if lockNeedsRepair { + args := []string{"--config", opts.WorkflowPath, "--manifest", opts.ManifestPath, "--lock-file", opts.LockPath} + actions = append(actions, repairAction{ + Kind: "plugin-lock", + Description: "Regenerate the project plugin lockfile from wfctl.yaml", + Command: repairCommandString("plugin lock", args), + Args: args, + }) + automatedKinds["plugin-lock"] = true + } + if installNeedsRepair { + args := []string{"--manifest", opts.ManifestPath, "--lock-file", opts.LockPath, "--plugin-dir", opts.PluginDir} + actions = append(actions, repairAction{ + Kind: "plugin-install", + Description: "Install project plugins from the lockfile", + Command: repairCommandString("plugin install", args), + Args: args, + }) + automatedKinds["plugin-install"] = true + } + return actions, automatedKinds +} + +func planManualRepairSuggestions(report doctorReport, automatedKinds map[string]bool) []repairAction { + var suggestions []repairAction + seen := make(map[string]bool) + for _, section := range report.Sections { + for _, check := range section.Checks { + if check.Fix == "" { + continue + } + if automatedKinds["plugin-lock"] && strings.Contains(check.Fix, "wfctl plugin lock") { + continue + } + if automatedKinds["plugin-install"] && strings.Contains(check.Fix, "wfctl plugin install") { + continue + } + key := check.Message + "\x00" + check.Fix + if seen[key] { + continue + } + seen[key] = true + suggestions = append(suggestions, repairAction{ + Kind: "manual", + Description: check.Message, + Command: check.Fix, + }) + } + } + return suggestions +} + +func applyRepairActions(actions []repairAction, runner repairRunner) error { + for _, action := range actions { + switch action.Kind { + case "plugin-lock": + if err := runner.PluginLock(action.Args); err != nil { + return fmt.Errorf("repair plugin lock: %w", err) + } + case "plugin-install": + if err := runner.PluginInstall(action.Args); err != nil { + return fmt.Errorf("repair plugin install: %w", err) + } + default: + return fmt.Errorf("unsupported automatic repair action %q", action.Kind) + } + } + return nil +} + +func renderRepairText(out io.Writer, report repairReport) { + fmt.Fprintln(out, "wfctl repair") + if report.Mode == "apply" { + fmt.Fprintln(out, "Mode: APPLY") + } else { + fmt.Fprintln(out, "Mode: DRY-RUN") + } + fmt.Fprintf(out, "Doctor: %s\n", report.Doctor) + fmt.Fprintf(out, "Status: %s\n", report.Status) + if len(report.Actions) == 0 { + fmt.Fprintln(out, "\nNo automatic repairs available.") + } else { + fmt.Fprintln(out, "\n[Automatic]") + for _, action := range report.Actions { + prefix := "Would run" + if report.Mode == "apply" && report.Status == repairStatusApplied { + prefix = "Ran" + } + fmt.Fprintf(out, "- %s: %s\n", action.Kind, action.Description) + fmt.Fprintf(out, " %s: %s\n", prefix, action.Command) + } + } + if len(report.Suggestions) > 0 { + fmt.Fprintln(out, "\n[Manual]") + for _, suggestion := range report.Suggestions { + fmt.Fprintf(out, "- %s\n", suggestion.Description) + fmt.Fprintf(out, " Try: %s\n", suggestion.Command) + } + } +} + +func repairCommandString(command string, args []string) string { + parts := append([]string{"wfctl"}, strings.Fields(command)...) + for _, arg := range args { + parts = append(parts, shellQuoteRepairArg(arg)) + } + return strings.Join(parts, " ") +} + +func shellQuoteRepairArg(arg string) string { + if arg == "" { + return "''" + } + if !strings.ContainsAny(arg, " \t\n'\"\\$`!*?[]{}()<>|&;") { + return filepath.ToSlash(arg) + } + return "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'" +} diff --git a/cmd/wfctl/repair_test.go b/cmd/wfctl/repair_test.go new file mode 100644 index 00000000..50c516d3 --- /dev/null +++ b/cmd/wfctl/repair_test.go @@ -0,0 +1,157 @@ +package main + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/GoCodeAlone/workflow/config" +) + +func TestRepairDryRunPlansLockThenInstall(t *testing.T) { + fixture := writeDoctorFixture(t, doctorFixtureOptions{}) + lock, err := config.LoadWfctlLockfile(fixture.lockPath) + if err != nil { + t.Fatalf("load lockfile: %v", err) + } + lock.SourceManifestSHA256 = "stale" + if err := config.SaveWfctlLockfile(fixture.lockPath, lock); err != nil { + t.Fatalf("save stale lockfile: %v", err) + } + + var out bytes.Buffer + if err := runRepairWithOutput(fixture.args(), &out); err != nil { + t.Fatalf("repair dry-run: %v\n%s", err, out.String()) + } + text := out.String() + for _, want := range []string{"wfctl repair", "Mode: DRY-RUN", "wfctl plugin lock", "wfctl plugin install"} { + if !strings.Contains(text, want) { + t.Fatalf("repair output missing %q:\n%s", want, text) + } + } + if strings.Index(text, "wfctl plugin lock") > strings.Index(text, "wfctl plugin install") { + t.Fatalf("expected lock before install:\n%s", text) + } +} + +func TestRepairMissingManifestIsSuggestionOnly(t *testing.T) { + dir := t.TempDir() + workflowPath := filepath.Join(dir, "workflow.yaml") + if err := os.WriteFile(workflowPath, []byte("modules: []\n"), 0o600); err != nil { + t.Fatalf("write workflow: %v", err) + } + + var out bytes.Buffer + err := runRepairWithOutput([]string{ + "--workflow", workflowPath, + "--manifest", filepath.Join(dir, "wfctl.yaml"), + "--lock-file", filepath.Join(dir, ".wfctl-lock.yaml"), + "--plugin-dir", filepath.Join(dir, "data", "plugins"), + }, &out) + if err != nil { + t.Fatalf("repair missing manifest: %v\n%s", err, out.String()) + } + text := out.String() + for _, want := range []string{"No automatic repairs available", "plugin manifest", "wfctl plugin add"} { + if !strings.Contains(text, want) { + t.Fatalf("repair output missing %q:\n%s", want, text) + } + } + if strings.Contains(text, "wfctl plugin lock") || strings.Contains(text, "wfctl plugin install") { + t.Fatalf("missing manifest should not plan plugin mutation:\n%s", text) + } +} + +func TestRepairApplyRunsLockThenInstall(t *testing.T) { + fixture := writeDoctorFixture(t, doctorFixtureOptions{}) + lock, err := config.LoadWfctlLockfile(fixture.lockPath) + if err != nil { + t.Fatalf("load lockfile: %v", err) + } + lock.SourceManifestSHA256 = "stale" + if err := config.SaveWfctlLockfile(fixture.lockPath, lock); err != nil { + t.Fatalf("save stale lockfile: %v", err) + } + + runner := &recordingRepairRunner{} + var out bytes.Buffer + if err := runRepairWithRunner(append(fixture.args(), "--apply"), &out, runner); err != nil { + t.Fatalf("repair apply: %v\n%s", err, out.String()) + } + if got, want := strings.Join(runner.calls, ","), "lock,install"; got != want { + t.Fatalf("calls = %q, want %q", got, want) + } + lockArgs := strings.Join(runner.lockArgs, " ") + for _, want := range []string{"--config " + fixture.workflowPath, "--manifest " + fixture.manifestPath, "--lock-file " + fixture.lockPath} { + if !strings.Contains(lockArgs, want) { + t.Fatalf("lock args missing %q: %v", want, runner.lockArgs) + } + } + installArgs := strings.Join(runner.installArgs, " ") + for _, want := range []string{"--manifest " + fixture.manifestPath, "--lock-file " + fixture.lockPath, "--plugin-dir " + fixture.pluginDir} { + if !strings.Contains(installArgs, want) { + t.Fatalf("install args missing %q: %v", want, runner.installArgs) + } + } + if !strings.Contains(out.String(), "Mode: APPLY") { + t.Fatalf("apply output missing mode:\n%s", out.String()) + } +} + +func TestRepairJSONOutput(t *testing.T) { + fixture := writeDoctorFixture(t, doctorFixtureOptions{}) + var out bytes.Buffer + if err := runRepairWithOutput(append(fixture.args(), "--format", "json"), &out); err != nil { + t.Fatalf("repair json: %v\n%s", err, out.String()) + } + var report struct { + Mode string `json:"mode"` + Status string `json:"status"` + Actions []struct { + Kind string `json:"kind"` + Command string `json:"command,omitempty"` + } `json:"actions"` + } + if err := json.Unmarshal(out.Bytes(), &report); err != nil { + t.Fatalf("repair output is not valid JSON: %v\n%s", err, out.String()) + } + if report.Mode != "dry-run" || report.Status != "planned" { + t.Fatalf("unexpected report: %+v", report) + } + if len(report.Actions) == 0 || report.Actions[0].Command == "" { + t.Fatalf("expected executable repair action: %+v", report) + } +} + +func TestRepairCommandWiring(t *testing.T) { + if commands["repair"] == nil { + t.Fatal("commands map is missing repair") + } + configText := string(wfctlConfigBytes) + for _, want := range []string{"name: repair", "cmd-repair", "command: repair"} { + if !strings.Contains(configText, want) { + t.Fatalf("embedded wfctl config missing %q", want) + } + } +} + +type recordingRepairRunner struct { + calls []string + lockArgs []string + installArgs []string +} + +func (r *recordingRepairRunner) PluginLock(args []string) error { + r.calls = append(r.calls, "lock") + r.lockArgs = append([]string(nil), args...) + return nil +} + +func (r *recordingRepairRunner) PluginInstall(args []string) error { + r.calls = append(r.calls, "install") + r.installArgs = append([]string(nil), args...) + return nil +} diff --git a/cmd/wfctl/wfctl.yaml b/cmd/wfctl/wfctl.yaml index e08206cf..7b088496 100644 --- a/cmd/wfctl/wfctl.yaml +++ b/cmd/wfctl/wfctl.yaml @@ -61,6 +61,8 @@ workflows: description: Plugin catalog registry management - name: update description: Update wfctl + - name: repair + description: Plan or apply safe wfctl lifecycle repairs - name: mcp description: Start the MCP server over stdio - name: modernize @@ -225,6 +227,10 @@ pipelines: trigger: {type: cli, config: {command: update}} steps: - {name: run, type: step.cli_invoke, config: {command: update}} + cmd-repair: + trigger: {type: cli, config: {command: repair}} + steps: + - {name: run, type: step.cli_invoke, config: {command: repair}} cmd-mcp: trigger: {type: cli, config: {command: mcp}} steps: From c8a0375119d7260d7e3d773c1bdb65a1fe9aace1 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 4 Jul 2026 02:00:06 -0400 Subject: [PATCH 4/6] docs(wfctl): document repair lifecycle --- README.md | 1 + docs/WFCTL.md | 42 ++++++++++++++++++++++++++++++++++++++ docs/WFCTL_INSTALLATION.md | 29 ++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/README.md b/README.md index 832992b9..c40f0d95 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ lifecycle instructions. - `wfctl inspect ` — summarize modules, workflows, triggers, and dependencies - `wfctl validate ` — deep validation against known module/step types - `wfctl doctor` — diagnose install, project, lockfile, and plugin lifecycle state +- `wfctl repair` — dry-run or apply safe project plugin lifecycle repairs - `wfctl api extract ` — generate OpenAPI 3.0 spec from HTTP workflows - `wfctl diff ` — compare configs and detect breaking changes - `wfctl manifest ` — produce infrastructure requirements manifest diff --git a/docs/WFCTL.md b/docs/WFCTL.md index 37f19c55..4aa63096 100644 --- a/docs/WFCTL.md +++ b/docs/WFCTL.md @@ -446,6 +446,48 @@ wfctl plugin install wfctl update --check ``` +### `repair` + +Plan or apply safe lifecycle repairs for the local project. The command is +dry-run by default: it prints the ordered commands it would run and leaves files +unchanged. Pass `--apply` to regenerate a stale project plugin lockfile and +install locked project plugins. + +```bash +wfctl repair [options] +``` + +| Flag | Default | Description | +|------|---------|-------------| +| `--workflow` | `workflow.yaml` | Workflow config path used by plugin lock fallback | +| `--manifest` | `wfctl.yaml` | Project plugin manifest | +| `--lock-file` | `.wfctl-lock.yaml` | Generated plugin lockfile | +| `--plugin-dir` | `data/plugins` | Project plugin install directory | +| `--include-global` | `false` | Include global plugin diagnostics as manual suggestions | +| `--online` | `false` | Check the latest GitHub release for manual suggestions | +| `--format` | `text` | `text` or `json` | +| `--apply` | `false` | Run automatic repairs | + +Examples: + +```bash +wfctl repair +wfctl repair --apply +wfctl repair --format json +``` + +Automatic repairs are intentionally narrow: + +- run `wfctl plugin lock` when `wfctl.yaml` and `.wfctl-lock.yaml` are out of + sync or the lockfile is missing a declared plugin; +- run `wfctl plugin install` when locked project plugins are missing or stale, + or after relocking. + +`wfctl repair` does not create workflow configs, create plugin manifests, +self-update the `wfctl` binary, mutate global plugins, or run provider-specific +fixes. Those remain manual commands surfaced by `wfctl doctor` and the repair +report. + ### `audit` Audit Workflow ecosystem metadata without mutating project code. The command is intended for dogfooding release readiness checks: plans and design docs should carry implementation evidence, plugin repos should expose compatible manifests, and repository files should be portable and safe. diff --git a/docs/WFCTL_INSTALLATION.md b/docs/WFCTL_INSTALLATION.md index 23ea63ef..c67c4684 100644 --- a/docs/WFCTL_INSTALLATION.md +++ b/docs/WFCTL_INSTALLATION.md @@ -167,6 +167,35 @@ verifies `checksums.txt` when available, and replaces the current executable. It is not recommended for Homebrew-managed binaries because it bypasses Homebrew's cellar and formula state. +## Diagnose And Repair Local State + +Use `wfctl doctor` to inspect the binary, workflow config, plugin manifest, +lockfile, and installed project plugins without changing files: + +```bash +wfctl doctor +wfctl doctor --online +``` + +Use `wfctl repair` to turn common project plugin diagnostics into an ordered +repair plan. The default is a dry-run: + +```bash +wfctl repair +``` + +Apply the automatic repairs only when the plan looks correct: + +```bash +wfctl repair --apply +``` + +Automatic repair is limited to regenerating `.wfctl-lock.yaml` from +`wfctl.yaml` and installing locked project plugins into `data/plugins` or the +directory passed with `--plugin-dir`. Binary updates, missing project files, +global plugin changes, and provider-specific fixes remain explicit manual +commands. + ## Project Plugin Lifecycle Workflow projects should commit a human-edited `wfctl.yaml` manifest and the From 4a06f4e92ef7d3ed5427771d45634f744b03a3b3 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 4 Jul 2026 02:03:06 -0400 Subject: [PATCH 5/6] chore: complete wfctl repair scope lock --- docs/plans/2026-07-04-wfctl-repair.md | 2 +- docs/plans/2026-07-04-wfctl-repair.md.scope-lock | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 docs/plans/2026-07-04-wfctl-repair.md.scope-lock diff --git a/docs/plans/2026-07-04-wfctl-repair.md b/docs/plans/2026-07-04-wfctl-repair.md index 4270f0c4..dd5d4ade 100644 --- a/docs/plans/2026-07-04-wfctl-repair.md +++ b/docs/plans/2026-07-04-wfctl-repair.md @@ -30,7 +30,7 @@ |------|-------|-------|--------| | 1 | Add guarded wfctl repair | Task 1, Task 2, Task 3, Task 4 | `feat/wfctl-repair-lifecycle` | -**Status:** Locked 2026-07-04T05:55:40Z +**Status:** Complete 2026-07-04T06:02:53Z ### Task 1: Repair Planner Tests diff --git a/docs/plans/2026-07-04-wfctl-repair.md.scope-lock b/docs/plans/2026-07-04-wfctl-repair.md.scope-lock deleted file mode 100644 index e5d4eb0e..00000000 --- a/docs/plans/2026-07-04-wfctl-repair.md.scope-lock +++ /dev/null @@ -1 +0,0 @@ -2a22706c2ab6769b484ea609ed49872a2ef195ff6166e2ddc9f1a27e1f07104b From 8e41f0ce90f870d05ac40a90f2360331c45964e6 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sat, 4 Jul 2026 02:17:17 -0400 Subject: [PATCH 6/6] fix(wfctl): repair corrupt lockfiles --- cmd/wfctl/repair.go | 4 ++-- cmd/wfctl/repair_test.go | 21 +++++++++++++++++++++ docs/WFCTL.md | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/cmd/wfctl/repair.go b/cmd/wfctl/repair.go index f6d56d60..81985557 100644 --- a/cmd/wfctl/repair.go +++ b/cmd/wfctl/repair.go @@ -63,7 +63,7 @@ func runRepairWithOutput(args []string, out io.Writer) error { func runRepairWithRunner(args []string, out io.Writer, runner repairRunner) error { fs := flag.NewFlagSet("repair", flag.ContinueOnError) fs.SetOutput(out) - workflowPath := fs.String("workflow", "workflow.yaml", "Workflow config path") + workflowPath := fs.String("workflow", "workflow.yaml", "Workflow config path passed to plugin lock") manifestPath := fs.String("manifest", wfctlManifestPath, "wfctl plugin manifest path") lockPath := fs.String("lock-file", wfctlLockPath, "wfctl plugin lockfile path") pluginDir := fs.String("plugin-dir", defaultDataDir, "Project plugin install directory") @@ -161,7 +161,7 @@ func planAutomaticRepairActions(opts doctorOptions) ([]repairAction, map[string] if errors.Is(err, os.ErrNotExist) { lockNeedsRepair = true } else if err != nil { - return nil, automatedKinds + lockNeedsRepair = true } else if err := config.ValidateWfctlLockfileProvenance(manifest, lock); err != nil { lockNeedsRepair = true } else { diff --git a/cmd/wfctl/repair_test.go b/cmd/wfctl/repair_test.go index 50c516d3..ff6c0ba1 100644 --- a/cmd/wfctl/repair_test.go +++ b/cmd/wfctl/repair_test.go @@ -65,6 +65,27 @@ func TestRepairMissingManifestIsSuggestionOnly(t *testing.T) { } } +func TestRepairCorruptLockfilePlansLockThenInstall(t *testing.T) { + fixture := writeDoctorFixture(t, doctorFixtureOptions{}) + if err := os.WriteFile(fixture.lockPath, []byte("plugins:\n - bad\n"), 0o600); err != nil { + t.Fatalf("write corrupt lockfile: %v", err) + } + + var out bytes.Buffer + if err := runRepairWithOutput(fixture.args(), &out); err != nil { + t.Fatalf("repair corrupt lockfile: %v\n%s", err, out.String()) + } + text := out.String() + for _, want := range []string{"wfctl plugin lock", "wfctl plugin install"} { + if !strings.Contains(text, want) { + t.Fatalf("repair output missing %q:\n%s", want, text) + } + } + if strings.Contains(text, "No automatic repairs available") { + t.Fatalf("corrupt lockfile should be automatically repairable:\n%s", text) + } +} + func TestRepairApplyRunsLockThenInstall(t *testing.T) { fixture := writeDoctorFixture(t, doctorFixtureOptions{}) lock, err := config.LoadWfctlLockfile(fixture.lockPath) diff --git a/docs/WFCTL.md b/docs/WFCTL.md index 4aa63096..e76b4043 100644 --- a/docs/WFCTL.md +++ b/docs/WFCTL.md @@ -459,7 +459,7 @@ wfctl repair [options] | Flag | Default | Description | |------|---------|-------------| -| `--workflow` | `workflow.yaml` | Workflow config path used by plugin lock fallback | +| `--workflow` | `workflow.yaml` | Workflow config path passed to `plugin lock` | | `--manifest` | `wfctl.yaml` | Project plugin manifest | | `--lock-file` | `.wfctl-lock.yaml` | Generated plugin lockfile | | `--plugin-dir` | `data/plugins` | Project plugin install directory |