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.
- package/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/docs/engine/commands.md +69 -6
- package/docs/orchestration.md +6 -3
- package/engine/go.mod +1 -1
- package/engine/internal/gate/gate.go +154 -46
- package/engine/internal/gate/gate_test.go +290 -2
- package/engine/internal/gate/readiness_binding.go +66 -100
- package/engine/internal/gate/readiness_binding_test.go +126 -3
- package/engine/internal/lib/cli_observe.go +66 -0
- package/engine/internal/lib/observe_summary.go +79 -0
- package/engine/internal/lib/resolve.go +6 -6
- 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/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/cmd/workflowmanifest/main.go +32 -3
- package/engine/internal/state/feature.go +1 -100
- package/engine/internal/state/observation.go +584 -0
- package/engine/internal/state/observation_open_other.go +9 -0
- package/engine/internal/state/observation_open_unix.go +12 -0
- package/engine/internal/state/observation_test.go +960 -0
- package/engine/internal/state/observation_unix_test.go +68 -0
- package/engine/internal/state/schema.go +60 -160
- package/engine/internal/state/state_test.go +489 -101
- package/engine/internal/state/status.go +97 -53
- package/engine/main.go +72 -3
- package/engine/root_routing_test.go +1 -1
- package/engine/tests/adr_0004_required_by_phase_test.go +16 -13
- package/engine/tests/adr_0011_define_plan_test.go +14 -12
- package/engine/tests/gate_test.go +403 -28
- package/engine/tests/workspace_observation_migration_test.go +1065 -0
- package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/.claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/.claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/.claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/.claude/skills/rite-build/SKILL.md +46 -81
- 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 +9 -22
- 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-doctor/SKILL.md +5 -1
- package/pack/.claude/skills/rite-plan/SKILL.md +17 -17
- package/pack/.claude/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +3 -0
- package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/.claude/skills/rite-prove/SKILL.md +95 -153
- package/pack/.claude/skills/rite-vet/SKILL.md +121 -174
- package/pack/.claude/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/.claude/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/.claude/skills/rite-vet/reference/depth.md +10 -3
- package/pack/.claude/skills/rite-vet/reference/review-axes.md +12 -4
- package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/generated/claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/generated/claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/generated/claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/generated/claude/skills/rite-build/SKILL.md +46 -81
- 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 +9 -22
- 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-doctor/SKILL.md +5 -1
- package/pack/generated/claude/skills/rite-plan/SKILL.md +17 -17
- package/pack/generated/claude/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +3 -0
- package/pack/generated/claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/generated/claude/skills/rite-prove/SKILL.md +95 -153
- package/pack/generated/claude/skills/rite-vet/SKILL.md +121 -174
- package/pack/generated/claude/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/generated/claude/skills/rite-vet/reference/depth.md +10 -3
- package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +12 -4
- package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/generated/codex/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/generated/codex/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +22 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/generated/codex/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/generated/codex/skills/rite-build/SKILL.md +46 -81
- 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 +9 -22
- 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-doctor/SKILL.md +5 -1
- package/pack/generated/codex/skills/rite-plan/SKILL.md +17 -17
- package/pack/generated/codex/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +3 -0
- package/pack/generated/codex/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/generated/codex/skills/rite-prove/SKILL.md +95 -153
- package/pack/generated/codex/skills/rite-vet/SKILL.md +121 -174
- package/pack/generated/codex/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/generated/codex/skills/rite-vet/reference/depth.md +10 -3
- package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +12 -4
- 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"),
|
|
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"),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
293
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
+
}
|