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
@@ -8,6 +8,7 @@ import (
8
8
  "testing"
9
9
 
10
10
  "github.com/devrites/devrites/internal/reason"
11
+ "github.com/devrites/devrites/internal/state"
11
12
  "github.com/devrites/devrites/internal/testutil"
12
13
  )
13
14
 
@@ -135,12 +136,12 @@ func TestReadinessBindingRejectsUnsafeInputsWithoutDisclosure(t *testing.T) {
135
136
  }
136
137
  }},
137
138
  {name: "per input limit", mutate: func(t *testing.T, workspace string) {
138
- if err := os.WriteFile(filepath.Join(workspace, "test-plan.md"), bytes.Repeat([]byte("a"), int(maxReadinessInputBytes)+1), 0o644); err != nil {
139
+ if err := os.WriteFile(filepath.Join(workspace, "test-plan.md"), bytes.Repeat([]byte("a"), (1<<20)+1), 0o644); err != nil {
139
140
  t.Fatal(err)
140
141
  }
141
142
  }},
142
143
  {name: "aggregate limit", mutate: func(t *testing.T, workspace string) {
143
- content := bytes.Repeat([]byte("a"), int(maxReadinessInputBytes))
144
+ content := bytes.Repeat([]byte("a"), 1<<20)
144
145
  for _, name := range []string{"spec.md", "decision-coverage.md", "architecture.md", "plan.md", "tasks.md", "traceability.md", "test-plan.md", "strategy.md", "design-brief.md"} {
145
146
  if err := os.WriteFile(filepath.Join(workspace, name), content, 0o644); err != nil {
146
147
  t.Fatal(err)
@@ -173,11 +174,18 @@ func TestVerifyReadinessBindingRequiresOneStructuralStandaloneLine(t *testing.T)
173
174
  wantErr bool
174
175
  }{
175
176
  {name: "exact", content: "# Engineering review\n\n" + expected + "\n"},
177
+ {name: "exact CRLF", content: "# Engineering review\r\n\r\n" + expected + "\r\n"},
176
178
  {name: "exact plus fenced example", content: expected + "\n```text\n" + expected + "\n```\n"},
177
179
  {name: "missing", content: "# Engineering review\n\nREADY\n", wantErr: true},
178
180
  {name: "malformed", content: readinessBindingLabel + strings.Repeat("a", 63) + "\n", wantErr: true},
179
181
  {name: "duplicate", content: expected + "\n" + expected + "\n", wantErr: true},
182
+ {name: "prefixed", content: "prefix " + expected + "\n", wantErr: true},
180
183
  {name: "fenced only", content: "```text\n" + expected + "\n```\n", wantErr: true},
184
+ {name: "unterminated fence", content: "```text\n" + expected + "\n", wantErr: true},
185
+ {name: "tilde fenced only", content: "~~~text\n" + expected + "\n~~~~~\n", wantErr: true},
186
+ {name: "tilde fenced plus visible", content: "~~~text\n" + expected + "\n~~~~~\n" + expected + "\n"},
187
+ {name: "HTML comment remains structural", content: "<!--\n" + expected + "\n-->\n"},
188
+ {name: "inline comment marker does not hide binding", content: "`<!--`\n" + expected + "\n"},
181
189
  {name: "mismatch", content: readinessBindingLabel + strings.Repeat("0", 64) + "\n", wantErr: true},
182
190
  {name: "not standalone", content: " " + expected + "\n", wantErr: true},
183
191
  }
@@ -185,7 +193,11 @@ func TestVerifyReadinessBindingRequiresOneStructuralStandaloneLine(t *testing.T)
185
193
  for _, test := range tests {
186
194
  t.Run(test.name, func(t *testing.T) {
187
195
  testutil.WriteFile(t, filepath.Join(workspace, "eng-review.md"), test.content)
188
- got, err := verifyReadinessBinding(root, "markers")
196
+ observation, observeErr := state.ObserveWorkspace(root, "markers")
197
+ if observeErr != nil {
198
+ t.Fatal(observeErr)
199
+ }
200
+ got, err := verifyReadinessBinding(observation)
189
201
  if (err != nil) != test.wantErr {
190
202
  t.Fatalf("verifyReadinessBinding() error=%v, wantErr=%v", err, test.wantErr)
191
203
  }
@@ -238,6 +250,117 @@ func TestCheckUsesReadinessBindingForBuildAndSeal(t *testing.T) {
238
250
  }
239
251
  }
240
252
 
253
+ func TestReadinessBindingObservationUsesRetainedBytes(t *testing.T) {
254
+ root := t.TempDir()
255
+ workspace := writeReadinessFixture(t, root, "retained-binding", "build")
256
+ observation, err := state.ObserveWorkspace(root, "retained-binding")
257
+ if err != nil {
258
+ t.Fatal(err)
259
+ }
260
+ before, err := readinessBindingFromObservation(observation)
261
+ if err != nil {
262
+ t.Fatal(err)
263
+ }
264
+
265
+ testutil.WriteFile(t, filepath.Join(workspace, "spec.md"), "# Spec\n\nChanged after observation.\n")
266
+ testutil.WriteFile(t, filepath.Join(workspace, "strategy.md"), "# Strategy\n\nAdded after observation.\n")
267
+ after, err := readinessBindingFromObservation(observation)
268
+ if err != nil {
269
+ t.Fatal(err)
270
+ }
271
+ if after != before {
272
+ t.Fatalf("retained observation binding changed: before=%q after=%q", before, after)
273
+ }
274
+ }
275
+
276
+ func TestReadinessBindingOptionalEmptyFilesRemainPresent(t *testing.T) {
277
+ forms := []struct {
278
+ name string
279
+ body string
280
+ }{
281
+ {name: "zero-byte"},
282
+ {name: "heading-only", body: "# Optional\n"},
283
+ {name: "frontmatter-only", body: "---\nkind: optional\n---\n"},
284
+ }
285
+ for _, logical := range []string{"strategy.md", "design-brief.md", "ai-spec.md", ".devrites/principles.md"} {
286
+ for _, form := range forms {
287
+ t.Run(logical+"/"+form.name, func(t *testing.T) {
288
+ root := t.TempDir()
289
+ workspace := writeReadinessFixture(t, root, "optional-empty", "build")
290
+ baseline := mustReadinessBinding(t, root, "optional-empty")
291
+ path := filepath.Join(workspace, logical)
292
+ if logical == ".devrites/principles.md" {
293
+ path = filepath.Join(root, "principles.md")
294
+ }
295
+ testutil.WriteFile(t, path, form.body)
296
+ present := mustReadinessBinding(t, root, "optional-empty")
297
+ if present == baseline {
298
+ t.Fatalf("adding %s %s did not encode present bytes", form.name, logical)
299
+ }
300
+ if err := os.Remove(path); err != nil {
301
+ t.Fatal(err)
302
+ }
303
+ if restored := mustReadinessBinding(t, root, "optional-empty"); restored != baseline {
304
+ t.Fatalf("removing %s did not restore absent binding: got %q want %q", logical, restored, baseline)
305
+ }
306
+ })
307
+ }
308
+ }
309
+ }
310
+
311
+ func TestCheckReadinessBindingDetectsEmptyOptionalAddAndRemove(t *testing.T) {
312
+ for _, logical := range []string{"strategy.md", "design-brief.md", "ai-spec.md", ".devrites/principles.md"} {
313
+ t.Run(logical, func(t *testing.T) {
314
+ root := t.TempDir()
315
+ workspace := writeReadinessFixture(t, root, "empty-stale", "build")
316
+ binding := mustReadinessBinding(t, root, "empty-stale")
317
+ testutil.AppendFile(t, filepath.Join(workspace, "eng-review.md"), "\n"+binding+"\n")
318
+ path := filepath.Join(workspace, logical)
319
+ if logical == ".devrites/principles.md" {
320
+ path = filepath.Join(root, "principles.md")
321
+ }
322
+ testutil.WriteFile(t, path, "")
323
+
324
+ result, err := Check(Readiness, root, "empty-stale")
325
+ if err != nil {
326
+ t.Fatal(err)
327
+ }
328
+ if !result.Blocked || result.ReasonID != reason.GateReadinessStale {
329
+ t.Fatalf("empty optional add blocked=%v reason=%q", result.Blocked, result.ReasonID)
330
+ }
331
+ if err := os.Remove(path); err != nil {
332
+ t.Fatal(err)
333
+ }
334
+ result, err = Check(Readiness, root, "empty-stale")
335
+ if err != nil {
336
+ t.Fatal(err)
337
+ }
338
+ if result.Blocked {
339
+ t.Fatalf("empty optional removal remained blocked: %s", result.Render())
340
+ }
341
+ })
342
+ }
343
+ }
344
+
345
+ func TestReadinessDiagnosticPayloads(t *testing.T) {
346
+ for _, tc := range []struct {
347
+ diagnostic state.ArtifactDiagnostic
348
+ want string
349
+ }{
350
+ {state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactMalformed, Code: state.DiagnosticMalformedMarkdown}, "readiness input strategy.md is malformed (malformed_markdown); replace invalid Markdown with valid Markdown"},
351
+ {state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnsafe, Code: state.DiagnosticParentSymlink}, "readiness input strategy.md is unsafe (parent_symlink); replace the symlinked parent with a real directory"},
352
+ {state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnsafe, Code: state.DiagnosticFinalSymlink}, "readiness input strategy.md is unsafe (final_symlink); replace the symlink with a regular file"},
353
+ {state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnsafe, Code: state.DiagnosticNonRegular}, "readiness input strategy.md is unsafe (non_regular); replace the non-regular entry with a regular file"},
354
+ {state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnsafe, Code: state.DiagnosticFileTooLarge}, "readiness input strategy.md is unsafe (file_too_large); reduce the file to at most 1 MiB"},
355
+ {state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnreadable, Code: state.DiagnosticPermissionDenied}, "readiness input strategy.md is unreadable (permission_denied); grant read permission"},
356
+ {state.ArtifactDiagnostic{Path: "strategy.md", State: state.ArtifactUnreadable, Code: state.DiagnosticReadFailure}, "readiness input strategy.md is unreadable (read_failure); restore a readable regular file"},
357
+ } {
358
+ if got := readinessDiagnosticError(tc.diagnostic).Error(); got != tc.want {
359
+ t.Errorf("readinessDiagnosticError(%+v)=%q, want %q", tc.diagnostic, got, tc.want)
360
+ }
361
+ }
362
+ }
363
+
241
364
  func writeReadinessFixture(t *testing.T, root, slug, phase string) string {
242
365
  t.Helper()
243
366
  workspace := filepath.Join(root, "work", slug)
@@ -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
+ }
@@ -286,15 +286,15 @@ func clearAwaiting(sfile, qid string) error {
286
286
  return fmt.Errorf("read state %s: %w", sfile, err)
287
287
  }
