devrites 4.1.0 → 4.3.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.
Files changed (133) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -1
  3. package/docs/engine/commands.md +69 -6
  4. package/docs/orchestration.md +6 -3
  5. package/engine/go.mod +1 -1
  6. package/engine/internal/gate/gate.go +154 -46
  7. package/engine/internal/gate/gate_test.go +290 -2
  8. package/engine/internal/gate/readiness_binding.go +66 -100
  9. package/engine/internal/gate/readiness_binding_test.go +126 -3
  10. package/engine/internal/lib/cli_observe.go +66 -0
  11. package/engine/internal/lib/observe_summary.go +79 -0
  12. package/engine/internal/lib/resolve.go +6 -6
  13. package/engine/internal/lib/skilltrust.go +196 -0
  14. package/engine/internal/lib/taskgraph.go +162 -0
  15. package/engine/internal/lib/taskgraph_test.go +150 -0
  16. package/engine/internal/lib/workspace_read.go +29 -0
  17. package/engine/internal/parallel/cli.go +467 -0
  18. package/engine/internal/parallel/git.go +181 -0
  19. package/engine/internal/parallel/lease.go +244 -0
  20. package/engine/internal/parallel/ops.go +416 -0
  21. package/engine/internal/parallel/parallel_test.go +242 -0
  22. package/engine/internal/parallel/pathdisjoint.go +129 -0
  23. package/engine/internal/parallel/pathdisjoint_test.go +97 -0
  24. package/engine/internal/state/cmd/workflowmanifest/main.go +32 -3
  25. package/engine/internal/state/feature.go +1 -100
  26. package/engine/internal/state/observation.go +584 -0
  27. package/engine/internal/state/observation_open_other.go +9 -0
  28. package/engine/internal/state/observation_open_unix.go +12 -0
  29. package/engine/internal/state/observation_test.go +960 -0
  30. package/engine/internal/state/observation_unix_test.go +68 -0
  31. package/engine/internal/state/schema.go +60 -160
  32. package/engine/internal/state/state_test.go +489 -101
  33. package/engine/internal/state/status.go +97 -53
  34. package/engine/main.go +72 -3
  35. package/engine/root_routing_test.go +1 -1
  36. package/engine/tests/adr_0004_required_by_phase_test.go +16 -13
  37. package/engine/tests/adr_0011_define_plan_test.go +14 -12
  38. package/engine/tests/gate_test.go +403 -28
  39. package/engine/tests/workspace_observation_migration_test.go +1065 -0
  40. package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +1 -10
  41. package/pack/.claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
  42. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
  43. package/pack/.claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
  44. package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  45. package/pack/.claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
  46. package/pack/.claude/skills/rite-autocomplete/SKILL.md +98 -146
  47. package/pack/.claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
  48. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +96 -134
  49. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
  50. package/pack/.claude/skills/rite-build/SKILL.md +46 -81
  51. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +19 -25
  52. package/pack/.claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  53. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
  54. package/pack/.claude/skills/rite-build/reference/output.md +2 -0
  55. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +64 -0
  56. package/pack/.claude/skills/rite-build/reference/phase-contract.md +9 -22
  57. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +7 -3
  58. package/pack/.claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
  59. package/pack/.claude/skills/rite-customize/SKILL.md +3 -2
  60. package/pack/.claude/skills/rite-doctor/SKILL.md +5 -1
  61. package/pack/.claude/skills/rite-plan/SKILL.md +17 -17
  62. package/pack/.claude/skills/rite-plan/reference/anti-patterns.md +11 -1
  63. package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +3 -0
  64. package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
  65. package/pack/.claude/skills/rite-prove/SKILL.md +95 -153
  66. package/pack/.claude/skills/rite-vet/SKILL.md +121 -174
  67. package/pack/.claude/skills/rite-vet/reference/anti-patterns.md +14 -2
  68. package/pack/.claude/skills/rite-vet/reference/artifacts.md +10 -6
  69. package/pack/.claude/skills/rite-vet/reference/depth.md +10 -3
  70. package/pack/.claude/skills/rite-vet/reference/review-axes.md +12 -4
  71. package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +1 -10
  72. package/pack/generated/claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
  73. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
  74. package/pack/generated/claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
  75. package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  76. package/pack/generated/claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
  77. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +98 -146
  78. package/pack/generated/claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
  79. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +96 -134
  80. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
  81. package/pack/generated/claude/skills/rite-build/SKILL.md +46 -81
  82. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +19 -25
  83. package/pack/generated/claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  84. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
  85. package/pack/generated/claude/skills/rite-build/reference/output.md +2 -0
  86. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +64 -0
  87. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +9 -22
  88. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +7 -3
  89. package/pack/generated/claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
  90. package/pack/generated/claude/skills/rite-customize/SKILL.md +3 -2
  91. package/pack/generated/claude/skills/rite-doctor/SKILL.md +5 -1
  92. package/pack/generated/claude/skills/rite-plan/SKILL.md +17 -17
  93. package/pack/generated/claude/skills/rite-plan/reference/anti-patterns.md +11 -1
  94. package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +3 -0
  95. package/pack/generated/claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
  96. package/pack/generated/claude/skills/rite-prove/SKILL.md +95 -153
  97. package/pack/generated/claude/skills/rite-vet/SKILL.md +121 -174
  98. package/pack/generated/claude/skills/rite-vet/reference/anti-patterns.md +14 -2
  99. package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +10 -6
  100. package/pack/generated/claude/skills/rite-vet/reference/depth.md +10 -3
  101. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +12 -4
  102. package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +1 -10
  103. package/pack/generated/codex/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
  104. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
  105. package/pack/generated/codex/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
  106. package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  107. package/pack/generated/codex/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
  108. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +98 -146
  109. package/pack/generated/codex/skills/rite-autocomplete/reference/decision-policy.md +11 -5
  110. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +96 -134
  111. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
  112. package/pack/generated/codex/skills/rite-build/SKILL.md +46 -81
  113. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +19 -25
  114. package/pack/generated/codex/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  115. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +8 -3
  116. package/pack/generated/codex/skills/rite-build/reference/output.md +2 -0
  117. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +64 -0
  118. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +9 -22
  119. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +7 -3
  120. package/pack/generated/codex/skills/rite-clarify/reference/anti-patterns.md +24 -0
  121. package/pack/generated/codex/skills/rite-customize/SKILL.md +3 -2
  122. package/pack/generated/codex/skills/rite-doctor/SKILL.md +5 -1
  123. package/pack/generated/codex/skills/rite-plan/SKILL.md +17 -17
  124. package/pack/generated/codex/skills/rite-plan/reference/anti-patterns.md +11 -1
  125. package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +3 -0
  126. package/pack/generated/codex/skills/rite-plan/reference/replan-and-repair.md +10 -3
  127. package/pack/generated/codex/skills/rite-prove/SKILL.md +95 -153
  128. package/pack/generated/codex/skills/rite-vet/SKILL.md +121 -174
  129. package/pack/generated/codex/skills/rite-vet/reference/anti-patterns.md +14 -2
  130. package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +10 -6
  131. package/pack/generated/codex/skills/rite-vet/reference/depth.md +10 -3
  132. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +12 -4
  133. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
