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.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/docs/cli.md +15 -0
- package/docs/engine/commands.md +20 -2
- package/docs/orchestration.md +6 -3
- package/engine/commands.go +1 -1
- package/engine/internal/lib/cli_observe.go +66 -0
- package/engine/internal/lib/observe_summary.go +79 -0
- package/engine/internal/lib/open_visual.go +254 -0
- package/engine/internal/lib/open_visual_test.go +280 -0
- package/engine/internal/lib/skilltrust.go +196 -0
- package/engine/internal/lib/taskgraph.go +162 -0
- package/engine/internal/lib/taskgraph_test.go +150 -0
- package/engine/internal/lib/visual_outline.go +163 -0
- package/engine/internal/lib/visual_outline_test.go +161 -0
- package/engine/internal/lib/workspace_read.go +29 -0
- package/engine/internal/parallel/cli.go +467 -0
- package/engine/internal/parallel/git.go +181 -0
- package/engine/internal/parallel/lease.go +244 -0
- package/engine/internal/parallel/ops.go +416 -0
- package/engine/internal/parallel/parallel_test.go +242 -0
- package/engine/internal/parallel/pathdisjoint.go +129 -0
- package/engine/internal/parallel/pathdisjoint_test.go +97 -0
- package/engine/internal/state/status.go +41 -16
- package/engine/main.go +76 -3
- package/engine/root_routing_test.go +50 -15
- package/engine/testdata/visual/open-visual-smoke.html +675 -0
- package/engine/testdata/visual/open-visual-smoke.outline.md +54 -0
- package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
- package/pack/.claude/skills/rite-build/SKILL.md +29 -36
- package/pack/.claude/skills/rite-build/reference/afk-discipline.md +19 -25
- package/pack/.claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
- package/pack/.claude/skills/rite-build/reference/output.md +2 -0
- package/pack/.claude/skills/rite-build/reference/parallel-batch.md +64 -0
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +6 -5
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +7 -3
- package/pack/.claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
- package/pack/.claude/skills/rite-customize/SKILL.md +3 -2
- package/pack/.claude/skills/rite-define/SKILL.md +10 -1
- package/pack/.claude/skills/rite-doctor/SKILL.md +5 -1
- package/pack/.claude/skills/rite-explain/SKILL.md +23 -7
- package/pack/.claude/skills/rite-explain/reference/intake.md +11 -2
- package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +3 -0
- package/pack/.claude/skills/rite-spec/reference/spec-template.md +5 -2
- package/pack/.claude/skills/rite-spec/reference/state-workspace.md +26 -3
- package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
- package/pack/generated/claude/skills/rite-build/SKILL.md +29 -36
- package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +19 -25
- package/pack/generated/claude/skills/rite-build/reference/checkpoint-protocol.md +29 -59
- package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +8 -3
- package/pack/generated/claude/skills/rite-build/reference/output.md +2 -0
- package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +64 -0
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +6 -5
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +7 -3
- package/pack/generated/claude/skills/rite-clarify/reference/anti-patterns.md +24 -0
- package/pack/generated/claude/skills/rite-customize/SKILL.md +3 -2
- package/pack/generated/claude/skills/rite-define/SKILL.md +10 -1
- package/pack/generated/claude/skills/rite-doctor/SKILL.md +5 -1
- package/pack/generated/claude/skills/rite-explain/SKILL.md +23 -7
- package/pack/generated/claude/skills/rite-explain/reference/intake.md +11 -2
- package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +3 -0
- package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +5 -2
- package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +26 -3
- package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
- package/pack/generated/codex/skills/rite-build/SKILL.md +29 -36
- package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +19 -25
- package/pack/generated/codex/skills/rite-build/reference/checkpoint-protocol.md +29 -59
- package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +8 -3
- package/pack/generated/codex/skills/rite-build/reference/output.md +2 -0
- package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +64 -0
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +6 -5
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +7 -3
- package/pack/generated/codex/skills/rite-clarify/reference/anti-patterns.md +24 -0
- package/pack/generated/codex/skills/rite-customize/SKILL.md +3 -2
- package/pack/generated/codex/skills/rite-define/SKILL.md +10 -1
- package/pack/generated/codex/skills/rite-doctor/SKILL.md +5 -1
- package/pack/generated/codex/skills/rite-explain/SKILL.md +23 -7
- package/pack/generated/codex/skills/rite-explain/reference/intake.md +11 -2
- package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +3 -0
- package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +5 -2
- package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +26 -3
- package/package.json +1 -1
- 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.
|
|
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
|
package/docs/engine/commands.md
CHANGED
|
@@ -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` |
|
package/docs/orchestration.md
CHANGED
|
@@ -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.
|
|
130
|
-
|
|
131
|
-
|
|
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/commands.go
CHANGED
|
@@ -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
|
+
}
|