devrites 4.2.0 → 4.4.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 (117) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/docs/cli.md +15 -0
  4. package/docs/engine/commands.md +20 -2
  5. package/docs/orchestration.md +6 -3
  6. package/engine/commands.go +1 -1
  7. package/engine/internal/lib/cli_observe.go +66 -0
  8. package/engine/internal/lib/observe_summary.go +79 -0
  9. package/engine/internal/lib/open_visual.go +254 -0
  10. package/engine/internal/lib/open_visual_test.go +280 -0
  11. package/engine/internal/lib/skilltrust.go +196 -0
  12. package/engine/internal/lib/taskgraph.go +162 -0
  13. package/engine/internal/lib/taskgraph_test.go +150 -0
  14. package/engine/internal/lib/visual_outline.go +163 -0
  15. package/engine/internal/lib/visual_outline_test.go +161 -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/status.go +41 -16
  25. package/engine/main.go +76 -3
  26. package/engine/root_routing_test.go +50 -15
  27. package/engine/testdata/visual/open-visual-smoke.html +675 -0
  28. package/engine/testdata/visual/open-visual-smoke.outline.md +54 -0
  29. package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
  30. package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  31. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
  32. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
  33. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
  34. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
  35. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
  36. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
  37. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
  38. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
  39. package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
  40. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
  41. package/pack/.claude/skills/rite-build/SKILL.md +29 -36
  42. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +19 -25
  43. package/pack/.claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  44. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
  45. package/pack/.claude/skills/rite-build/reference/output.md +2 -0
  46. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +64 -0
  47. package/pack/.claude/skills/rite-build/reference/phase-contract.md +6 -5
  48. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +7 -3
  49. package/pack/.claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
  50. package/pack/.claude/skills/rite-customize/SKILL.md +3 -2
  51. package/pack/.claude/skills/rite-define/SKILL.md +10 -1
  52. package/pack/.claude/skills/rite-doctor/SKILL.md +5 -1
  53. package/pack/.claude/skills/rite-explain/SKILL.md +23 -7
  54. package/pack/.claude/skills/rite-explain/reference/intake.md +11 -2
  55. package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +3 -0
  56. package/pack/.claude/skills/rite-spec/reference/spec-template.md +5 -2
  57. package/pack/.claude/skills/rite-spec/reference/state-workspace.md +26 -3
  58. package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
  59. package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  60. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
  61. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
  62. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
  63. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
  64. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
  65. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
  66. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
  67. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
  68. package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
  69. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
  70. package/pack/generated/claude/skills/rite-build/SKILL.md +29 -36
  71. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +19 -25
  72. package/pack/generated/claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  73. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
  74. package/pack/generated/claude/skills/rite-build/reference/output.md +2 -0
  75. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +64 -0
  76. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +6 -5
  77. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +7 -3
  78. package/pack/generated/claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
  79. package/pack/generated/claude/skills/rite-customize/SKILL.md +3 -2
  80. package/pack/generated/claude/skills/rite-define/SKILL.md +10 -1
  81. package/pack/generated/claude/skills/rite-doctor/SKILL.md +5 -1
  82. package/pack/generated/claude/skills/rite-explain/SKILL.md +23 -7
  83. package/pack/generated/claude/skills/rite-explain/reference/intake.md +11 -2
  84. package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +3 -0
  85. package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +5 -2
  86. package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +26 -3
  87. package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
  88. package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
  89. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
  90. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
  91. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
  92. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
  93. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
  94. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
  95. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
  96. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
  97. package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
  98. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
  99. package/pack/generated/codex/skills/rite-build/SKILL.md +29 -36
  100. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +19 -25
  101. package/pack/generated/codex/skills/rite-build/reference/checkpoint-protocol.md +29 -59
  102. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +8 -3
  103. package/pack/generated/codex/skills/rite-build/reference/output.md +2 -0
  104. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +64 -0
  105. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +6 -5
  106. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +7 -3
  107. package/pack/generated/codex/skills/rite-clarify/reference/anti-patterns.md +24 -0
  108. package/pack/generated/codex/skills/rite-customize/SKILL.md +3 -2
  109. package/pack/generated/codex/skills/rite-define/SKILL.md +10 -1
  110. package/pack/generated/codex/skills/rite-doctor/SKILL.md +5 -1
  111. package/pack/generated/codex/skills/rite-explain/SKILL.md +23 -7
  112. package/pack/generated/codex/skills/rite-explain/reference/intake.md +11 -2
  113. package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +3 -0
  114. package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +5 -2
  115. package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +26 -3
  116. package/package.json +1 -1
  117. package/scripts/install-lib.sh +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
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.4.0](https://github.com/ViktorsBaikers/DevRites/compare/v4.3.0...v4.4.0) (2026-08-26)
6
+
7
+ ### Added
8
+
9
+ * **devrites:** add local visual HTML artifacts and open-visual ([#42](https://github.com/ViktorsBaikers/DevRites/issues/42)) ([577389e](https://github.com/ViktorsBaikers/DevRites/commit/577389ec3016312e51bbdcad918179b161386aed))
10
+
5
11
  ## [4.2.0](https://github.com/ViktorsBaikers/DevRites/compare/v4.1.0...v4.2.0) (2026-08-23)
6
12
 
7
13
  ### 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.2.0`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v4.2.0): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
31
+ **Status:** [`v4.4.0`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v4.4.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
 
package/docs/cli.md CHANGED
@@ -24,6 +24,7 @@ devrites-engine state resolve <qid> "<answer>"
24
24
  devrites-engine state close <slug>
25
25
 
26
26
  devrites-engine secret-scan [--staged] [--stdin] [slug]
27
+ devrites-engine open-visual <path-or-name> [--slug <slug>] [--no-open]
27
28
  devrites-engine version
28
29
  ```
29
30
 
@@ -69,10 +70,12 @@ with the self-contained updater.
69
70
 
70
71
  - `check candidate <slug>` validates the strict `touched-files.md` manifest and
71
72
  hashes its exact path/state/type/mode/content identity. A pass prints:
73
+
72
74
  ```text
73
75
  candidate-sha256: <64 lowercase hex>
74
76
  candidate-files: <manifest row count>
75
77
  ```
78
+
76
79
  - `check readiness <slug>` verifies the files required to leave the workspace's
77
80
  current phase and, once `eng-review.md` is required, its exact stable
78
81
  Build-input binding.
@@ -118,6 +121,18 @@ secret bytes. It accepts at most 4,096 entries, 64 MiB total captured input, and
118
121
  Findings include only severity, a redacted or escaped source label, category,
119
122
  and zero-based byte offset. HIGH findings exit `3`.
120
123
 
124
+ ## Open visual
125
+
126
+ `open-visual` resolves a portable HTML visualization under the active or
127
+ `--slug` workspace `visual/` directory, or an absolute/relative `.html` path.
128
+ Unless `--no-open` is set, it launches the OS default browser for that local
129
+ file only. A missing sibling `.outline.md` prints a stderr warning and still
130
+ succeeds. When the outline exists, inventory ids missing from HTML also warn
131
+ (non-fatal; HTML-only decorative ids are ignored). Stdout prints compact agent
132
+ tips: absolute HTML path, outline path (or missing), the playbook index hint,
133
+ and `ids=ok` / `ids=mismatch` when an inventory is present. The command never
134
+ fetches remote hosts.
135
+
121
136
  ## Output and exits
122
137
 
123
138
  Lifecycle checks print stable line-oriented fields. `reason: DRV-...` identifies
@@ -7,7 +7,7 @@ workflow strategy.
7
7
  ## Complete operational command inventory
8
8
 
9
9
  | Command | Deterministic responsibility |
10
- |---|---|
10
+ | --- | --- |
11
11
  | `install [flags]` | Install manifest-owned host artifacts and the optional shared binary. |
12
12
  | `update [flags]` | Refresh an existing managed installation. |
13
13
  | `uninstall [flags]` | Remove managed artifacts while preserving runtime workspace state. |
@@ -15,9 +15,14 @@ workflow strategy.
15
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
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` with optional `--root DIR` and optional `JSON-FILE` or `-` | 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. |
25
+ | `open-visual <path-or-name> [--slug <slug>] [--no-open]` | Resolve a local visual HTML file, optionally open it in the OS browser, warn if the sibling outline is missing or inventory ids are absent from HTML, and print agent path tips. No network. |
21
26
  | `version` | Print the engine version. |
22
27
 
23
28
  `help`, `-h`, and `--help` print this operational inventory. `version` and
@@ -82,6 +87,19 @@ Each invocation accepts at most 4,096 entries, 64 MiB total captured input, and
82
87
  4,096 findings. Findings never include matched bytes, excerpts, or value hashes.
83
88
  Input, limit, and output errors exit `2`; HIGH findings exit `3`.
84
89
 
90
+ ## Open visual
91
+
92
+ `open-visual` resolves `<path-or-name>` to a local `.html` file under the
93
+ active/`DEVRITES_WORKSPACE`/`--slug` workspace `visual/` directory, or via an
94
+ absolute/relative path. Missing sibling `.outline.md` warns on stderr but does
95
+ not hard-fail. When the outline exists, the engine compares `## ID inventory`
96
+ ids to HTML `id="..."` attributes and warns (non-fatal) for inventory ids
97
+ missing from HTML; HTML-only decorative ids are ignored. Unless `--no-open`,
98
+ the engine starts the OS opener (`open`, `xdg-open`, or Windows `start`) for
99
+ the local file only — never a network fetch. Stdout prints the absolute HTML
100
+ path, outline path tip, playbook index hint, and an `ids=ok` / `ids=mismatch`
101
+ summary when an inventory is present.
102
+
85
103
  ## Output and exit contracts
86
104
 
87
105
  `check candidate` passes with exactly:
@@ -117,7 +135,7 @@ artifact is classified as `absent`, `empty`, `malformed`, `unsafe`,
117
135
  The closed diagnostic codes and recoveries are:
118
136
 
119
137
  | Code | Exact Gate recovery | Exact standalone readiness-binding payload |
120
- |---|---|---|
138
+ | --- | --- | --- |
121
139
  | `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` |
122
140
  | `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` |
123
141
  | `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` |
@@ -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
 
@@ -26,7 +26,7 @@ const (
26
26
  func rootModeFor(command string, args []string) rootMode {
27
27
  subcommand := firstRootOperand(args)
28
28
  switch command {
29
- case "secret-scan":
29
+ case "secret-scan", "open-visual":
30
30
  return rootLenient
31
31
  case "state":
32
32
  switch subcommand {
@@ -0,0 +1,66 @@
1
+ package lib
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+
7
+ "github.com/devrites/devrites/internal/devritespaths"
8
+ )
9
+
10
+ // RunTaskGraphCheck validates tasks.md for one slug.
11
+ func RunTaskGraphCheck(root, slug string, stdout, stderr io.Writer) int {
12
+ graph, err := CheckTaskGraph(root, slug)
13
+ if err != nil {
14
+ fmt.Fprintf(stderr, "task-graph: %v\n", err)
15
+ return 2
16
+ }
17
+ if len(graph.Problems) > 0 {
18
+ for _, problem := range graph.Problems {
19
+ fmt.Fprintf(stdout, "task-graph: BLOCKED: %s\n", problem)
20
+ }
21
+ return 3
22
+ }
23
+ fmt.Fprintf(stdout, "task-graph: ok (%d slices)\n", len(graph.Slices))
24
+ return 0
25
+ }
26
+
27
+ // RunSkillTrustCheck scans one Markdown path.
28
+ func RunSkillTrustCheck(path string, stdout, stderr io.Writer) int {
29
+ result, err := ScanSkillTrust(path)
30
+ if err != nil {
31
+ fmt.Fprintf(stderr, "skill-trust: %v\n", err)
32
+ return 2
33
+ }
34
+ fmt.Fprint(stdout, FormatSkillTrust(result))
35
+ if SkillTrustBlocks(result.Findings) {
36
+ return 3
37
+ }
38
+ return 0
39
+ }
40
+
41
+ // RunObserveSummary emits JSON for one slug.
42
+ func RunObserveSummary(root, slug string, stdout, stderr io.Writer) int {
43
+ if err := WriteObserveSummaryJSON(root, slug, stdout); err != nil {
44
+ fmt.Fprintf(stderr, "observe: %v\n", err)
45
+ return 2
46
+ }
47
+ return 0
48
+ }
49
+
50
+ // ActiveSlug resolves slug from args or ACTIVE pointer.
51
+ func ActiveSlug(root string, args []string) (string, int, error) {
52
+ if len(args) == 1 {
53
+ return args[0], 0, nil
54
+ }
55
+ if len(args) != 0 {
56
+ return "", 2, fmt.Errorf("expected one slug argument")
57
+ }
58
+ slug, err := devritespaths.ActiveSlug(root)
59
+ if err != nil {
60
+ return "", 2, err
61
+ }
62
+ if slug == "" {
63
+ return "", 2, fmt.Errorf("no slug and ACTIVE is empty")
64
+ }
65
+ return slug, 0, nil
66
+ }
@@ -0,0 +1,79 @@
1
+ package lib
2
+
3
+ import (
4
+ "encoding/json"
5
+ "io"
6
+
7
+ "github.com/devrites/devrites/internal/state"
8
+ )
9
+
10
+ // ObserveSummary is a sanitized, machine-readable workspace snapshot.
11
+ type ObserveSummary struct {
12
+ Slug string `json:"slug"`
13
+ Phase string `json:"phase,omitempty"`
14
+ Status string `json:"status,omitempty"`
15
+ NextAction string `json:"next_action,omitempty"`
16
+ MissingSections []string `json:"missing_sections,omitempty"`
17
+ MissingFiles []string `json:"missing_files,omitempty"`
18
+ PrinciplesPresent bool `json:"principles_present"`
19
+ TaskGraph *struct {
20
+ SliceCount int `json:"slice_count"`
21
+ Cycle []string `json:"cycle,omitempty"`
22
+ Unknown []string `json:"unknown_dependencies,omitempty"`
23
+ OK bool `json:"ok"`
24
+ } `json:"task_graph,omitempty"`
25
+ }
26
+
27
+ // ObserveSummaryFor builds a summary for one feature slug.
28
+ func ObserveSummaryFor(root, slug string) (ObserveSummary, error) {
29
+ report, err := state.Status(root, slug)
30
+ if err != nil {
31
+ return ObserveSummary{}, err
32
+ }
33
+ summary := ObserveSummary{
34
+ Slug: report.Slug,
35
+ Phase: string(report.Phase),
36
+ Status: report.Status,
37
+ NextAction: report.NextAction,
38
+ PrinciplesPresent: report.PrinciplesPresent,
39
+ MissingFiles: append([]string(nil), report.MissingFiles...),
40
+ MissingSections: missingSectionNames(report.Missing),
41
+ }
42
+
43
+ if graph, graphErr := CheckTaskGraph(root, slug); graphErr == nil && len(graph.Slices) > 0 {
44
+ summary.TaskGraph = &struct {
45
+ SliceCount int `json:"slice_count"`
46
+ Cycle []string `json:"cycle,omitempty"`
47
+ Unknown []string `json:"unknown_dependencies,omitempty"`
48
+ OK bool `json:"ok"`
49
+ }{
50
+ SliceCount: len(graph.Slices),
51
+ Cycle: graph.Cycle,
52
+ Unknown: graph.Unknown,
53
+ OK: len(graph.Problems) == 0,
54
+ }
55
+ }
56
+ return summary, nil
57
+ }
58
+
59
+ func missingSectionNames(sections []state.Section) []string {
60
+ if len(sections) == 0 {
61
+ return nil
62
+ }
63
+ out := make([]string, len(sections))
64
+ for i, section := range sections {
65
+ out[i] = string(section)
66
+ }
67
+ return out
68
+ }
69
+
70
+ // WriteObserveSummaryJSON prints one JSON object to stdout.
71
+ func WriteObserveSummaryJSON(root, slug string, stdout io.Writer) error {
72
+ summary, err := ObserveSummaryFor(root, slug)
73
+ if err != nil {
74
+ return err
75
+ }
76
+ enc := json.NewEncoder(stdout)
77
+ enc.SetIndent("", " ")
78
+ return enc.Encode(summary)
79
+ }
@@ -0,0 +1,254 @@
1
+ package lib
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "os"
7
+ "os/exec"
8
+ "path/filepath"
9
+ "runtime"
10
+ "strings"
11
+
12
+ "github.com/devrites/devrites/internal/devritespaths"
13
+ "github.com/devrites/devrites/internal/safepath"
14
+ )
15
+
16
+ const openVisualPlaybookHint = "pack/.claude/skills/devrites-lib/reference/visual-playbooks/index.md"
17
+
18
+ // openVisualOpener launches a local file in the OS default browser.
19
+ // Tests replace this to assert --no-open and avoid spawning a browser.
20
+ var openVisualOpener = openLocalFile
21
+
22
+ // OpenVisual resolves a workspace visual HTML file, optionally opens it in the
23
+ // OS browser, warns when the sibling outline is missing or inventory ids are
24
+ // absent from HTML, and prints an agent tip. It never performs network I/O.
25
+ //
26
+ // Usage: open-visual <path-or-name> [--slug <slug>] [--no-open]
27
+ func OpenVisual(root string, args []string, stdout, stderr io.Writer) int {
28
+ operand, slug, noOpen, err := parseOpenVisualArgs(args)
29
+ if err != nil {
30
+ fmt.Fprintf(stderr, "open-visual: %v\n", err)
31
+ fmt.Fprintln(stderr, "usage: devrites-engine open-visual <path-or-name> [--slug <slug>] [--no-open]")
32
+ return 2
33
+ }
34
+
35
+ htmlPath, nameMode, err := resolveOpenVisualHTML(root, slug, operand)
36
+ if err != nil {
37
+ fmt.Fprintf(stderr, "open-visual: %v\n", err)
38
+ return 2
39
+ }
40
+
41
+ info, err := os.Stat(htmlPath)
42
+ if err != nil {
43
+ fmt.Fprintf(stderr, "open-visual: cannot open %s: %v\n", htmlPath, err)
44
+ return 2
45
+ }
46
+ if info.IsDir() {
47
+ fmt.Fprintf(stderr, "open-visual: %s is a directory, not an HTML file\n", htmlPath)
48
+ return 2
49
+ }
50
+ if resolved, err := filepath.EvalSymlinks(htmlPath); err == nil {
51
+ htmlPath = resolved
52
+ }
53
+ if !strings.EqualFold(filepath.Ext(htmlPath), ".html") {
54
+ fmt.Fprintf(stderr, "open-visual: require a .html file, got %s\n", htmlPath)
55
+ return 2
56
+ }
57
+ if nameMode {
58
+ feature, err := resolveOpenVisualFeatureDir(root, slug)
59
+ if err != nil {
60
+ fmt.Fprintf(stderr, "open-visual: %v\n", err)
61
+ return 2
62
+ }
63
+ visualDir := filepath.Join(feature, "visual")
64
+ if !safepath.WithinResolved(htmlPath, visualDir) {
65
+ fmt.Fprintf(stderr, "open-visual: refused: resolved path escapes workspace visual/\n")
66
+ return 2
67
+ }
68
+ }
69
+
70
+ outlinePath := strings.TrimSuffix(htmlPath, filepath.Ext(htmlPath)) + ".outline.md"
71
+ outlineMissing := false
72
+ if st, err := os.Stat(outlinePath); err != nil || st.IsDir() {
73
+ outlineMissing = true
74
+ fmt.Fprintf(stderr, "open-visual: warning: missing outline companion %s\n", outlinePath)
75
+ }
76
+
77
+ var idReport VisualIDConsistency
78
+ idsChecked := false
79
+ if !outlineMissing {
80
+ htmlBody, herr := os.ReadFile(htmlPath)
81
+ outlineBody, oerr := os.ReadFile(outlinePath)
82
+ switch {
83
+ case herr != nil:
84
+ fmt.Fprintf(stderr, "open-visual: warning: cannot read HTML for id check: %v\n", herr)
85
+ case oerr != nil:
86
+ fmt.Fprintf(stderr, "open-visual: warning: cannot read outline for id check: %v\n", oerr)
87
+ default:
88
+ idReport = CheckVisualIDConsistency(string(htmlBody), string(outlineBody))
89
+ idsChecked = true
90
+ if len(idReport.MissingInHTML) > 0 {
91
+ fmt.Fprintf(stderr, "open-visual: warning: %d outline inventory id(s) missing from HTML: %s\n",
92
+ len(idReport.MissingInHTML), strings.Join(idReport.MissingInHTML, ", "))
93
+ }
94
+ }
95
+ }
96
+
97
+ // Print tips before OS open so agents still get paths if the opener fails.
98
+ fmt.Fprintf(stdout, "open-visual: html=%s\n", htmlPath)
99
+ if outlineMissing {
100
+ fmt.Fprintf(stdout, "open-visual: outline=(missing) %s\n", outlinePath)
101
+ } else {
102
+ fmt.Fprintf(stdout, "open-visual: outline=%s\n", outlinePath)
103
+ }
104
+ fmt.Fprintf(stdout, "open-visual: playbooks=%s\n", openVisualPlaybookHint)
105
+ if idsChecked {
106
+ switch {
107
+ case len(idReport.MissingInHTML) > 0:
108
+ fmt.Fprintf(stdout, "open-visual: ids=mismatch missing=%d inventory=%d\n",
109
+ len(idReport.MissingInHTML), len(idReport.Inventory))
110
+ case len(idReport.Inventory) > 0:
111
+ fmt.Fprintf(stdout, "open-visual: ids=ok (%d inventory)\n", len(idReport.Inventory))
112
+ }
113
+ }
114
+
115
+ if !noOpen {
116
+ if err := openVisualOpener(htmlPath); err != nil {
117
+ fmt.Fprintf(stderr, "open-visual: warning: failed to open browser: %v\n", err)
118
+ // Tips already printed; HTML is local and resolved — warn-and-continue.
119
+ }
120
+ }
121
+ return 0
122
+ }
123
+
124
+ func parseOpenVisualArgs(args []string) (operand, slug string, noOpen bool, err error) {
125
+ for i := 0; i < len(args); i++ {
126
+ arg := args[i]
127
+ switch {
128
+ case arg == "--no-open":
129
+ noOpen = true
130
+ case arg == "--slug":
131
+ if i+1 >= len(args) {
132
+ return "", "", false, fmt.Errorf("--slug requires a value")
133
+ }
134
+ i++
135
+ slug = strings.TrimSpace(args[i])
136
+ if slug == "" {
137
+ return "", "", false, fmt.Errorf("--slug requires a value")
138
+ }
139
+ case strings.HasPrefix(arg, "--slug="):
140
+ slug = strings.TrimSpace(strings.TrimPrefix(arg, "--slug="))
141
+ if slug == "" {
142
+ return "", "", false, fmt.Errorf("--slug requires a value")
143
+ }
144
+ case strings.HasPrefix(arg, "-"):
145
+ return "", "", false, fmt.Errorf("unknown flag %q", arg)
146
+ default:
147
+ if operand != "" {
148
+ return "", "", false, fmt.Errorf("unexpected argument %q", arg)
149
+ }
150
+ operand = arg
151
+ }
152
+ }
153
+ if strings.TrimSpace(operand) == "" {
154
+ return "", "", false, fmt.Errorf("path or visual name required")
155
+ }
156
+ return operand, slug, noOpen, nil
157
+ }
158
+
159
+ // resolveOpenVisualHTML returns the HTML path and whether the operand was a
160
+ // workspace visual name (nameMode). Absolute/relative path operands open any
161
+ // local .html; name operands resolve under the feature visual/ tree.
162
+ func resolveOpenVisualHTML(root, slug, operand string) (string, bool, error) {
163
+ if isOpenVisualPathOperand(operand) {
164
+ abs, err := filepath.Abs(operand)
165
+ if err != nil {
166
+ retErr := fmt.Errorf("resolve path: %w", err)
167
+ return "", false, retErr
168
+ }
169
+ cleaned := filepath.Clean(abs)
170
+ return cleaned, false, nil
171
+ }
172
+
173
+ name := operand
174
+ if !strings.EqualFold(filepath.Ext(name), ".html") {
175
+ name += ".html"
176
+ }
177
+ if filepath.Base(name) != name {
178
+ retErr := fmt.Errorf("visual name must not contain path separators")
179
+ return "", true, retErr
180
+ }
181
+
182
+ feature, err := resolveOpenVisualFeatureDir(root, slug)
183
+ if err != nil {
184
+ return "", true, err
185
+ }
186
+ htmlPath := filepath.Join(feature, "visual", name)
187
+ return htmlPath, true, nil
188
+ }
189
+
190
+ // isOpenVisualPathOperand reports whether operand is a filesystem path rather
191
+ // than a workspace visual name. Absolute paths and operands with separators are
192
+ // paths. Leading "./" or "../" (and bare "." / "..") are relative paths; a
193
+ // leading-dot basename alone (e.g. ".draft") is still a visual name.
194
+ func isOpenVisualPathOperand(operand string) bool {
195
+ if filepath.IsAbs(operand) {
196
+ return true
197
+ }
198
+ if operand == "." || operand == ".." {
199
+ return true
200
+ }
201
+ if strings.HasPrefix(operand, "./") || strings.HasPrefix(operand, "../") {
202
+ return true
203
+ }
204
+ if strings.HasPrefix(operand, `.\`) || strings.HasPrefix(operand, `..\`) {
205
+ return true
206
+ }
207
+ return strings.ContainsAny(operand, `/\`)
208
+ }
209
+
210
+ func resolveOpenVisualFeatureDir(root, slug string) (string, error) {
211
+ if root == "" {
212
+ retErr := fmt.Errorf("DevRites root required to resolve a visual name")
213
+ return "", retErr
214
+ }
215
+ if slug == "" {
216
+ active, err := devritespaths.ActiveSlug(root)
217
+ if err != nil {
218
+ return "", err
219
+ }
220
+ if active == "" {
221
+ retErr := fmt.Errorf("no slug: pass --slug or set ACTIVE / DEVRITES_WORKSPACE")
222
+ return "", retErr
223
+ }
224
+ slug = active
225
+ }
226
+ dir, err := devritespaths.ExistingFeatureDirChecked(root, slug)
227
+ if err != nil {
228
+ if os.IsNotExist(err) {
229
+ retErr := fmt.Errorf("no workspace for slug %q", slug)
230
+ return "", retErr
231
+ }
232
+ return "", err
233
+ }
234
+ return dir, nil
235
+ }
236
+
237
+ func openLocalFile(path string) error {
238
+ var cmd *exec.Cmd
239
+ switch runtime.GOOS {
240
+ case "darwin":
241
+ // argv only — no shell.
242
+ cmd = exec.Command("open", path)
243
+ case "windows":
244
+ // Avoid cmd.exe /c shell composition; FileProtocolHandler takes one path argv.
245
+ cmd = exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", path)
246
+ default:
247
+ // argv only — no shell.
248
+ cmd = exec.Command("xdg-open", path)
249
+ }
250
+ if err := cmd.Start(); err != nil {
251
+ return fmt.Errorf("open local file: %w", err)
252
+ }
253
+ return nil
254
+ }