4
4
 
5
+ ## [4.3.0](https://github.com/ViktorsBaikers/DevRites/compare/v4.2.0...v4.3.0) (2026-08-25)
6
+
7
+ ### Added
8
+
9
+ * **devrites:** add skill-trust, task-graph, and observe commands ([#38](https://github.com/ViktorsBaikers/DevRites/issues/38)) ([2562355](https://github.com/ViktorsBaikers/DevRites/commit/2562355a970726fb95e57e2f328961ca136c9f81))
10
+ * **rite-build:** opt-in parallel worktree slices (N≤3) ([#37](https://github.com/ViktorsBaikers/DevRites/issues/37)) ([fe71d67](https://github.com/ViktorsBaikers/DevRites/commit/fe71d672408559af030e95f700b18ead798f18fc))
11
+
12
+ ## [4.2.0](https://github.com/ViktorsBaikers/DevRites/compare/v4.1.0...v4.2.0) (2026-08-23)
13
+
14
+ ### Added
15
+
16
+ * **rite:** ship workflow artifact, reslice, and observation ([#36](https://github.com/ViktorsBaikers/DevRites/issues/36)) ([456a00e](https://github.com/ViktorsBaikers/DevRites/commit/456a00e8280acdab9525a762a267be7084b26a9f))
17
+
5
18
  ## [4.1.0](https://github.com/ViktorsBaikers/DevRites/compare/v4.0.12...v4.1.0) (2026-08-12)
6
19
 
7
20
  ### Added
package/README.md CHANGED
@@ -28,7 +28,7 @@ project-conventional push, tag, or PR action, and archive the workspace.
28
28
  Unattended runs may create local WIP checkpoint commits along the way, but they
29
29
  remain local unless Ship's disclosed plan includes an approved remote action.
30
30
 
31
- **Status:** [`v4.1.0`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v4.1.0): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
31
+ **Status:** [`v4.3.0`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v4.3.0): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
32
32
 
33
33
  This is the latest published release; `main` may contain unreleased work.
34
34
 
@@ -12,9 +12,13 @@ workflow strategy.
12
12
  | `update [flags]` | Refresh an existing managed installation. |
13
13
  | `uninstall [flags]` | Remove managed artifacts while preserving runtime workspace state. |
14
14
  | `check candidate <slug>` | Validate the strict manifest and compute the content-bound project-candidate identity. |
15
- | `check readiness <slug>` | Check phase files, human gates, and the current stable Build-input binding when applicable. |
15
+ | `check readiness <slug>` | Check target-Phase files, open human gates from Clarify onward, and the current stable Build-input binding when applicable. |
16
16
  | `check readiness --emit-binding <slug>` | Render the exact stable Build-input binding for Vet to record after review. |
17
- | `check seal <slug>` | Check final files, human gates, the stable Build-input binding, and exact candidate bindings. |
17
+ | `check seal <slug>` | Check files required by target Phase `seal`, open human gates, the stable Build-input binding, and exact candidate bindings. |
18
+ | `check path-disjoint [--root <dir>] [<json-file>|-]` | Verify slice path sets are pairwise disjoint. |
19
+ | `check task-graph <slug>` | Validate `tasks.md` slice dependency graph for cycles and unknown dependencies. |
20
+ | `check skill-trust <path>` | Scan one skill/agent Markdown file for structural trust violations. |
21
+ | `observe summary <slug>` | Emit sanitized JSON workspace summary from one retained observation. |
18
22
  | `state resolve <qid> "<answer>"` | Resolve an open question and update `questions.md` plus `state.md` atomically. |
19
23
  | `state close <slug>` | Archive a shipped workspace and clear matching `ACTIVE`. |
20
24
  | `secret-scan [--staged] [--stdin] [slug]` | Scan exact staged blobs, stdin, or touched regular files for credential material. |
@@ -29,10 +33,12 @@ not to the engine command namespace.
29
33
  ## Check boundary
30
34
 
31
35
  The candidate gate validates and hashes path/state/type/mode/content identity;
32
- it does not infer scope from Git. The readiness gate checks structure plus the
33
- exact stable Build-input binding after Vet. The seal gate repeats that binding
34
- and checks exact candidate bindings in evidence, optional browser evidence,
35
- review, and seal. None judges the meaning of `CLEAR`/`READY` prose,
36
+ it does not infer scope from Git. The readiness gate checks target-Phase
37
+ structure and applies open-question blocking only when that target is Clarify
38
+ or later, plus the exact stable Build-input binding after Vet. The seal gate
39
+ always targets Phase `seal`, repeats that binding, and checks exact candidate
40
+ bindings in evidence, optional browser evidence, review, and seal. None judges
41
+ the meaning of `CLEAR`/`READY` prose,
36
42
  parses reviewer narratives, infers acceptance coverage, counts assertions,
37
43
  interprets capability deltas, or decides whether a technical plan is sound.
38
44
 
@@ -104,6 +110,63 @@ ambient Git state. Ordinary readiness and Seal require that exact standalone
104
110
  line in `eng-review.md`; stale input returns
105
111
  `reason: DRV-GATE-READINESS-STALE` and routes through `/rite-vet`.
106
112
 
113
+ ### Workspace observation diagnostics
114
+
115
+ Lifecycle checks acquire the fixed workspace Markdown inventory once. Each
116
+ artifact is classified as `absent`, `empty`, `malformed`, `unsafe`,
117
+ `unreadable`, or `present`. Retained content is limited to 1 MiB per file and
118
+ 8 MiB aggregate. Diagnostic lines use this exact shape:
119
+ `artifact: <logical-path>: <state> (<code>)`.
120
+
121
+ The closed diagnostic codes and recoveries are:
122
+
123
+ | Code | Exact Gate recovery | Exact standalone readiness-binding payload |
124
+ |---|---|---|
125
+ | `malformed_markdown` | `next: repair <logical-path>: replace invalid Markdown with valid Markdown; required artifacts need substantive content` | `readiness input <logical-path> is malformed (malformed_markdown); replace invalid Markdown with valid Markdown` |
126
+ | `parent_symlink` | `next: repair <logical-path>: replace the symlinked parent with a real directory` | `readiness input <logical-path> is unsafe (parent_symlink); replace the symlinked parent with a real directory` |
127
+ | `final_symlink` | `next: repair <logical-path>: replace the symlink with a regular file` | `readiness input <logical-path> is unsafe (final_symlink); replace the symlink with a regular file` |
128
+ | `non_regular` | `next: repair <logical-path>: replace the non-regular entry with a regular file` | `readiness input <logical-path> is unsafe (non_regular); replace the non-regular entry with a regular file` |
129
+ | `file_too_large` | `next: repair <logical-path>: reduce the file to at most 1 MiB` | `readiness input <logical-path> is unsafe (file_too_large); reduce the file to at most 1 MiB` |
130
+ | `permission_denied` | `next: repair <logical-path>: grant read permission` | `readiness input <logical-path> is unreadable (permission_denied); grant read permission` |
131
+ | `read_failure` | `next: repair <logical-path>: restore a readable regular file` | `readiness input <logical-path> is unreadable (read_failure); restore a readable regular file` |
132
+
133
+ The Gate recovery column remains exact for target-policy-required artifacts. For
134
+ a selected optional readiness input, the same code-specific repair appends
135
+ `; optional readiness input may instead be removed` and does not call the input
136
+ required.
137
+
138
+ These seven codes are the closed Workspace Observation classification and
139
+ recovery mapping outcomes. A selected public consumer emits only a code
140
+ reachable for its consumed fixed logical path. Invalid workspace ancestry is
141
+ `workspace_invalid`, not an artifact `parent_symlink` diagnostic.
142
+
143
+ Status emits diagnostics without recovery or `next:` lines, after section rows
144
+ and before `result`. Gate emits diagnostics after `reason` and before recovery,
145
+ `invariant`, and `retry` lines. Generic add-content recovery applies only to
146
+ absent or empty target-required artifacts. Standalone readiness-binding
147
+ failures use the existing `readiness-binding: BLOCKED:` prefix and the logical
148
+ readiness-input state/code plus recovery; they never disclose physical paths or
149
+ content.
150
+
151
+ Whole observation failures are `workspace_invalid`, `aggregate_too_large`, and
152
+ `concurrent_change`. Their disclosure-safe payloads are exact:
153
+
154
+ - `workspace observation: workspace_invalid: workspace is unavailable; verify the selected logical workspace and canonical workspace override, then retry`
155
+ - `workspace observation: aggregate_too_large: retained content exceeds the 8 MiB aggregate limit; reduce retained Markdown below 8 MiB, then retry`
156
+ - `workspace observation: concurrent_change: workspace changed during acquisition; retry`
157
+
158
+ An absent or empty `state.md` appends `add real content to state.md and retry` to
159
+ the existing logical error. A malformed, unsafe, or unreadable `state.md`
160
+ appends `repair state.md and retry`. A ledger without a phase appends `record
161
+ phase in state.md and retry`; an unknown phase appends `record a known phase in
162
+ state.md and retry`.
163
+
164
+ Whole observation failures use stderr, exit `2`, and no lifecycle result or
165
+ reason on stdout. Standalone readiness-binding failures use one stderr line,
166
+ exit `3`, and empty stdout. Per-artifact lifecycle blocks keep existing reason
167
+ IDs and stdout exit `3`; successful checks keep stdout exit `0`. Seal evidence
168
+ freshness still runs separately after a successful Seal gate.
169
+
107
170
  - `0`: passed or completed.
108
171
  - `2`: common invalid request or unreadable-state result.
109
172
  - `3`: common deterministic lifecycle or safety block.
@@ -126,9 +126,12 @@ base before native transfer, then re-proves the reconciled candidate. Missing tr
126
126
  conflict, extra paths, moved base, or cleanup failure stops with worktree evidence
127
127
  preserved. No ad hoc copy/cherry-pick/merge occurs from the read-only root.
128
128
 
129
- Isolation does not authorize throughput. At most one writer runs across all linked
130
- worktrees for the workspace; parallel writers remain forbidden until serial transfer,
131
- conflict, proof, and review measurements justify a separate design.
129
+ Isolation does not authorize same-worktree throughput. Parallel writers are allowed
130
+ **only** under `/rite-build --parallel N` when path-disjoint eligibility, abort-batch,
131
+ and a control `parallel-lease.md` apply (see
132
+ [`parallel-batch.md`](../pack/.claude/skills/rite-build/reference/parallel-batch.md)).
133
+ Same-worktree multi-writer and root-emulated worktrees remain forbidden; default
134
+ `/rite-build` stays one writer across linked worktrees.
132
135
 
133
136
  ## Engine boundary
134
137
 
package/engine/go.mod CHANGED
@@ -2,4 +2,4 @@ module github.com/devrites/devrites
2
2
 
3
3
  go 1.26
4
4
 
5
- toolchain go1.26.5
5
+ toolchain go1.26.7
@@ -6,11 +6,8 @@ package gate
6
6
 
7
7
  import (
8
8
  "fmt"
9
- "os"
10
- "path/filepath"
11
9
  "strings"
12
10
 
13
- "github.com/devrites/devrites/internal/devritespaths"
14
11
  "github.com/devrites/devrites/internal/reason"
15
12
  "github.com/devrites/devrites/internal/state"
16
13
  )
@@ -27,50 +24,82 @@ const (
27
24
  Seal Kind = "seal"
28
25
  )
29
26
 
30
- // Result is a gate outcome. Blocked is true iff a required section is missing;
31
- // Missing lists them in canonical order for an actionable message.
27
+ // Result is a gate outcome. Blocked is true iff a deterministic requirement
28
+ // failed; missing files and diagnostics retain their canonical policy order.
32
29
  type Result struct {
33
- Kind Kind
34
- Slug string
35
- Phase state.Phase
36
- Target state.Phase // the phase whose requirements were checked
37
- Missing []state.Section // compact legacy view
38
- MissingFiles []string // authoritative per-file view
39
- StateProblems []string // deterministic cross-file invariant failures
40
- Blocked bool
41
- ReasonID reason.ID
30
+ Kind Kind
31
+ Slug string
32
+ Phase state.Phase
33
+ Target state.Phase
34
+ Missing []state.Section
35
+ MissingFiles []string
36
+ Diagnostics []state.ArtifactDiagnostic
37
+ AddContentFiles []string
38
+ StateProblems []string
39
+ Blocked bool
40
+ ReasonID reason.ID
42
41
  }
43
42
 
44
- // Check runs a gate against feature <slug> under root. It reads workspace files
45
- // directly instead of trusting a cache. Invalid requests and unreadable state
46
- // return errors; missing required content returns a blocked Result.
43
+ // Check runs a gate against feature <slug> under root. One workspace observation
44
+ // supplies every lifecycle, question, and readiness fact used by the result.
47
45
  func Check(kind Kind, root, slug string) (*Result, error) {
48
- f, err := state.LoadFeature(root, slug)
46
+ observation, err := state.ObserveWorkspace(root, slug)
49
47
  if err != nil {
50
48
  return nil, fmt.Errorf("gate %s: %w", kind, err)
51
49
  }
52
- target := f.Phase
50
+ result, err := checkObservation(kind, observation)
51
+ if err != nil {
52
+ return nil, fmt.Errorf("gate %s: %w", kind, err)
53
+ }
54
+ return result, nil
55
+ }
56
+
57
+ func checkObservation(kind Kind, observation *state.WorkspaceObservation) (*Result, error) {
58
+ phase, err := observation.DeclaredPhase()
59
+ if err != nil {
60
+ return nil, err
61
+ }
62
+ target := phase
53
63
  if kind == Seal {
54
64
  target = state.PhaseSeal
55
65
  }
56
- missing := state.MissingFor(f, target)
57
- missingFiles := state.MissingWorkspaceFiles(f, target)
66
+ policy, ok := state.PolicyFor(target)
67
+ if !ok {
68
+ return nil, fmt.Errorf("unknown target phase %q", target)
69
+ }
70
+ target = policy.Target
71
+
72
+ missing := missingSections(observation, policy.RequiredSections)
73
+ missingArtifacts, diagnostics := observation.Missing(policy.RequiredArtifacts)
74
+ missingFiles := make([]string, len(missingArtifacts))
75
+ var addContentFiles []string
76
+ for i, artifact := range missingArtifacts {
77
+ missingFiles[i] = string(artifact)
78
+ fact, ok := observation.Fact(artifact)
79
+ if ok && (fact.State() == state.ArtifactAbsent || fact.State() == state.ArtifactEmpty) {
80
+ addContentFiles = append(addContentFiles, string(artifact))
81
+ }
82
+ }
58
83
  blocked := len(missingFiles) > 0
59
84
  readinessStale := false
60
85
  var stateProblems []string
61
- if gates, awaitingHuman := openHumanGates(devritespaths.FeatureDir(root, slug)); len(gates) > 0 {
62
- problem := fmt.Sprintf("open %s human question(s) remain in questions.md", strings.Join(gates, "/"))
63
- if !awaitingHuman {
64
- problem += " but state.md is not awaiting_human"
86
+
87
+ if policy.BlocksOpenQuestions {
88
+ if gates, awaitingHuman := retainedHumanGates(observation); len(gates) > 0 {
89
+ problem := fmt.Sprintf("open %s human question(s) remain in questions.md", strings.Join(gates, "/"))
90
+ if !awaitingHuman {
91
+ problem += " but state.md is not awaiting_human"
92
+ }
93
+ stateProblems = append(stateProblems, problem)
94
+ blocked = true
65
95
  }
66
- stateProblems = append(stateProblems, problem)
67
- blocked = true
68
96
  }
69
- if len(missingFiles) == 0 && phaseRequiresReadinessBinding(target) {
70
- expected, bindingErr := verifyReadinessBinding(root, slug)
97
+ if len(missingFiles) == 0 && phaseRequiresReadinessBinding(policy) {
98
+ expected, bindingErr := verifyReadinessBinding(observation)
71
99
  if bindingErr != nil {
72
100
  readinessStale = true
73
101
  blocked = true
102
+ diagnostics = append(diagnostics, readinessDiagnostics(observation)...)
74
103
  if expected == "" {
75
104
  stateProblems = append(stateProblems, bindingErr.Error()+"; repair the input and rerun /rite-vet")
76
105
  } else {
@@ -78,15 +107,18 @@ func Check(kind Kind, root, slug string) (*Result, error) {
78
107
  }
79
108
  }
80
109
  }
110
+
81
111
  result := &Result{
82
- Kind: kind,
83
- Slug: slug,
84
- Phase: f.Phase,
85
- Target: target,
86
- Missing: missing,
87
- MissingFiles: missingFiles,
88
- StateProblems: stateProblems,
89
- Blocked: blocked,
112
+ Kind: kind,
113
+ Slug: observation.Slug(),
114
+ Phase: phase,
115
+ Target: target,
116
+ Missing: missing,
117
+ MissingFiles: missingFiles,
118
+ Diagnostics: diagnostics,
119
+ AddContentFiles: addContentFiles,
120
+ StateProblems: stateProblems,
121
+ Blocked: blocked,
90
122
  }
91
123
  result.ReasonID = ResultReasonID(kind, blocked)
92
124
  if readinessStale {
@@ -95,6 +127,24 @@ func Check(kind Kind, root, slug string) (*Result, error) {
95
127
  return result, nil
96
128
  }
97
129
 
130
+ func missingSections(observation *state.WorkspaceObservation, required []state.Section) []state.Section {
131
+ var missing []state.Section
132
+ for _, section := range required {
133
+ artifact := state.ArtifactPath(string(section) + ".md")
134
+ switch section {
135
+ case state.SectionProof:
136
+ artifact = state.EvidenceFile
137
+ case state.SectionStatus:
138
+ artifact = state.LedgerFile
139
+ }
140
+ fact, ok := observation.Fact(artifact)
141
+ if !ok || fact.State() != state.ArtifactPresent {
142
+ missing = append(missing, section)
143
+ }
144
+ }
145
+ return missing
146
+ }
147
+
98
148
  // ResultReasonID returns the typed outcome owned by the lifecycle gate.
99
149
  func ResultReasonID(kind Kind, blocked bool) reason.ID {
100
150
  switch kind {
@@ -114,7 +164,7 @@ func ResultReasonID(kind Kind, blocked bool) reason.ID {
114
164
  }
115
165
 
116
166
  // Render returns stable, greppable output with a trailing newline. A blocked
117
- // result names the missing files and the command to rerun.
167
+ // result names missing files, selected diagnostics, recovery, and the retry.
118
168
  func (r *Result) Render() string {
119
169
  var b strings.Builder
120
170
  fmt.Fprintf(&b, "gate: %s\n", r.Kind)
@@ -135,8 +185,17 @@ func (r *Result) Render() string {
135
185
  fmt.Fprintf(&b, "result: blocked (missing to leave %q: %s)\n", r.Phase, strings.Join(missing, ", "))
136
186
  }
137
187
  fmt.Fprintf(&b, "reason: %s\n", r.ReasonID)
138
- if len(missing) > 0 {
139
- fmt.Fprintf(&b, "next: add real content to %s\n", strings.Join(missing, ", "))
188
+ for _, diagnostic := range r.Diagnostics {
189
+ fmt.Fprintf(&b, "artifact: %s: %s (%s)\n", diagnostic.Path, diagnostic.State, diagnostic.Code)
190
+ }
191
+ if len(r.AddContentFiles) > 0 {
192
+ fmt.Fprintf(&b, "next: add real content to %s\n", strings.Join(r.AddContentFiles, ", "))
193
+ }
194
+ for _, diagnostic := range r.Diagnostics {
195
+ if recovery := diagnosticRecovery(diagnostic, targetRequiresArtifact(r.Target, diagnostic.Path)); recovery != "" {
196
+ b.WriteString(recovery)
197
+ b.WriteByte('\n')
198
+ }
140
199
  }
141
200
  for _, problem := range r.StateProblems {
142
201
  fmt.Fprintf(&b, "invariant: %s\n", problem)
@@ -145,19 +204,68 @@ func (r *Result) Render() string {
145
204
  return b.String()
146
205
  }
147
206
 
207
+ func diagnosticRecovery(diagnostic state.ArtifactDiagnostic, required bool) string {
208
+ prefix := fmt.Sprintf("next: repair %s: ", diagnostic.Path)
209
+ repair := diagnosticRepair(diagnostic.Code)
210
+ if repair == "" {
211
+ return ""
212
+ }
213
+ if required && diagnostic.Code == state.DiagnosticMalformedMarkdown {
214
+ repair += "; required artifacts need substantive content"
215
+ }
216
+ if !required {
217
+ repair += "; optional readiness input may instead be removed"
218
+ }
219
+ return prefix + repair
220
+ }
221
+
222
+ func targetRequiresArtifact(target state.Phase, path state.ArtifactPath) bool {
223
+ policy, ok := state.PolicyFor(target)
224
+ if !ok {
225
+ return false
226
+ }
227
+ for _, required := range policy.RequiredArtifacts {
228
+ if required == path {
229
+ return true
230
+ }
231
+ }
232
+ return false
233
+ }
234
+
235
+ func diagnosticRepair(code state.DiagnosticCode) string {
236
+ switch code {
237
+ case state.DiagnosticMalformedMarkdown:
238
+ return "replace invalid Markdown with valid Markdown"
239
+ case state.DiagnosticParentSymlink:
240
+ return "replace the symlinked parent with a real directory"
241
+ case state.DiagnosticFinalSymlink:
242
+ return "replace the symlink with a regular file"
243
+ case state.DiagnosticNonRegular:
244
+ return "replace the non-regular entry with a regular file"
245
+ case state.DiagnosticFileTooLarge:
246
+ return "reduce the file to at most 1 MiB"
247
+ case state.DiagnosticPermissionDenied:
248
+ return "grant read permission"
249
+ case state.DiagnosticReadFailure:
250
+ return "restore a readable regular file"
251
+ default:
252
+ return ""
253
+ }
254
+ }
255
+
148
256
  const gateSpaceChars = " \t\n\v\f\r"
149
257
 
150
- func openHumanGates(featureDir string) ([]string, bool) {
151
- qdata, err := os.ReadFile(filepath.Join(featureDir, "questions.md"))
152
- if err != nil {
258
+ func retainedHumanGates(observation *state.WorkspaceObservation) ([]string, bool) {
259
+ questions, ok := observation.Fact("questions.md")
260
+ if !ok || (questions.State() != state.ArtifactPresent && questions.State() != state.ArtifactEmpty) {
153
261
  return nil, false
154
262
  }
155
- gates := openBlockingQuestionGates(qdata)
263
+ gates := openBlockingQuestionGates(questions.Bytes())
156
264
  if len(gates) == 0 {
157
265
  return nil, false
158
266
  }
159
- sdata, err := os.ReadFile(filepath.Join(featureDir, "state.md"))
160
- return gates, err == nil && stateAwaitingHuman(sdata)
267
+ ledger, ok := observation.Fact(state.LedgerFile)
268
+ return gates, ok && ledger.State() == state.ArtifactPresent && stateAwaitingHuman(ledger.Bytes())
161
269
  }
162
270
 
163
271
  func openBlockingQuestionGates(data []byte) []string {