288
288
  lines := splitLinesNoTrailing(data)
289
- resumePhase := state.PhaseBuild
289
+ resumePolicy, _ := state.PolicyFor(state.PhaseBuild)
290
290
  if rawPhase, ok := state.CursorField(lines, state.CursorPhase); ok {
291
291
  if fields := strings.Fields(strings.ToLower(rawPhase)); len(fields) > 0 {
292
- if phase, known := state.PhaseForName(fields[0]); known && state.ResumeVerb(phase) != "" {
293
- resumePhase = phase
292
+ if policy, known := state.PolicyFor(state.Phase(fields[0])); known && policy.ResumeVerb != "" {
293
+ resumePolicy = policy
294
294
  }
295
295
  }
296
296
  }
297
- resumeCommand := workflow.ForVerb(state.ResumeVerb(resumePhase))
297
+ resumeCommand := workflow.ForVerb(resumePolicy.ResumeVerb)
298
298
  // First check whether the awaiting block references this question at all.
299
299
  inAw := false
300
300
  var awaitingLines []string
@@ -337,7 +337,7 @@ func clearAwaiting(sfile, qid string) error {
337
337
  continue
338
338
  case inLog && hdrSpaceRe.MatchString(line):
339
339
  if !logAppended {
340
- out = append(out, fmt.Sprintf("- %s %s: resolved %s", ts, resumePhase, qid))
340
+ out = append(out, fmt.Sprintf("- %s %s: resolved %s", ts, resumePolicy.Target, qid))
341
341
  logAppended = true
342
342
  }
343
343
  inLog = false
@@ -347,7 +347,7 @@ func clearAwaiting(sfile, qid string) error {
347
347
  out = append(out, line)
348
348
  }
349
349
  if inLog && !logAppended {
350
- out = append(out, fmt.Sprintf("- %s %s: resolved %s", ts, resumePhase, qid))
350
+ out = append(out, fmt.Sprintf("- %s %s: resolved %s", ts, resumePolicy.Target, qid))
351
351
  }
352
352
  out, _ = state.SetCursorField(out, state.CursorStatus, "running")
353
353
  out, _ = state.SetCursorField(out, state.CursorNextAction, "(resume: `"+resumeCommand.Both()+"` to continue the workflow)")
@@ -0,0 +1,196 @@
1
+ package lib
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "os"
7
+ "path/filepath"
8
+ "regexp"
9
+ "strings"
10
+ "unicode"
11
+ )
12
+
13
+ // SkillTrustFinding is one deterministic trust scan result.
14
+ type SkillTrustFinding struct {
15
+ Severity string // HIGH or MEDIUM
16
+ Rule string
17
+ Line int
18
+ Detail string
19
+ }
20
+
21
+ // SkillTrustResult summarizes a skill-trust scan.
22
+ type SkillTrustResult struct {
23
+ Path string
24
+ Findings []SkillTrustFinding
25
+ }
26
+
27
+ var skillTrustRules = []struct {
28
+ severity string
29
+ rule string
30
+ re *regexp.Regexp
31
+ detail string
32
+ }{
33
+ {
34
+ severity: "HIGH",
35
+ rule: "prompt_injection_disregard",
36
+ re: regexp.MustCompile(`(?i)\b(disregard|ignore)\b.{0,40}\b(prior|previous|above|system|instruction|rule|constraint|safety)\b`),
37
+ detail: "instruction attempts to override prior constraints",
38
+ },
39
+ {
40
+ severity: "HIGH",
41
+ rule: "credential_exfil_curl_env",
42
+ re: regexp.MustCompile(`(?i)\bcurl\b.{0,120}\$(?:ENV|HOME|[A-Z_][A-Z0-9_]*)`),
43
+ detail: "curl command references environment variables (possible exfiltration)",
44
+ },
45
+ {
46
+ severity: "HIGH",
47
+ rule: "sensitive_path_reference",
48
+ re: regexp.MustCompile(`(?i)(~/.ssh|~/.aws|/etc/passwd|id_rsa|\.pem\b|api[_-]?key\s*[:=])`),
49
+ detail: "references sensitive filesystem or credential material",
50
+ },
51
+ {
52
+ severity: "MEDIUM",
53
+ rule: "hidden_remote_fetch",
54
+ re: regexp.MustCompile(`(?i)\b(curl|wget|fetch)\b.{0,80}\b(http|https)://`),
55
+ detail: "network fetch instruction in skill prose",
56
+ },
57
+ }
58
+
59
+ // ScanSkillTrust reads one Markdown skill/agent file and returns structural trust findings.
60
+ func ScanSkillTrust(path string) (SkillTrustResult, error) {
61
+ info, err := os.Stat(path)
62
+ if err != nil {
63
+ return SkillTrustResult{}, err
64
+ }
65
+ if info.IsDir() {
66
+ return SkillTrustResult{}, fmt.Errorf("skill-trust: %q is a directory", path)
67
+ }
68
+ if info.Size() > 1<<20 {
69
+ return SkillTrustResult{}, fmt.Errorf("skill-trust: %q exceeds 1 MiB limit", path)
70
+ }
71
+ file, err := os.Open(path)
72
+ if err != nil {
73
+ return SkillTrustResult{}, err
74
+ }
75
+ defer file.Close()
76
+ raw, err := io.ReadAll(io.LimitReader(file, 1<<20+1))
77
+ if err != nil {
78
+ return SkillTrustResult{}, err
79
+ }
80
+ if len(raw) > 1<<20 {
81
+ return SkillTrustResult{}, fmt.Errorf("skill-trust: %q exceeds 1 MiB limit", path)
82
+ }
83
+ return SkillTrustResult{Path: filepath.Clean(path), Findings: analyzeSkillTrustContent(string(raw))}, nil
84
+ }
85
+
86
+ func analyzeSkillTrustContent(text string) []SkillTrustFinding {
87
+ var findings []SkillTrustFinding
88
+ lines := strings.Split(text, "\n")
89
+ for i, line := range lines {
90
+ lineNum := i + 1
91
+ for _, r := range line {
92
+ if isSuspiciousUnicode(r) {
93
+ findings = append(findings, SkillTrustFinding{
94
+ Severity: "HIGH",
95
+ Rule: "suspicious_unicode",
96
+ Line: lineNum,
97
+ Detail: fmt.Sprintf("suspicious Unicode U+%04X", r),
98
+ })
99
+ break
100
+ }
101
+ }
102
+ for _, rule := range skillTrustRules {
103
+ if rule.re.MatchString(line) {
104
+ findings = append(findings, SkillTrustFinding{
105
+ Severity: rule.severity,
106
+ Rule: rule.rule,
107
+ Line: lineNum,
108
+ Detail: rule.detail,
109
+ })
110
+ }
111
+ }
112
+ }
113
+ findings = appendMissingNameFinding(text, findings)
114
+ return dedupeFindings(findings)
115
+ }
116
+
117
+ func looksLikeSkillFile(text string) bool {
118
+ lower := strings.ToLower(text)
119
+ return strings.Contains(lower, "user-invocable:") || strings.Contains(lower, "# /rite-") || strings.Contains(lower, "disable-model-invocation:")
120
+ }
121
+
122
+ func appendMissingNameFinding(text string, findings []SkillTrustFinding) []SkillTrustFinding {
123
+ if strings.Contains(text, "---") && !strings.Contains(text, "\nname:") && looksLikeSkillFile(text) {
124
+ findings = append(findings, SkillTrustFinding{
125
+ Severity: "MEDIUM",
126
+ Rule: "missing_name_frontmatter",
127
+ Line: 1,
128
+ Detail: "skill-like Markdown lacks name frontmatter",
129
+ })
130
+ }
131
+ return findings
132
+ }
133
+
134
+ func isSuspiciousUnicode(r rune) bool {
135
+ switch r {
136
+ case '\u200B', '\u200C', '\u200D', '\uFEFF', '\u2060':
137
+ return true
138
+ }
139
+ if r >= '\u202A' && r <= '\u202E' {
140
+ return true
141
+ }
142
+ if r >= '\u2066' && r <= '\u2069' {
143
+ return true
144
+ }
145
+ return false
146
+ }
147
+
148
+ func dedupeFindings(in []SkillTrustFinding) []SkillTrustFinding {
149
+ if len(in) == 0 {
150
+ return nil
151
+ }
152
+ seen := make(map[string]bool, len(in))
153
+ out := make([]SkillTrustFinding, 0, len(in))
154
+ for _, finding := range in {
155
+ key := finding.Rule + "|" + fmt.Sprint(finding.Line) + "|" + finding.Detail
156
+ if seen[key] {
157
+ continue
158
+ }
159
+ seen[key] = true
160
+ out = append(out, finding)
161
+ }
162
+ return out
163
+ }
164
+
165
+ // SkillTrustBlocks returns true when findings contain any HIGH severity item.
166
+ func SkillTrustBlocks(findings []SkillTrustFinding) bool {
167
+ for _, finding := range findings {
168
+ if finding.Severity == "HIGH" {
169
+ return true
170
+ }
171
+ }
172
+ return false
173
+ }
174
+
175
+ // FormatSkillTrust writes a human-readable report.
176
+ func FormatSkillTrust(result SkillTrustResult) string {
177
+ if len(result.Findings) == 0 {
178
+ return fmt.Sprintf("skill-trust: PASS (%s)\n", result.Path)
179
+ }
180
+ var b strings.Builder
181
+ fmt.Fprintf(&b, "skill-trust: FINDINGS (%s)\n", result.Path)
182
+ for _, finding := range result.Findings {
183
+ fmt.Fprintf(&b, "%s %s line=%d %s\n", finding.Severity, finding.Rule, finding.Line, finding.Detail)
184
+ }
185
+ return b.String()
186
+ }
187
+
188
+ // ContainsOnlyASCII returns false when non-ASCII control or bidi characters appear.
189
+ func ContainsOnlyASCII(text string) bool {
190
+ for _, r := range text {
191
+ if r > unicode.MaxASCII && isSuspiciousUnicode(r) {
192
+ return false
193
+ }
194
+ }
195
+ return true
196
+ }