devrites 3.2.5 → 3.2.7
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/cli.md +0 -1
- package/docs/command-map.md +1 -1
- package/docs/orchestration.md +1 -2
- package/engine/commands.go +1 -1
- package/engine/hooks_agent_dispatch.go +98 -33
- package/engine/internal/lib/reconcile.go +1 -1
- package/engine/internal/lib/reconcile_test.go +4 -12
- package/engine/internal/lib/util.go +2 -2
- package/engine/main.go +0 -3
- package/engine/root_routing_test.go +0 -1
- package/engine/tests/hook_test.go +235 -0
- package/pack/.claude/skills/rite-build/reference/forge.md +2 -2
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +11 -20
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +2 -5
- package/pack/.claude/skills/rite-prove/reference/acceptance-proof.md +0 -1
- package/pack/.claude/skills/rite-seal/reference/final-evidence.md +0 -1
- package/pack/generated/claude/skills/rite-build/reference/forge.md +2 -2
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +11 -20
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +2 -5
- package/pack/generated/claude/skills/rite-prove/reference/acceptance-proof.md +0 -1
- package/pack/generated/claude/skills/rite-seal/reference/final-evidence.md +0 -1
- package/pack/generated/codex/skills/rite-build/reference/forge.md +2 -2
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +11 -20
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +2 -5
- package/pack/generated/codex/skills/rite-prove/reference/acceptance-proof.md +0 -1
- package/pack/generated/codex/skills/rite-seal/reference/final-evidence.md +0 -1
- package/package.json +1 -1
- package/scripts/build-release-tarball.sh +1 -0
- package/scripts/validate.sh +0 -1
- package/engine/internal/lib/packageexistence.go +0 -968
- package/engine/internal/lib/packageexistence_test.go +0 -506
- package/engine/testdata/golden/TestParityPackageExistence/declared.golden +0 -2
- package/engine/testdata/golden/TestParityPackageExistence/default-import-subpath.golden +0 -2
- package/engine/tests/parity_packageexistence_test.go +0 -64
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
|
|
4
4
|
|
|
5
|
+
## [3.2.7](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.6...v3.2.7) (2026-07-26)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
* **agents:** enforce conditional v2 dispatch ([f4169d4](https://github.com/ViktorsBaikers/DevRites/commit/f4169d41c456f352172be8d96afdcdc69800d849))
|
|
10
|
+
* **ci:** satisfy staticcheck error style ([0023265](https://github.com/ViktorsBaikers/DevRites/commit/00232656283647db8d94b2ed61dccb1629091b25))
|
|
11
|
+
|
|
12
|
+
## [3.2.6](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.5...v3.2.6) (2026-07-25)
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
* **devrites:** remove package-existence gate ([4cbcbe9](https://github.com/ViktorsBaikers/DevRites/commit/4cbcbe9002eaf97f8f921e13e97b4dcfc6b61b34))
|
|
17
|
+
|
|
5
18
|
## [3.2.5](https://github.com/ViktorsBaikers/DevRites/compare/v3.2.4...v3.2.5) (2026-07-25)
|
|
6
19
|
|
|
7
20
|
### Fixed
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ final commit, push, and tag, and it requires a typed `GO` confirmation.
|
|
|
25
25
|
Unattended runs may create local WIP checkpoint commits along the way, but only
|
|
26
26
|
Ship collapses and pushes them.
|
|
27
27
|
|
|
28
|
-
**Status:** [`v3.2.
|
|
28
|
+
**Status:** [`v3.2.7`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v3.2.7): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
package/docs/cli.md
CHANGED
|
@@ -21,7 +21,6 @@ devrites-engine recovery route <class> # typed owner/action; JSON re
|
|
|
21
21
|
devrites-engine recovery check|record|clear ... # durable three-failure budget; record/clear accept --class
|
|
22
22
|
devrites-engine reconcile snapshot|check|close [slug] # retained writer baseline
|
|
23
23
|
devrites-engine test-integrity [slug] # reject weakened tests against that baseline
|
|
24
|
-
devrites-engine package-existence [slug] # verify new imports are declared
|
|
25
24
|
devrites-engine evidence-fresh [slug] # proof freshness gate (exit 0 fresh · 3 stale)
|
|
26
25
|
devrites-engine check-acceptance <dir> # acceptance gate (exit 0 proven · 1 gap)
|
|
27
26
|
devrites-engine ledger sync <dir> # fold a feature's spec deltas into the living capability ledger
|
package/docs/command-map.md
CHANGED
|
@@ -41,7 +41,7 @@ these lanes so its owner is clear:
|
|
|
41
41
|
|
|
42
42
|
| Lane | Commands | Owner |
|
|
43
43
|
|---|---|---|
|
|
44
|
-
| Workflow gates | `preamble`, `build-readiness`, `readiness-digest`, `spec-skeleton`, `spec-validate`, `check-acceptance`, `evidence-fresh`, `coverage`, `doubt-coverage`, `budget`, `test-integrity`, `mutation-gate`, `
|
|
44
|
+
| Workflow gates | `preamble`, `build-readiness`, `readiness-digest`, `spec-skeleton`, `spec-validate`, `check-acceptance`, `evidence-fresh`, `coverage`, `doubt-coverage`, `budget`, `test-integrity`, `mutation-gate`, `review-integrity`, `footprint`, `reconcile`, `conventions`, `learnings`, `review-fingerprints`, `timeline`, `health`, `progress` | Called by the relevant `rite-*` workflow or shared reply contract. |
|
|
45
45
|
| Workspace utilities | `status`, `snapshot`, `analyze`, `archive-search`, `clarify-return`, `recovery`, `resolve`, `close-out`, `stuck`, `tick-afk`, `ledger`, `profile` | Called by a specific utility/phase when its condition is met. |
|
|
46
46
|
| Low-level completeness API | `readiness`, `seal` | Available for scripts/CI and documented engine use. Feature rites use stricter phase-specific gates (`build-readiness`, `/rite-seal` phase contract) instead of auto-running these weaker aggregate checks. |
|
|
47
47
|
| Install / operator / CI | `install`, `update`, `uninstall`, `doctor`, `migrate`, `validate-pack`, `harness-matrix`, `extensions`, `overrides`, `reviewers`, `hook`, `version` | Called by `npx devrites ...`, `/rite-doctor`, hooks, CI, or a human operator; do not auto-run during feature work just because the command exists. |
|
package/docs/orchestration.md
CHANGED
|
@@ -65,8 +65,7 @@ Each dispatch follows the retained-baseline sequence:
|
|
|
65
65
|
2. The sole wright returns code/tests; the root validates its typed identity and
|
|
66
66
|
exact changed-file set.
|
|
67
67
|
3. `reconcile check` rejects anything outside the root allowlist.
|
|
68
|
-
4. `test-integrity`
|
|
69
|
-
baseline.
|
|
68
|
+
4. `test-integrity` runs against the retained baseline.
|
|
70
69
|
5. After proof and decision checks pass, `reconcile close` retires the private
|
|
71
70
|
window; only then does the root write canonical records.
|
|
72
71
|
|
package/engine/commands.go
CHANGED
|
@@ -34,7 +34,7 @@ func rootModeFor(command string, args []string) rootMode {
|
|
|
34
34
|
case "first-task", "spec-dedupe", "evidence-fresh", "coverage",
|
|
35
35
|
"doubt-coverage", "budget", "preamble", "progress",
|
|
36
36
|
"build-readiness", "readiness-digest", "analyze", "mutation-gate",
|
|
37
|
-
"test-integrity", "review-integrity",
|
|
37
|
+
"test-integrity", "review-integrity",
|
|
38
38
|
"archive-search", "config", "reviewers", "outside-voice", "docs-stale",
|
|
39
39
|
"secret-scan", "lanes", "overrides":
|
|
40
40
|
return rootLenient
|
|
@@ -25,6 +25,7 @@ const (
|
|
|
25
25
|
agentDispatchStateVersion = "devrites-agent-dispatch/v1"
|
|
26
26
|
agentDispatchStateDir = "devrites-agent-dispatch-v1"
|
|
27
27
|
agentDispatchMetadataRole = "devrites-skill-contract-error"
|
|
28
|
+
agentDispatchSkillGuard = "devrites-skill-dispatch-guard"
|
|
28
29
|
maxCodexRolloutLine = 8 << 20
|
|
29
30
|
)
|
|
30
31
|
|
|
@@ -309,22 +310,24 @@ func requiredAgentRolesFromSkill(path string) ([]string, error) {
|
|
|
309
310
|
return roles, nil
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
func requiredRolesInPrompt(root, text string) ([]string, error) {
|
|
313
|
+
func requiredRolesInPrompt(root, text string) ([]string, bool, error) {
|
|
313
314
|
seen := map[string]struct{}{}
|
|
315
|
+
skillInvoked := false
|
|
314
316
|
skillsDir := filepath.Join(filepath.Dir(root), ".agents", "skills")
|
|
315
317
|
for _, match := range skillInvocationRe.FindAllStringSubmatch(text, -1) {
|
|
316
318
|
skill := strings.ToLower(match[1])
|
|
317
319
|
path := filepath.Join(skillsDir, skill, "SKILL.md")
|
|
318
320
|
if !safepath.WithinResolved(path, skillsDir) {
|
|
319
|
-
return nil, fmt.Errorf("unsafe skill path for %s", skill)
|
|
321
|
+
return nil, false, fmt.Errorf("unsafe skill path for %s", skill)
|
|
320
322
|
}
|
|
321
323
|
roles, err := requiredAgentRolesFromSkill(path)
|
|
322
324
|
if os.IsNotExist(err) {
|
|
323
325
|
continue
|
|
324
326
|
}
|
|
325
327
|
if err != nil {
|
|
326
|
-
return nil, fmt.Errorf("%s: %w", skill, err)
|
|
328
|
+
return nil, false, fmt.Errorf("%s: %w", skill, err)
|
|
327
329
|
}
|
|
330
|
+
skillInvoked = true
|
|
328
331
|
for _, role := range roles {
|
|
329
332
|
seen[role] = struct{}{}
|
|
330
333
|
}
|
|
@@ -334,7 +337,7 @@ func requiredRolesInPrompt(root, text string) ([]string, error) {
|
|
|
334
337
|
roles = append(roles, role)
|
|
335
338
|
}
|
|
336
339
|
sort.Strings(roles)
|
|
337
|
-
return roles, nil
|
|
340
|
+
return roles, skillInvoked, nil
|
|
338
341
|
}
|
|
339
342
|
|
|
340
343
|
func expectedAgentType(role string) string {
|
|
@@ -488,6 +491,19 @@ func durableCodexV2DispatchAttempts(
|
|
|
488
491
|
}
|
|
489
492
|
var attempts []*agentDispatchAttempt
|
|
490
493
|
for _, spawn := range spawns {
|
|
494
|
+
if !devritesAgentNameRe.MatchString(spawn.Role) || spawn.Role == agentDispatchSkillGuard {
|
|
495
|
+
if strings.TrimSpace(spawn.Result) != "" {
|
|
496
|
+
return nil, fmt.Errorf("codex V2 completed a default or non-DevRites child during a DevRites skill turn; %s", conditionalDispatchInstruction())
|
|
497
|
+
}
|
|
498
|
+
continue
|
|
499
|
+
}
|
|
500
|
+
armed[spawn.Role] = struct{}{}
|
|
501
|
+
attempt := &agentDispatchAttempt{
|
|
502
|
+
Role: spawn.Role,
|
|
503
|
+
AgentType: spawn.Role,
|
|
504
|
+
Started: true,
|
|
505
|
+
}
|
|
506
|
+
attempts = append(attempts, attempt)
|
|
491
507
|
if strings.TrimSpace(spawn.Result) == "" || !lineBetween(waitLines, spawn.SpawnLine, spawn.ResultLine) {
|
|
492
508
|
continue
|
|
493
509
|
}
|
|
@@ -504,24 +520,17 @@ func durableCodexV2DispatchAttempts(
|
|
|
504
520
|
if childID == "" || strings.TrimSpace(childResult) == "" {
|
|
505
521
|
continue
|
|
506
522
|
}
|
|
507
|
-
|
|
523
|
+
attempt.AgentID = childID
|
|
524
|
+
attempt.Stopped = true
|
|
525
|
+
attempt.Waited = true
|
|
508
526
|
if spawn.Role == "devrites-slice-wright" {
|
|
509
|
-
|
|
510
|
-
if
|
|
527
|
+
attempt.WindowID = currentReconcileWindowID()
|
|
528
|
+
if attempt.WindowID == "" || !reconcileWindowPredates(spawn.SpawnedAt) {
|
|
511
529
|
continue
|
|
512
530
|
}
|
|
513
531
|
}
|
|
514
532
|
sum := sha256.Sum256([]byte(childResult))
|
|
515
|
-
|
|
516
|
-
Role: spawn.Role,
|
|
517
|
-
AgentType: spawn.Role,
|
|
518
|
-
AgentID: childID,
|
|
519
|
-
WindowID: windowID,
|
|
520
|
-
Started: true,
|
|
521
|
-
Stopped: true,
|
|
522
|
-
Waited: true,
|
|
523
|
-
ResultSHA256: hex.EncodeToString(sum[:]),
|
|
524
|
-
})
|
|
533
|
+
attempt.ResultSHA256 = hex.EncodeToString(sum[:])
|
|
525
534
|
}
|
|
526
535
|
return attempts, nil
|
|
527
536
|
}
|
|
@@ -570,6 +579,7 @@ func readCodexV2ParentRollout(
|
|
|
570
579
|
spawns := map[string]*codexV2Spawn{}
|
|
571
580
|
var waitLines []int
|
|
572
581
|
metaSeen := false
|
|
582
|
+
_, guarded := armed[agentDispatchSkillGuard]
|
|
573
583
|
scanner := bufio.NewScanner(file)
|
|
574
584
|
scanner.Buffer(make([]byte, 64<<10), maxCodexRolloutLine)
|
|
575
585
|
for lineNumber := 1; scanner.Scan(); lineNumber++ {
|
|
@@ -604,9 +614,9 @@ func readCodexV2ParentRollout(
|
|
|
604
614
|
if json.Unmarshal([]byte(record.Payload.Arguments), &args) != nil {
|
|
605
615
|
continue
|
|
606
616
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
args.ForkTurns != "none" {
|
|
617
|
+
_, required := armed[args.AgentType]
|
|
618
|
+
if (!required && !guarded) || args.TaskName == "" ||
|
|
619
|
+
strings.Contains(args.TaskName, "/") || args.ForkTurns != "none" {
|
|
610
620
|
continue
|
|
611
621
|
}
|
|
612
622
|
spawnedAt, err := time.Parse(time.RFC3339Nano, record.Timestamp)
|
|
@@ -797,9 +807,23 @@ func roleSatisfied(role, windowID string, attempts []*agentDispatchAttempt) bool
|
|
|
797
807
|
return false
|
|
798
808
|
}
|
|
799
809
|
|
|
810
|
+
func dispatchInstruction(role string) string {
|
|
811
|
+
return fmt.Sprintf(
|
|
812
|
+
"On MultiAgent V2 call spawn_agent with agent_type=%s, a unique task_name, and fork_turns=\"none\" so Codex loads .codex/agents/%s.toml natively. GPT-5.6 V2 may omit agent_type from the visible tool schema even though the runtime accepts it; send agent_type anyway rather than using a default child. On V1 use agent_type=%s with fork_turns=\"none\" and name that role TOML in the message. Wait for the returned child and use its non-empty result. Do not call wait before spawn_agent and do not synthesize the agent result.",
|
|
813
|
+
role, role, expectedAgentType(role),
|
|
814
|
+
)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
func conditionalDispatchInstruction() string {
|
|
818
|
+
return "CONDITIONAL DISPATCH RULE — If this skill reaches a child-agent step, call spawn_agent with the exact named agent_type=devrites-<role> specified by the skill, a unique task_name, and fork_turns=\"none\". GPT-5.6 V2 may omit agent_type from the visible tool schema; send agent_type anyway and never use a default child. Wait for the returned child and use its non-empty result."
|
|
819
|
+
}
|
|
820
|
+
|
|
800
821
|
func incompleteDispatchReason(armed map[string]struct{}, attempts []*agentDispatchAttempt) string {
|
|
801
822
|
roles := make([]string, 0, len(armed))
|
|
802
823
|
for role := range armed {
|
|
824
|
+
if role == agentDispatchSkillGuard {
|
|
825
|
+
continue
|
|
826
|
+
}
|
|
803
827
|
if !roleSatisfied(role, "", attempts) {
|
|
804
828
|
roles = append(roles, role)
|
|
805
829
|
}
|
|
@@ -812,10 +836,7 @@ func incompleteDispatchReason(armed map[string]struct{}, attempts []*agentDispat
|
|
|
812
836
|
if role == agentDispatchMetadataRole {
|
|
813
837
|
return "DevRites could not determine this skill's required agents because its installed required-agent-roles contract is invalid. Reinstall or repair the DevRites skill pack before continuing."
|
|
814
838
|
}
|
|
815
|
-
return fmt.Sprintf(
|
|
816
|
-
"DevRites dispatch for %s is not complete. On MultiAgent V2 call spawn_agent with agent_type=%s, a unique task_name, and fork_turns=\"none\" so Codex loads .codex/agents/%s.toml natively. GPT-5.6 V2 may omit agent_type from the visible tool schema even though the runtime accepts it; send agent_type anyway rather than using a default child. On V1 use agent_type=%s with fork_turns=\"none\" and name that role TOML in the message. Wait for the returned child and use its non-empty result. Do not call wait before spawn_agent and do not synthesize the agent result.",
|
|
817
|
-
role, role, role, expectedAgentType(role),
|
|
818
|
-
)
|
|
839
|
+
return fmt.Sprintf("DevRites dispatch for %s is not complete. %s", role, dispatchInstruction(role))
|
|
819
840
|
}
|
|
820
841
|
|
|
821
842
|
func preToolDeny(h harness.Harness, reason string, stdout, stderr io.Writer) int {
|
|
@@ -888,7 +909,7 @@ func hookAgentDispatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Wri
|
|
|
888
909
|
|
|
889
910
|
switch in.HookEventName {
|
|
890
911
|
case "UserPromptSubmit":
|
|
891
|
-
roles, rolesErr := requiredRolesInPrompt(root, in.Prompt)
|
|
912
|
+
roles, skillInvoked, rolesErr := requiredRolesInPrompt(root, in.Prompt)
|
|
892
913
|
if rolesErr != nil {
|
|
893
914
|
if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
|
|
894
915
|
Event: "armed",
|
|
@@ -899,6 +920,15 @@ func hookAgentDispatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Wri
|
|
|
899
920
|
}
|
|
900
921
|
return stopDispatchBlock(h, "DevRites could not read the required agent roles: "+rolesErr.Error(), stdout, stderr)
|
|
901
922
|
}
|
|
923
|
+
if skillInvoked {
|
|
924
|
+
if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
|
|
925
|
+
Event: "armed",
|
|
926
|
+
TurnID: in.TurnID,
|
|
927
|
+
Role: agentDispatchSkillGuard,
|
|
928
|
+
}); err != nil {
|
|
929
|
+
return stopDispatchBlock(h, "DevRites could not arm the skill dispatch guard: "+err.Error(), stdout, stderr)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
902
932
|
for _, role := range roles {
|
|
903
933
|
if err := appendAgentDispatchEvent(root, in.SessionID, agentDispatchEvent{
|
|
904
934
|
Event: "armed",
|
|
@@ -909,7 +939,21 @@ func hookAgentDispatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Wri
|
|
|
909
939
|
}
|
|
910
940
|
}
|
|
911
941
|
if len(roles) > 0 {
|
|
912
|
-
|
|
942
|
+
instructions := make([]string, 0, len(roles))
|
|
943
|
+
for _, role := range roles {
|
|
944
|
+
instructions = append(instructions, dispatchInstruction(role))
|
|
945
|
+
}
|
|
946
|
+
fmt.Fprintf(
|
|
947
|
+
stdout,
|
|
948
|
+
"MANDATORY DISPATCH THIS TURN — At the skill's dispatch step, execute every required child before finishing or claiming completion; a success phrase is not evidence.\n%s",
|
|
949
|
+
strings.Join(instructions, "\n"),
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
if skillInvoked {
|
|
953
|
+
if len(roles) > 0 {
|
|
954
|
+
fmt.Fprintln(stdout)
|
|
955
|
+
}
|
|
956
|
+
fmt.Fprint(stdout, conditionalDispatchInstruction())
|
|
913
957
|
}
|
|
914
958
|
return exitOK
|
|
915
959
|
|
|
@@ -949,7 +993,8 @@ func hookAgentDispatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Wri
|
|
|
949
993
|
return stopDispatchBlock(h, "DevRites could not verify the required agent dispatch: "+err.Error(), stdout, stderr)
|
|
950
994
|
}
|
|
951
995
|
armed, attempts := dispatchTurnState(events, in.TurnID)
|
|
952
|
-
|
|
996
|
+
_, guarded := armed[agentDispatchSkillGuard]
|
|
997
|
+
if reason := incompleteDispatchReason(armed, attempts); reason != "" || guarded && len(attempts) == 0 {
|
|
953
998
|
durable, durableErr := durableCodexV2DispatchAttempts(
|
|
954
999
|
root, in.SessionID, in.TurnID, armed,
|
|
955
1000
|
)
|
|
@@ -979,10 +1024,17 @@ func hookAgentDispatchPreTool(h harness.Harness, root string, in agentDispatchHo
|
|
|
979
1024
|
if isAgentDispatchTool(in.ToolName) {
|
|
980
1025
|
agentType := in.ToolInput.AgentType
|
|
981
1026
|
if agentType == "" {
|
|
982
|
-
|
|
983
|
-
|
|
1027
|
+
roles := explicitRolesInText(in.ToolInput.Message + "\n" + in.ToolInput.TaskName)
|
|
1028
|
+
if len(roles) == 1 {
|
|
1029
|
+
return preToolDeny(h, "DevRites conditional dispatch requires the exact named specialist. "+dispatchInstruction(roles[0]), stdout, stderr)
|
|
1030
|
+
}
|
|
1031
|
+
if reason := incompleteDispatchReason(armed, attempts); reason != "" {
|
|
1032
|
+
return preToolDeny(h, reason, stdout, stderr)
|
|
984
1033
|
}
|
|
985
|
-
|
|
1034
|
+
if _, guarded := armed[agentDispatchSkillGuard]; guarded {
|
|
1035
|
+
return preToolDeny(h, conditionalDispatchInstruction(), stdout, stderr)
|
|
1036
|
+
}
|
|
1037
|
+
return exitOK
|
|
986
1038
|
}
|
|
987
1039
|
role := ""
|
|
988
1040
|
if strings.HasPrefix(agentType, "devrites-") {
|
|
@@ -995,11 +1047,20 @@ func hookAgentDispatchPreTool(h harness.Harness, root string, in agentDispatchHo
|
|
|
995
1047
|
}
|
|
996
1048
|
if role == "" {
|
|
997
1049
|
if agentType == "explorer" || agentType == "worker" {
|
|
998
|
-
if
|
|
1050
|
+
if reason := incompleteDispatchReason(armed, attempts); reason != "" {
|
|
1051
|
+
return preToolDeny(h, reason, stdout, stderr)
|
|
1052
|
+
}
|
|
1053
|
+
if _, guarded := armed[agentDispatchSkillGuard]; !guarded {
|
|
999
1054
|
return exitOK // Unrelated generic subagent in a DevRites repository.
|
|
1000
1055
|
}
|
|
1001
1056
|
return preToolDeny(h, "DevRites generic dispatch must name exactly one .codex/agents/devrites-<role>.toml contract.", stdout, stderr)
|
|
1002
1057
|
}
|
|
1058
|
+
if reason := incompleteDispatchReason(armed, attempts); reason != "" {
|
|
1059
|
+
return preToolDeny(h, reason, stdout, stderr)
|
|
1060
|
+
}
|
|
1061
|
+
if _, guarded := armed[agentDispatchSkillGuard]; guarded {
|
|
1062
|
+
return preToolDeny(h, conditionalDispatchInstruction(), stdout, stderr)
|
|
1063
|
+
}
|
|
1003
1064
|
return exitOK
|
|
1004
1065
|
}
|
|
1005
1066
|
instructions, contractErr := agentRoleDeveloperInstructions(root, role)
|
|
@@ -1016,7 +1077,7 @@ func hookAgentDispatchPreTool(h harness.Harness, root string, in agentDispatchHo
|
|
|
1016
1077
|
return preToolDeny(h, "devrites-slice-wright requires the generic worker identity and exact wright allowlist.", stdout, stderr)
|
|
1017
1078
|
}
|
|
1018
1079
|
if agentType != "" && agentType != role && agentType != expectedAgentType(role) {
|
|
1019
|
-
return preToolDeny(h, fmt.Sprintf("DevRites role %s requires agent_type=%s or its exposed named role.", role, expectedAgentType(role)), stdout, stderr)
|
|
1080
|
+
return preToolDeny(h, fmt.Sprintf("DevRites role %s requires agent_type=%s or its exposed named role. %s", role, expectedAgentType(role), dispatchInstruction(role)), stdout, stderr)
|
|
1020
1081
|
}
|
|
1021
1082
|
if in.ToolUseID == "" {
|
|
1022
1083
|
return preToolDeny(h, "DevRites cannot bind a spawn without tool_use_id.", stdout, stderr)
|
|
@@ -1057,7 +1118,11 @@ func hookAgentDispatchPreTool(h harness.Harness, root string, in agentDispatchHo
|
|
|
1057
1118
|
}
|
|
1058
1119
|
}
|
|
1059
1120
|
if len(started) == 0 {
|
|
1060
|
-
|
|
1121
|
+
reason := incompleteDispatchReason(armed, attempts)
|
|
1122
|
+
if reason == "" {
|
|
1123
|
+
reason = conditionalDispatchInstruction()
|
|
1124
|
+
}
|
|
1125
|
+
return preToolDeny(h, reason, stdout, stderr)
|
|
1061
1126
|
}
|
|
1062
1127
|
targets := in.ToolInput.ReceiverThreadIDs
|
|
1063
1128
|
if len(targets) == 0 {
|
|
@@ -34,7 +34,7 @@ const (
|
|
|
34
34
|
// clean check, another snapshot re-arms only the dispatch state for a retry while
|
|
35
35
|
// retaining the original source baseline.
|
|
36
36
|
// `check` compares the captured state with the current state and retains the
|
|
37
|
-
// immutable baseline for the later test-integrity
|
|
37
|
+
// immutable baseline for the later test-integrity gate.
|
|
38
38
|
// `close` explicitly ends the window and removes its private artifacts.
|
|
39
39
|
//
|
|
40
40
|
// 0 clean check, snapshot/close completed, or skipped (not a git repo)
|
|
@@ -408,15 +408,13 @@ func TestReconcileCheckFailsClosedWhenObjectDatabaseIsCorrupt(t *testing.T) {
|
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
func
|
|
411
|
+
func TestPostSliceTestIntegrityUsesBaselineUntilClose(t *testing.T) {
|
|
412
412
|
gitRoot := newGitRepo(t)
|
|
413
413
|
root := workspace(t, "feat")
|
|
414
|
-
writeFile(t, filepath.Join(gitRoot, "
|
|
415
|
-
commitAll(t, gitRoot, "
|
|
414
|
+
writeFile(t, filepath.Join(gitRoot, "README.md"), "fixture\n")
|
|
415
|
+
commitAll(t, gitRoot, "baseline")
|
|
416
416
|
|
|
417
|
-
//
|
|
418
|
-
// must not be blamed on the wright by package-existence.
|
|
419
|
-
writeFile(t, filepath.Join(gitRoot, "src/user-delta.ts"), `import ghost from "preexisting-user-package";`+"\n")
|
|
417
|
+
// This user delta predates the slice and must not be blamed on the wright.
|
|
420
418
|
writeFile(t, filepath.Join(gitRoot, "tests/user_delta_test.go"), "package tests\n\nfunc TestUserDelta(t *testing.T) { t.Fatal(\"baseline\") }\n")
|
|
421
419
|
writeWrightAllowlist(t, root, "feat", "seed.go")
|
|
422
420
|
|
|
@@ -432,12 +430,6 @@ func TestPostSliceGatesShareBaselineUntilClose(t *testing.T) {
|
|
|
432
430
|
if code := TestIntegrity(root, []string{"feat"}, &stdout, &stderr); code != 0 {
|
|
433
431
|
t.Fatalf("test-integrity = %d, want 0\nstdout: %s\nstderr: %s", code, stdout.String(), stderr.String())
|
|
434
432
|
}
|
|
435
|
-
stdout.Reset()
|
|
436
|
-
stderr.Reset()
|
|
437
|
-
if code := PackageExistence(root, []string{"feat"}, &stdout, &stderr); code != 0 {
|
|
438
|
-
t.Fatalf("package-existence = %d, want 0\nstdout: %s\nstderr: %s", code, stdout.String(), stderr.String())
|
|
439
|
-
}
|
|
440
|
-
|
|
441
433
|
if code, out := runReconcile(t, root, "close", "feat"); code != 0 {
|
|
442
434
|
t.Fatalf("close = %d, want 0\n%s", code, out)
|
|
443
435
|
}
|
|
@@ -7,8 +7,8 @@ import (
|
|
|
7
7
|
)
|
|
8
8
|
|
|
9
9
|
// gitToplevel returns the absolute path of the git working tree containing dir,
|
|
10
|
-
// or "" when dir is not inside a repository. The git-backed gates
|
|
11
|
-
//
|
|
10
|
+
// or "" when dir is not inside a repository. The git-backed gates
|
|
11
|
+
// (test-integrity and reconcile) skip rather than block when it is empty.
|
|
12
12
|
func gitToplevel(dir string) string {
|
|
13
13
|
out, err := exec.Command("git", "-C", dir, "rev-parse", "--show-toplevel").Output()
|
|
14
14
|
if err != nil {
|
package/engine/main.go
CHANGED
|
@@ -56,7 +56,6 @@ Usage:
|
|
|
56
56
|
devrites-engine mutation-gate [slug] Advisory: detect the mutation runner and scope it
|
|
57
57
|
devrites-engine test-integrity [slug] Gate: no test deleted, skipped, or de-asserted
|
|
58
58
|
devrites-engine review-integrity [slug] Gate: no adversarial review axis is silent (zero findings, no justification)
|
|
59
|
-
devrites-engine package-existence [slug] Gate: every new import is declared in a manifest
|
|
60
59
|
devrites-engine reconcile <sub> [slug] A1 gate: snapshot|check|close the wright's source writes
|
|
61
60
|
devrites-engine resolve <qid> "<ans>" Resolve an open question; keep state.md consistent
|
|
62
61
|
devrites-engine close-out <slug> Archive a shipped feature and clear ACTIVE
|
|
@@ -239,8 +238,6 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
239
238
|
return lib.TestIntegrity(root, args[1:], stdout, stderr)
|
|
240
239
|
case "review-integrity":
|
|
241
240
|
return lib.ReviewIntegrity(root, args[1:], stdout, stderr)
|
|
242
|
-
case "package-existence":
|
|
243
|
-
return lib.PackageExistence(root, args[1:], stdout, stderr)
|
|
244
241
|
case "reconcile":
|
|
245
242
|
return lib.Reconcile(root, args[1:], stdout, stderr)
|
|
246
243
|
case "resolve":
|
|
@@ -45,7 +45,6 @@ func TestRootModeForCoversReadAndWriteSurfaces(t *testing.T) {
|
|
|
45
45
|
{name: "mutation gate", command: "mutation-gate", want: rootLenient},
|
|
46
46
|
{name: "test integrity", command: "test-integrity", want: rootLenient},
|
|
47
47
|
{name: "review integrity", command: "review-integrity", want: rootLenient},
|
|
48
|
-
{name: "package existence", command: "package-existence", want: rootLenient},
|
|
49
48
|
{name: "reconcile snapshot", command: "reconcile", args: []string{"snapshot"}, want: rootStrict},
|
|
50
49
|
{name: "reconcile check", command: "reconcile", args: []string{"check"}, want: rootStrict},
|
|
51
50
|
{name: "reconcile close", command: "reconcile", args: []string{"close"}, want: rootStrict},
|