devlyn-cli 1.15.0 → 2.0.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 (158) hide show
  1. package/AGENTS.md +104 -0
  2. package/CLAUDE.md +135 -21
  3. package/README.md +43 -125
  4. package/benchmark/auto-resolve/BENCHMARK-DESIGN.md +272 -0
  5. package/benchmark/auto-resolve/README.md +114 -0
  6. package/benchmark/auto-resolve/RUBRIC.md +162 -0
  7. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/NOTES.md +30 -0
  8. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/expected.json +68 -0
  9. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/metadata.json +10 -0
  10. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/setup.sh +4 -0
  11. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/spec.md +45 -0
  12. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/task.txt +8 -0
  13. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/NOTES.md +54 -0
  14. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/expected-pair-plan-registry.json +170 -0
  15. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/expected.json +84 -0
  16. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/metadata.json +21 -0
  17. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/pair-plan.sample-fail.json +214 -0
  18. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/pair-plan.sample-pass.json +223 -0
  19. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/setup.sh +5 -0
  20. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/spec.md +56 -0
  21. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/task.txt +14 -0
  22. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/NOTES.md +28 -0
  23. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/expected-pair-plan-registry.json +162 -0
  24. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/expected.json +65 -0
  25. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/metadata.json +19 -0
  26. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/setup.sh +4 -0
  27. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/spec.md +56 -0
  28. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/task.txt +9 -0
  29. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/NOTES.md +40 -0
  30. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/expected.json +57 -0
  31. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/metadata.json +10 -0
  32. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/setup.sh +6 -0
  33. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/spec.md +49 -0
  34. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/task.txt +9 -0
  35. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/NOTES.md +38 -0
  36. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/expected.json +65 -0
  37. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/metadata.json +10 -0
  38. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/setup.sh +55 -0
  39. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/spec.md +49 -0
  40. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/task.txt +7 -0
  41. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/NOTES.md +38 -0
  42. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/expected.json +77 -0
  43. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/metadata.json +10 -0
  44. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/setup.sh +4 -0
  45. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/spec.md +49 -0
  46. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/task.txt +10 -0
  47. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/NOTES.md +50 -0
  48. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/expected.json +76 -0
  49. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/metadata.json +10 -0
  50. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/setup.sh +36 -0
  51. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/spec.md +46 -0
  52. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/task.txt +7 -0
  53. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/NOTES.md +50 -0
  54. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/expected.json +63 -0
  55. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/metadata.json +10 -0
  56. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/setup.sh +4 -0
  57. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/spec.md +48 -0
  58. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/task.txt +1 -0
  59. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/NOTES.md +93 -0
  60. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/expected.json +74 -0
  61. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/metadata.json +10 -0
  62. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/setup.sh +28 -0
  63. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/spec.md +62 -0
  64. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/task.txt +5 -0
  65. package/benchmark/auto-resolve/fixtures/SCHEMA.md +130 -0
  66. package/benchmark/auto-resolve/fixtures/test-repo/README.md +27 -0
  67. package/benchmark/auto-resolve/fixtures/test-repo/bin/cli.js +63 -0
  68. package/benchmark/auto-resolve/fixtures/test-repo/package-lock.json +823 -0
  69. package/benchmark/auto-resolve/fixtures/test-repo/package.json +22 -0
  70. package/benchmark/auto-resolve/fixtures/test-repo/playwright.config.js +17 -0
  71. package/benchmark/auto-resolve/fixtures/test-repo/server/index.js +37 -0
  72. package/benchmark/auto-resolve/fixtures/test-repo/tests/cli.test.js +25 -0
  73. package/benchmark/auto-resolve/fixtures/test-repo/tests/server.test.js +58 -0
  74. package/benchmark/auto-resolve/fixtures/test-repo/web/index.html +37 -0
  75. package/benchmark/auto-resolve/scripts/build-pair-eligible-manifest.py +174 -0
  76. package/benchmark/auto-resolve/scripts/check-f9-artifacts.py +256 -0
  77. package/benchmark/auto-resolve/scripts/compile-report.py +331 -0
  78. package/benchmark/auto-resolve/scripts/iter-0033c-compare.py +552 -0
  79. package/benchmark/auto-resolve/scripts/judge-opus-pass.sh +430 -0
  80. package/benchmark/auto-resolve/scripts/judge.sh +359 -0
  81. package/benchmark/auto-resolve/scripts/oracle-scope-tier-a.py +260 -0
  82. package/benchmark/auto-resolve/scripts/oracle-scope-tier-b.py +274 -0
  83. package/benchmark/auto-resolve/scripts/oracle-test-fidelity.py +328 -0
  84. package/benchmark/auto-resolve/scripts/pair-plan-idgen.py +401 -0
  85. package/benchmark/auto-resolve/scripts/pair-plan-lint.py +468 -0
  86. package/benchmark/auto-resolve/scripts/run-fixture.sh +691 -0
  87. package/benchmark/auto-resolve/scripts/run-iter-0033c.sh +234 -0
  88. package/benchmark/auto-resolve/scripts/run-suite.sh +214 -0
  89. package/benchmark/auto-resolve/scripts/ship-gate.py +222 -0
  90. package/bin/devlyn.js +129 -17
  91. package/config/skills/_shared/adapters/README.md +64 -0
  92. package/config/skills/_shared/adapters/gpt-5-5.md +29 -0
  93. package/config/skills/_shared/adapters/opus-4-7.md +29 -0
  94. package/config/skills/{devlyn:auto-resolve/scripts → _shared}/archive_run.py +26 -0
  95. package/config/skills/_shared/codex-config.md +54 -0
  96. package/config/skills/_shared/codex-monitored.sh +141 -0
  97. package/config/skills/_shared/engine-preflight.md +35 -0
  98. package/config/skills/_shared/expected.schema.json +93 -0
  99. package/config/skills/_shared/pair-plan-schema.md +298 -0
  100. package/config/skills/_shared/runtime-principles.md +110 -0
  101. package/config/skills/_shared/spec-verify-check.py +519 -0
  102. package/config/skills/devlyn:ideate/SKILL.md +99 -429
  103. package/config/skills/devlyn:ideate/references/elicitation.md +97 -0
  104. package/config/skills/devlyn:ideate/references/from-spec-mode.md +54 -0
  105. package/config/skills/devlyn:ideate/references/project-mode.md +76 -0
  106. package/config/skills/devlyn:ideate/references/spec-template.md +102 -0
  107. package/config/skills/devlyn:resolve/SKILL.md +172 -184
  108. package/config/skills/devlyn:resolve/references/free-form-mode.md +68 -0
  109. package/config/skills/devlyn:resolve/references/phases/build-gate.md +45 -0
  110. package/config/skills/devlyn:resolve/references/phases/cleanup.md +39 -0
  111. package/config/skills/devlyn:resolve/references/phases/implement.md +42 -0
  112. package/config/skills/devlyn:resolve/references/phases/plan.md +42 -0
  113. package/config/skills/devlyn:resolve/references/phases/verify.md +69 -0
  114. package/config/skills/devlyn:resolve/references/state-schema.md +106 -0
  115. package/{config/skills → optional-skills}/devlyn:design-system/SKILL.md +1 -0
  116. package/{config/skills → optional-skills}/devlyn:reap/SKILL.md +1 -0
  117. package/{config/skills → optional-skills}/devlyn:team-design-ui/SKILL.md +5 -0
  118. package/package.json +12 -2
  119. package/scripts/lint-skills.sh +431 -0
  120. package/config/skills/devlyn:auto-resolve/SKILL.md +0 -252
  121. package/config/skills/devlyn:auto-resolve/evals/evals.json +0 -21
  122. package/config/skills/devlyn:auto-resolve/evals/task-doctor-subcommand.md +0 -42
  123. package/config/skills/devlyn:auto-resolve/references/build-gate.md +0 -130
  124. package/config/skills/devlyn:auto-resolve/references/engine-routing.md +0 -82
  125. package/config/skills/devlyn:auto-resolve/references/findings-schema.md +0 -103
  126. package/config/skills/devlyn:auto-resolve/references/phases/phase-1-build.md +0 -54
  127. package/config/skills/devlyn:auto-resolve/references/phases/phase-2-evaluate.md +0 -45
  128. package/config/skills/devlyn:auto-resolve/references/phases/phase-3-critic.md +0 -84
  129. package/config/skills/devlyn:auto-resolve/references/pipeline-routing.md +0 -114
  130. package/config/skills/devlyn:auto-resolve/references/pipeline-state.md +0 -201
  131. package/config/skills/devlyn:auto-resolve/scripts/terminal_verdict.py +0 -96
  132. package/config/skills/devlyn:browser-validate/SKILL.md +0 -164
  133. package/config/skills/devlyn:browser-validate/references/flow-testing.md +0 -118
  134. package/config/skills/devlyn:browser-validate/references/tier1-chrome.md +0 -137
  135. package/config/skills/devlyn:browser-validate/references/tier2-playwright.md +0 -195
  136. package/config/skills/devlyn:browser-validate/references/tier3-curl.md +0 -57
  137. package/config/skills/devlyn:clean/SKILL.md +0 -285
  138. package/config/skills/devlyn:design-ui/SKILL.md +0 -351
  139. package/config/skills/devlyn:discover-product/SKILL.md +0 -124
  140. package/config/skills/devlyn:evaluate/SKILL.md +0 -564
  141. package/config/skills/devlyn:feature-spec/SKILL.md +0 -630
  142. package/config/skills/devlyn:ideate/references/challenge-rubric.md +0 -122
  143. package/config/skills/devlyn:ideate/references/codex-critic-template.md +0 -42
  144. package/config/skills/devlyn:ideate/references/templates/item-spec.md +0 -90
  145. package/config/skills/devlyn:implement-ui/SKILL.md +0 -466
  146. package/config/skills/devlyn:preflight/SKILL.md +0 -355
  147. package/config/skills/devlyn:preflight/references/auditors/browser-auditor.md +0 -32
  148. package/config/skills/devlyn:preflight/references/auditors/code-auditor.md +0 -86
  149. package/config/skills/devlyn:preflight/references/auditors/docs-auditor.md +0 -38
  150. package/config/skills/devlyn:product-spec/SKILL.md +0 -603
  151. package/config/skills/devlyn:recommend-features/SKILL.md +0 -286
  152. package/config/skills/devlyn:review/SKILL.md +0 -161
  153. package/config/skills/devlyn:team-resolve/SKILL.md +0 -631
  154. package/config/skills/devlyn:team-review/SKILL.md +0 -493
  155. package/config/skills/devlyn:update-docs/SKILL.md +0 -463
  156. package/config/skills/workflow-routing/SKILL.md +0 -73
  157. /package/{config/skills → optional-skills}/devlyn:reap/scripts/reap.sh +0 -0
  158. /package/{config/skills → optional-skills}/devlyn:reap/scripts/scan.sh +0 -0
@@ -0,0 +1,93 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/fysoul17/devlyn-cli/config/skills/_shared/expected.schema.json",
4
+ "title": "spec.expected.json — mechanical acceptance contract",
5
+ "description": "Load-bearing LLM-agnostic decoupler for the devlyn-cli harness. Defines the machine-readable acceptance criteria a spec ships alongside spec.md. Stable across model upgrades — when Opus 5 / GPT-6 / Qwen / Gemini land, this schema does not move; only the per-model adapter files in _shared/adapters/ do.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "verification_commands": {
10
+ "type": "array",
11
+ "description": "Each command is executed against the post-BUILD code. Each pass/fail contributes to verify_score. At least one entry is required when the spec has any observable runtime check (CLI, test command, HTTP request).",
12
+ "items": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": ["cmd"],
16
+ "properties": {
17
+ "cmd": {
18
+ "type": "string",
19
+ "description": "Shell command, executed via `subprocess.run(..., shell=True)` from the build's working directory.",
20
+ "minLength": 1
21
+ },
22
+ "exit_code": {
23
+ "type": "integer",
24
+ "description": "Required exit code. Default 0 if omitted.",
25
+ "default": 0
26
+ },
27
+ "stdout_contains": {
28
+ "type": "array",
29
+ "description": "Each substring must appear verbatim in (stdout + stderr) for pass.",
30
+ "items": { "type": "string", "minLength": 1 },
31
+ "default": []
32
+ },
33
+ "stdout_not_contains": {
34
+ "type": "array",
35
+ "description": "None of these substrings may appear in (stdout + stderr) for pass.",
36
+ "items": { "type": "string", "minLength": 1 },
37
+ "default": []
38
+ }
39
+ }
40
+ }
41
+ },
42
+ "forbidden_patterns": {
43
+ "type": "array",
44
+ "description": "Regex patterns scanned across diff.patch. Match at severity=disqualifier is a hard-floor fail; match at severity=warning is judge-only critical-finding.",
45
+ "items": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "required": ["pattern", "description", "severity"],
49
+ "properties": {
50
+ "pattern": {
51
+ "type": "string",
52
+ "description": "Python re.search-compatible regex. Anchored implicitly by the surrounding regex, NOT by ^ / $ unless intended.",
53
+ "minLength": 1
54
+ },
55
+ "description": {
56
+ "type": "string",
57
+ "description": "Human-readable explanation of what the pattern catches and why it's forbidden.",
58
+ "minLength": 1
59
+ },
60
+ "files": {
61
+ "type": "array",
62
+ "description": "Optional allow-list of files (substrings of diff --git lines). When present, scan is sliced to hunks touching only these files.",
63
+ "items": { "type": "string", "minLength": 1 },
64
+ "default": []
65
+ },
66
+ "severity": {
67
+ "type": "string",
68
+ "enum": ["disqualifier", "warning"],
69
+ "description": "disqualifier = hard-floor fail (DQ). warning = judge-visible critical-finding only."
70
+ }
71
+ }
72
+ }
73
+ },
74
+ "required_files": {
75
+ "type": "array",
76
+ "description": "Files that must exist after the arm runs.",
77
+ "items": { "type": "string", "minLength": 1 },
78
+ "default": []
79
+ },
80
+ "forbidden_files": {
81
+ "type": "array",
82
+ "description": "Files that must NOT appear in the arm's diff (e.g. tooling artifacts the spec didn't request).",
83
+ "items": { "type": "string", "minLength": 1 },
84
+ "default": []
85
+ },
86
+ "max_deps_added": {
87
+ "type": "integer",
88
+ "description": "Hard cap on new entries under dependencies/devDependencies in package.json. Exceeds → DQ.",
89
+ "minimum": 0,
90
+ "default": 0
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,298 @@
1
+ # Shared — `pair-plan.json` schema (iter-0022 archive)
2
+
3
+ > **Archive header (iter-0034 Phase 4 cutover, 2026-05-04)** — this schema was iter-0022 infrastructure for the now-deleted `/devlyn:auto-resolve` PHASE 0 plan-pair contract. The `/devlyn:resolve` PHASE 1 PLAN at HEAD runs solo (per iter-0033 (C1) PASS evidence + iter-0033g § "CLOSURE"). The schema is preserved here as a design archive: the unblock conditions for re-instating PLAN-pair (per `iterations/0034-phase-4-cutover.md` § "L2 PLAN-pair research-only label") are A — container/sandbox isolation justified by another product need, OR B — production telemetry captures positive evidence of subagent introspection that a PLAN-pair measurement would need to isolate. When either condition fires, this schema (and the associated lint / idgen / preflight tooling under `benchmark/auto-resolve/scripts/`) is the starting point.
4
+
5
+ Single source of truth for `pair-plan.json` and its companion `canonical_id_registry.json` when the architecture re-enters scope. Read this once before editing `pair-plan-idgen.py`, `pair-plan-lint.py`, `pair-plan-preflight.sh`, or any future plan-pair PHASE that consumes `state.plan`.
6
+
7
+ ## Audience (when re-instated)
8
+
9
+ - `benchmark/auto-resolve/scripts/pair-plan-idgen.py` — produces `canonical_id_registry.json` from `expected.json` + checked-in oracle scripts.
10
+ - `benchmark/auto-resolve/scripts/pair-plan-lint.py` — validates a `pair-plan.json` against its registry.
11
+ - `autoresearch/scripts/pair-plan-preflight.sh` — orchestrates solo + pair plan generation against blind-aliased fixtures.
12
+ - A future `/devlyn:resolve` PHASE 1 plan-pair branch (currently solo; gated on unblock A or B above): would accept `--plan-path` / JSON payload, set `state.plan.{mode, path}`, and run lint before IMPLEMENT, mirroring the deleted `devlyn:auto-resolve` PHASE 0 contract.
13
+
14
+ ## File locations and naming (canonical)
15
+
16
+ - Registry per fixture: `benchmark/auto-resolve/fixtures/<F>/expected-pair-plan-registry.json` (committed snapshot for diff-against-baseline; iter-0023 verifies the live idgen output equals this snapshot).
17
+ - Plan produced by preflight: `benchmark/auto-resolve/results/<run_id>/<blind_fixture>/plan-preflight/merged/pair-plan.json`.
18
+ - Plan supplied to a re-instated plan-pair branch by an external caller: any path the user chooses, passed via `--plan-path <path>` (the contract surface is preserved as iter-0022 design archive).
19
+ - The registry filename is `canonical_id_registry.json` for **runtime artifacts** — both inside the bundle dir and in the preflight output root. (HANDOFF.md:280 mentions `canonical-ids.json` for the preflight output dir; that name is deprecated — D4 emits `canonical_id_registry.json` to align with the rest of the toolchain.)
20
+ - The **committed fixture snapshot** is named `expected-pair-plan-registry.json` (one per fixture, under `benchmark/auto-resolve/fixtures/<F>/`) — distinct file name to make snapshots greppable separately from runtime artifacts. iter-0023 verifies the live idgen output equals the committed snapshot for the same fixture.
21
+
22
+ ## `canonical_id_registry.json` shape
23
+
24
+ Top-level wrapper:
25
+
26
+ ```jsonc
27
+ {
28
+ "schema_version": "1",
29
+ "fixture_id": "F2-cli-medium-subcommand",
30
+ "generated_at": "2026-04-29T18:30:00Z",
31
+ "generated_from": {
32
+ "expected_path": "benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/expected.json",
33
+ "expected_sha256": "...", // raw file bytes sha256
34
+ "metadata_path": "benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/metadata.json",
35
+ "metadata_sha256": "...", // raw file bytes sha256
36
+ "oracle_script_shas": {
37
+ "test-fidelity": "...", // raw bytes sha256 of oracle-test-fidelity.py
38
+ "scope-tier-a": "...",
39
+ "scope-tier-b": "..."
40
+ }
41
+ },
42
+ "required_invariants": [
43
+ {
44
+ "id": "...",
45
+ "source_field": "expected.json/forbidden_patterns/0 | expected.json/verification_commands/3 | expected.json/required_files | expected.json/forbidden_files | expected.json/max_deps_added | expected.json/spec_output_files | oracle/<oracle-name>/<category-id>",
46
+ "source_ref": "expected.json:60 | expected.json/verification_commands/0 | oracle-test-fidelity.py",
47
+ "operational_check": "...natural-language description of what the variant must do or must not do...",
48
+ "severity": "disqualifier | hard | flag | warn",
49
+ "authority": "expected.json/forbidden_patterns | expected.json/verification_commands | expected.json/required_files | expected.json/forbidden_files | expected.json/max_deps_added | expected.json/spec_output_files | metadata/oracle-allowlist"
50
+ }
51
+ // ...sorted lexicographically by id
52
+ ]
53
+ }
54
+ ```
55
+
56
+ **Hard rules**:
57
+ - `required_invariants` MUST be sorted lexicographically by `id`. idgen sorts before serializing; lint rejects an unsorted file.
58
+ - All file shas (`expected_sha256`, `metadata_sha256`, `oracle_script_shas.*`) are **raw file bytes sha256** — `sha256(open(path, "rb").read())`. NOT canonical-JSON form. (Canonical form is reserved for the pair-plan pre-stamp hash; see below.)
59
+ - `info`-severity oracle categories are NOT registry entries (e.g. scope-tier-b's `tier-b-reachable` is a positive signal, not an invariant violation).
60
+ - The umbrella oracle category `scope-tier-a:tier-a-violation` is ONE registry entry; the 5 path-glob groups (planning-doc, ci-config, node-modules, test-results-or-coverage, env-secrets) are described inside `operational_check`, not split into 5 entries.
61
+
62
+ **Determinism**: same `(expected.json, metadata.json, oracle scripts)` input → byte-identical `canonical_id_registry.json`. Achieved by:
63
+ - `json.dumps(obj, sort_keys=True, indent=2, ensure_ascii=False)` for the on-disk file.
64
+ - All lists pre-sorted before dumping (registry items by `id`).
65
+ - No timestamps that change run-to-run except `generated_at` — see exemption below.
66
+
67
+ `generated_at` is the ONE volatile field. Lint ignores it for sha-stability checks; lint's determinism check sets `generated_at` to a fixed value before comparing two consecutive idgen runs. (Implementation: idgen accepts `--generated-at <iso8601>` for testing.)
68
+
69
+ ## `pair-plan.json` shape
70
+
71
+ ```jsonc
72
+ {
73
+ "schema_version": "1",
74
+ "plan_status": "final | blocked | draft",
75
+ "planning_mode": "solo | pair",
76
+ "fixture_id": "F2-cli-medium-subcommand", // human label; not authoritative
77
+ "source": {
78
+ "spec_path": "benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/spec.md",
79
+ "spec_sha256": "...", // raw file bytes
80
+ "expected_path": "benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/expected.json",
81
+ "expected_sha256": "...", // raw file bytes (optional only when expected.json absent)
82
+ "rubric_path": "benchmark/auto-resolve/RUBRIC.md",
83
+ "rubric_sha256": "...", // raw file bytes
84
+ "canonical_id_registry_path": "benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/expected-pair-plan-registry.json",
85
+ "canonical_id_registry_sha256": "..." // raw file bytes of the registry file
86
+ },
87
+ "authority_order": [
88
+ "spec.md",
89
+ "expected.json/rubric",
90
+ "phase prompt",
91
+ "model preference"
92
+ ],
93
+ "rounds": [
94
+ {
95
+ "round": 1,
96
+ "claude_draft_sha256": "...", // raw file bytes of the per-round draft artifact
97
+ "codex_draft_sha256": "...",
98
+ "merged_sha256": "...",
99
+ "note": "..."
100
+ }
101
+ // up to 3 rounds; iter-0022 preflight stops at the first round where neither model has new substantive critique
102
+ ],
103
+ "accepted_invariants": [
104
+ {
105
+ "id": "no_silent_catch_return_fallback",
106
+ "paraphrase": "...", // human-readable; informational only, NOT enforced
107
+ "source_refs": ["spec.md:36", "expected.json/forbidden_patterns/0"],
108
+ "operational_check": "BUILD output must not contain `catch[^{]*\\{[^}]*return [^}]*\\}` in bin/cli.js",
109
+ "authority": "expected.json/forbidden_patterns"
110
+ }
111
+ ],
112
+ "rejected_alternatives": [
113
+ {
114
+ "id": "alt_silent_catch_with_log",
115
+ "rationale": "Authority order says expected.json/forbidden_patterns dominates; logging does not change visible-error contract.",
116
+ "conflicts_with_ids": ["no_silent_catch_return_fallback"],
117
+ "claude_stamp": "rejected",
118
+ "codex_stamp": "rejected"
119
+ }
120
+ ],
121
+ "unresolved": [], // MUST be empty in final plans
122
+ "escalated_to_user": [], // populated only during draft / blocked status; final must have user_resolution per item if non-empty
123
+ "model_stamps": {
124
+ "claude": {
125
+ "status": "sign | block",
126
+ "blocked_ids": [],
127
+ "signed_plan_sha256": "...", // canonical pre-stamp sha (see below)
128
+ "model": "claude-opus-4-7",
129
+ "timestamp": "2026-04-29T..."
130
+ },
131
+ "codex": {
132
+ "status": "sign | block",
133
+ "blocked_ids": [],
134
+ "signed_plan_sha256": "...",
135
+ "model": "gpt-5.5",
136
+ "timestamp": "..."
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ ## Severity decoupling (registry vs findings)
143
+
144
+ The registry's `required_invariants[].severity` taxonomy is **metadata for human review only**: `disqualifier | hard | flag | warn`. It is NOT mapped onto the `references/findings-schema.md` taxonomy used by EVAL / CRITIC findings (`CRITICAL | HIGH | MEDIUM | LOW`). When a phase emits a finding for a missed plan invariant, severity is assigned by that phase's own existing severity policy (per `findings-schema.md`), not by reading the registry severity directly. The two taxonomies serve different audiences (registry severity = "how the oracle classifies it"; findings severity = "what the orchestrator should do about it") and are intentionally not coupled in iter-0022.
145
+
146
+ ## Hard rules (lint-enforced)
147
+
148
+ 1. `unresolved.length > 0` → `plan_status` MUST be `blocked` or `draft`. Final accepted plan MUST have `unresolved == []`.
149
+ 2. `escalated_to_user[]` non-empty → each item MUST carry a `user_resolution` field, OR `plan_status` MUST be `blocked` / `draft`.
150
+ 3. Every `accepted_invariants[].id` MUST appear in the registry's `required_invariants[].id` exactly (string match — no paraphrase, no synonym, no new IDs at plan-time). `paraphrase` is informational only.
151
+ 4. **Final-plan coverage**: when `plan_status == "final"`, every registry entry MUST be accounted for in the plan — each `required_invariants[].id` is in `accepted_invariants[].id` OR in some `rejected_alternatives[].conflicts_with_ids[]` OR in `escalated_to_user[].id` OR in `unresolved[].id`. (`draft` and `blocked` plans are NOT subject to full coverage; they may still carry un-decided ids in `unresolved[]` per Rule #1.)
152
+ 5. `authority_order` MUST be the exact 4-string array `["spec.md", "expected.json/rubric", "phase prompt", "model preference"]` (snapshot at iter-0022 ship time; future iters can amend with explicit `schema_version` bump).
153
+ 6. `model_stamps.{claude,codex}.status == "sign"` MUST hold for `plan_status: "final"`. A `block` from either model forces `plan_status` to `blocked` or `draft`.
154
+ 7. `model_stamps.{claude,codex}.signed_plan_sha256` MUST be byte-identical AND MUST equal the canonical pre-stamp sha256 of the file (see "Two sha256 contracts" below).
155
+ 8. `source.{spec_sha256, expected_sha256, rubric_sha256, canonical_id_registry_sha256}` MUST equal the actual raw-bytes sha256 of the referenced files at lint time (catches stale plans against changed sources).
156
+ 9. `source.canonical_id_registry_path` MUST resolve to an existing registry file. lint reads it from this field; if `--registry <path>` is passed on the lint command line, the override wins.
157
+ 10. `planning_mode: "pair"` requires `rounds.length >= 1`. `planning_mode: "solo"` requires `rounds.length == 0` (no merge artifacts).
158
+
159
+ ## Two sha256 contracts (DO NOT CONFLATE)
160
+
161
+ ### Contract A — raw file bytes
162
+
163
+ Used for: every `source.*_sha256` field (spec, expected, rubric, registry), every `generated_from.*_sha256` field in the registry, every `rounds[].*_draft_sha256` and `merged_sha256`.
164
+
165
+ ```python
166
+ import hashlib
167
+ with open(path, "rb") as f:
168
+ sha = hashlib.sha256(f.read()).hexdigest()
169
+ ```
170
+
171
+ No canonicalization. The bytes on disk are what gets hashed. This catches "the plan claims spec.md is sha X but spec.md actually has bytes producing sha Y" drift.
172
+
173
+ ### Contract B — canonical pre-stamp form (pair-plan stamps only)
174
+
175
+ Used for: `model_stamps.claude.signed_plan_sha256` and `model_stamps.codex.signed_plan_sha256`. Both stamps sign **byte-identical** canonical bytes, so both sha values are byte-identical.
176
+
177
+ Algorithm (writers and verifiers MUST implement exactly):
178
+
179
+ ```python
180
+ import json
181
+ import hashlib
182
+ import copy
183
+
184
+ def canonical_pre_stamp_sha256(plan: dict) -> str:
185
+ # Reject duplicate keys when LOADING the plan; this function assumes a clean dict.
186
+ pre = copy.deepcopy(plan)
187
+ pre["model_stamps"] = {} # replace value, keep key
188
+ s = json.dumps(
189
+ pre,
190
+ sort_keys=True,
191
+ separators=(",", ":"),
192
+ ensure_ascii=False,
193
+ allow_nan=False,
194
+ )
195
+ return hashlib.sha256(s.encode("utf-8")).hexdigest()
196
+ ```
197
+
198
+ When LOADING the plan, reject duplicate keys:
199
+
200
+ ```python
201
+ def _strict_pairs(pairs):
202
+ keys = [k for k, _ in pairs]
203
+ if len(keys) != len(set(keys)):
204
+ raise ValueError("duplicate key in pair-plan.json")
205
+ return dict(pairs)
206
+
207
+ with open(path, "r", encoding="utf-8") as f:
208
+ plan = json.load(f, object_pairs_hook=_strict_pairs)
209
+ ```
210
+
211
+ **Why no Unicode normalization**: the canonical form hashes input bytes as-is. Writers and verifiers must agree on input form (NFC recommended for any user-supplied free-text strings, but not enforced — the scheme survives because both sides derive from the same source bytes).
212
+
213
+ **Why no floats**: integer + string serialize byte-stably across implementations. Floats vary (e.g. `1.0` vs `1`). Avoid floats in this schema until a future field absolutely requires one; if added, document the canonical float-printing rule in this file.
214
+
215
+ ## Slug rules for registry IDs (idgen)
216
+
217
+ When an `expected.json` item lacks an explicit `id` field, idgen synthesizes a deterministic slug.
218
+
219
+ ### `forbidden_patterns[i]` slug
220
+
221
+ ```
222
+ forbidden_pattern__<sanitize(description, 60)>__<sanitize(files[0], 30)>
223
+ ```
224
+
225
+ `sanitize(s, max_len)`: lowercase; replace any non-`[a-z0-9]` run with a single `_`; strip leading/trailing `_`; truncate to `max_len` (right-truncate, no hash suffix at this level).
226
+
227
+ If two items in the same `forbidden_patterns[]` array produce the same slug after sanitization, the FIRST one (by source-array index) keeps the bare slug; each subsequent collision appends `__i<index>`. idgen detects this deterministically by walking the array in order.
228
+
229
+ Example F2:
230
+ - `forbidden_patterns[0]` (description="silent catch returning a fallback value — violates no-silent-catches policy", files=["bin/cli.js"]) → `forbidden_pattern__silent_catch_returning_a_fallback_value_violate__bin_cli_js`
231
+ - `forbidden_patterns[1]` (description="@ts-ignore escape hatch", files=["bin/cli.js"]) → `forbidden_pattern__ts_ignore_escape_hatch__bin_cli_js`
232
+
233
+ ### `verification_commands[i]` slug
234
+
235
+ ```
236
+ verification__<sha8(canonical_json(verification_obj))>
237
+ ```
238
+
239
+ `canonical_json(obj)`: same compact form as Contract B (`json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False)`).
240
+ `sha8(s)`: first 8 hex chars of `sha256(s.encode("utf-8"))`.
241
+
242
+ The full verification object is hashed (cmd + exit_code + stdout_contains + stdout_not_contains), so reordering the array does not change the slug. Array-index lives in `source_ref` (`expected.json/verification_commands/<i>`) for human navigation only.
243
+
244
+ ### Other expected.json fields
245
+
246
+ - `required_files`: one registry entry per file path: `required_file__<sanitize(path, 60)>`.
247
+ - `forbidden_files`: same shape: `forbidden_file__<sanitize(path, 60)>`.
248
+ - `max_deps_added`: one registry entry: `max_deps_added__<value>` (e.g. `max_deps_added__0`).
249
+ - `spec_output_files`: one registry entry per path: `spec_output_file__<sanitize(path, 60)>`.
250
+
251
+ ### Oracle category IDs (no slug — fixed strings)
252
+
253
+ Oracle `--list-categories` returns category IDs in the form `<oracle-name>:<finding-type>`. These are stable strings that idgen passes through verbatim into `required_invariants[].id`. Each oracle script defines its own enum; iter-0022 ship snapshot:
254
+
255
+ - `test-fidelity:test-file-deleted`
256
+ - `test-fidelity:test-file-renamed`
257
+ - `test-fidelity:mock-swap`
258
+ - `test-fidelity:assertion-regression`
259
+ - `scope-tier-a:lockfile-deletion`
260
+ - `scope-tier-a:tier-a-violation`
261
+ - `scope-tier-b:scope-unmatched`
262
+
263
+ `scope-tier-b:tier-b-reachable` is `info`-severity and NOT a registry entry.
264
+
265
+ ## metadata.json field for per-fixture oracle allowlist
266
+
267
+ iter-0022 adds one new field to each fixture's `metadata.json`:
268
+
269
+ ```json
270
+ {
271
+ "id": "F2-cli-medium-subcommand",
272
+ // ... existing fields unchanged ...
273
+ "pair_plan_oracle_categories": [
274
+ "test-fidelity:test-file-deleted",
275
+ "test-fidelity:test-file-renamed",
276
+ "test-fidelity:mock-swap",
277
+ "test-fidelity:assertion-regression",
278
+ "scope-tier-a:lockfile-deletion",
279
+ "scope-tier-a:tier-a-violation",
280
+ "scope-tier-b:scope-unmatched"
281
+ ]
282
+ }
283
+ ```
284
+
285
+ Hard rule: idgen filters oracle categories to exactly this allowlist. If the field is missing, idgen treats it as the empty array (no oracle categories registered) — `expected.json`-derived invariants still appear. Schema-version bump if the allowlist semantics change.
286
+
287
+ The runner `run-fixture.sh` reads `timeout_seconds` (line 54) and the report reads `category` (compile-report.py line 76); no other consumer reads metadata.json today, so adding a new field is a pure metadata enrichment with no scoring implication.
288
+
289
+ ## Plan field minimum/maximum policy
290
+
291
+ - A field listed in this schema with no "optional" annotation is REQUIRED.
292
+ - Fields explicitly marked optional: `source.expected_path` / `source.expected_sha256` (only when `expected.json` is genuinely absent — not the case for any current fixture).
293
+ - Unknown extra fields in `pair-plan.json` are NOT rejected by lint (forward-compat), but the canonical pre-stamp sha is computed over the whole object so unknown fields participate in the signature.
294
+ - Unknown extra fields in `canonical_id_registry.json` ARE rejected by lint (idgen owns the registry shape; drift here is a bug).
295
+
296
+ ## Versioning
297
+
298
+ `schema_version` starts at `"1"`. A breaking change to any hard rule above bumps the version and the lint script gains a per-version dispatcher. iter-0022 ships version `1`. Future iters MUST update this file before bumping the version field anywhere else.
@@ -0,0 +1,110 @@
1
+ # Runtime principles — sub-agent contract
2
+
3
+ The runtime contract every sub-agent inside `/devlyn:resolve` (PLAN / IMPLEMENT / BUILD_GATE / CLEANUP / VERIFY) and `/devlyn:ideate` (FRAME / EXPLORE / SPEC / CHALLENGE) must satisfy. Source of truth for sub-agent behavior on user tasks. NOT for autoresearch-loop / harness-developer concerns (see `autoresearch/PRINCIPLES.md`).
4
+
5
+ The four sections below mirror the corresponding CLAUDE.md sections (Subtractive-first editing, Goal-locked execution, No-workaround discipline, Evidence over claim). Each section is wrapped in `<!-- runtime-principles:section=NAME:begin -->` / `:end -->` markers in BOTH this file and CLAUDE.md; lint Check 12 (added in iter-0019.A Step 5) extracts each named block from both files and diffs to detect drift.
6
+
7
+ <!-- runtime-principles:contract:begin -->
8
+ ## Subtractive-first editing — perfection = nothing left to remove
9
+ <!-- runtime-principles:section=subtractive-first:begin -->
10
+
11
+ > "Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away." — Saint-Exupéry. **This is the operating definition of "done" in this repo.** A change is finished when no further line, branch, flag, or doc paragraph can be removed without breaking a learned failure mode. Not before.
12
+
13
+ This rule overrides instinct. LLMs (including you) are trained on corpora that reward elaborate, defensive, "thorough" code — so the default impulse is to add. That impulse is wrong here. Read the rules below as hard tests, not aesthetic preferences. They are not optional, not negotiable, and not satisfiable by writing more careful additions.
14
+
15
+ **Mandatory pre-edit question.** Before writing any change, you must answer in this order:
16
+
17
+ 1. **What can I delete that makes the addition unnecessary?** If the addition becomes redundant after the deletion, ship the deletion alone.
18
+ 2. **What can I delete that makes the addition smaller?** Trim the surrounding accretion before adding.
19
+ 3. **Only then**, what is the minimum addition required?
20
+
21
+ If you skip question 1 or 2, you are violating this rule even if the resulting code looks clean.
22
+
23
+ **Hard tests every edit must pass:**
24
+
25
+ - **Net-negative is the default; pure-addition needs a citation.** A diff that adds N lines and removes 0 must point to a specific cause: a previously-observed failure mode (commit hash, fixture ID, finding ID, user-reported incident), OR an explicit user request / spec requirement that demands new user-visible behavior. The latter is a sufficient citation — do not block legitimate requested additions on the absence of a past failure. What is rejected: vague justifications like "it seems clearer," "for future flexibility," "just in case," "to be safe," "for completeness," "to handle edge cases" — these are the exact phrases that produce accretion.
26
+ - **Delete the line that makes the bug impossible, not the line that catches it.** Defensive wrappers, validation layers, error normalizers, and `try/catch` shells are usually evidence that an upstream contract is unclear. Fix the contract upstream and remove the defenses downstream. The trap: adding the wrapper feels like progress because it makes a test pass. The wrapper is debt; the contract fix is the work. **Scope guard**: if the upstream contract fix is outside the user's stated scope, stop and surface the scope expansion to the user before editing — Goal-locked execution overrides this. The right scope-expansion outcome is "user authorizes the upstream fix" or "user accepts a scoped local fix and a follow-up for upstream"; never silently restructure something the user didn't ask you to.
27
+ - **A new flag, branch, or option is admitting two failures**: (a) the default was wrong, (b) every reader pays attention cost forever. Default-fix-and-delete-flag beats add-flag-with-better-default. The bar for adding a configuration knob is "I have observed two real users with genuinely conflicting needs," not "this might be useful someday."
28
+ - **Doc additions are subject to the same rule.** Before adding a section to any `.md` file (CLAUDE.md, SKILL.md, README, references/), find the now-stale sentence or section the new one supersedes — delete that first. A growing instructions file dilutes the instructions that actually need to be followed; readers (human and LLM) skim long files and miss load-bearing rules.
29
+ - **A "cleaner" refactor that grows line count is not cleaner.** It is a sideways move that increases context, parsing, and review cost. **For refactor-only changes**, line count must drop unless a cited observed failure requires the new shape. **Never delete tests, contracts, public API, comments documenting non-obvious WHY, or user-facing behavior just to win the count** — that is gaming the metric, not honoring the principle. The metric serves complexity reduction; if a deletion would lose information not recoverable from code + commit history, it is the wrong deletion.
30
+ - **Stop adding when no further deletion is possible.** This is the Saint-Exupéry test inverted into a stopping rule: if you have made an addition and you cannot identify anything else that can be removed, examine the addition itself — is part of it still removable? Iterate until the diff is irreducible.
31
+
32
+ **Anti-rationalization clause** — explicitly guarding against LLM-style hedging:
33
+
34
+ - "More explicit is safer" is **not** a justification. Explicitness has a cost in attention and rot. Required-explicit goes in; nice-to-explicit gets cut.
35
+ - "Adding context for future readers" is **not** a justification. Future readers benefit more from shorter files than from explanatory prose. The code and the commit message together carry the why.
36
+ - "Defense-in-depth" is **not** a justification at the harness layer. Two layers that catch the same bug are evidence one of them should be the only layer.
37
+ - If you find yourself writing the phrase "in case" in a comment, code reviewer note, or doc, **stop and re-evaluate** — that phrase predicts an unjustified addition.
38
+
39
+ **Stopping rule.** A change is done when (a) all hypotheses it was meant to close are closed, AND (b) you have attempted at least one further deletion and confirmed it would break something. If you have not tried to delete more, you are not done. If nothing can be deleted to justify the current addition, the addition itself is too large — re-scope or surface the conflict to the user before proceeding.
40
+
41
+ **Never grow surface area silently.** Every accretion-shaped change must be visible: in the commit message, in the iteration file, or in a flagged review. Silent growth is the failure mode this rule exists to prevent.
42
+ <!-- runtime-principles:section=subtractive-first:end -->
43
+
44
+ ## Goal-locked execution — stay on the North Star, do not wander
45
+ <!-- runtime-principles:section=goal-locked:begin -->
46
+
47
+ Even with a North Star defined, work drifts off-course ("산으로 간다" / "삼천포로 빠진다" — going up the wrong mountain instead of forward). The harness must **actively block** this drift at run time, not merely discourage it. The default is ruler-straight execution toward the user's stated goal; any deviation requires explicit justification, not the inverse.
48
+
49
+ This rule exists because LLMs (including you) are trained to be helpful, comprehensive, and thorough — and "helpful" easily becomes "did more than asked." Doing more than asked is not helpfulness; it is scope creep. Read the rules below as hard blocks, not soft preferences.
50
+
51
+ **The five drift patterns you must refuse to execute on:**
52
+
53
+ 1. **Unrequested work.** "While I'm here, I noticed X is broken/ugly/inefficient" → **stop**. The user did not ask for X. If X is a real defect, surface it as a finding, a follow-up suggestion, or an entry in a TODO list — do NOT fix it inside the current change. Mixing unrequested work with requested work is what makes diffs unreviewable and PRs eternal.
54
+ 2. **Tangential cleanup.** "This file looks messy, let me also tidy..." → **stop**. The current task is the only task. Unrelated cleanup is a separate change requiring its own justification, scope, and pre-flight 0 check.
55
+ 3. **Speculative robustness.** "Just adding a check / fallback / handler for the case where..." → **stop**. If the case has not been observed (in production, in tests, in a finding), it does not belong in this change. Defensive code added for unobserved cases is the most common form of accretion debt — it never gets removed because nobody can prove the case never happens.
56
+ 4. **Re-scoping mid-flight.** "Actually, the better way to do this is to also restructure / rename / migrate..." → **stop**. If you discover the requested approach is wrong, surface that to the user with evidence and let them adjudicate. Do NOT silently expand scope. The user's explicit redirect is the only authorization to enlarge a task.
57
+ 5. **Curiosity detours.** "Let me also explore how Y works to understand this better..." → **stop**, unless Y is provably on the goal's critical path. Curiosity-driven exploration is creative-mode; default is execution-mode.
58
+
59
+ **The single drift test before any deviation from the stated goal:** *"Did the user ask for this, OR does the user's stated goal strictly require it?"* If the answer to both is no, do not do it. Surface it as a note (commit message, end-of-turn summary, finding) and continue on the original path.
60
+
61
+ **Creative-mode is the narrow exception, not the default.** Creative-mode applies only when (a) the user explicitly invoked an ideation/exploration surface (`/devlyn:ideate`, optional `/devlyn:design-system`, "let's brainstorm", "explore options for"), OR (b) the goal is genuinely under-specified and a clarifying question is impossible (extremely rare — usually you should ask). For everything else — bug fixes, feature work, refactors, doc updates, pipeline runs, code review, debugging — execution-mode is the default and drift is a defect, not a feature.
62
+
63
+ **Anti-rationalization clause** — explicitly guarding against LLM hedging:
64
+
65
+ - "It's a small extra change" is **not** a justification. Small accretions compound; one of them is always small.
66
+ - "It's related to what they asked for" is **not** a justification. Related ≠ requested. Requested is the only standard.
67
+ - "It would be incomplete without this" is **not** a justification. The user defines completeness, not your sense of it.
68
+ - "I'm being thorough" is **not** a justification. Thoroughness on the requested goal is required; thoroughness extending past the goal is drift.
69
+
70
+ **When in doubt, ask — outside hands-free pipelines.** In interactive sessions a short clarification ("the requested fix touches the X code path; I notice Y also looks broken — should I fix it in this change or surface it as a follow-up?") is always cheaper than a wrong-scope diff. Asking is not a weakness; silently expanding scope is. **Inside hands-free pipelines** (`/devlyn:resolve`, scheduled remote agents, autonomous skill runs) the contract forbids mid-pipeline prompts — there asking is unsafe because there is no user to answer. The substitute is: stay strictly on the requested goal, do not expand scope, and log the question/assumption explicitly in the final report (or `.devlyn/runs/<run_id>/` artifacts) so the user can adjudicate after the run completes. Choosing scope creep over logging-and-staying-on-path is always wrong.
71
+
72
+ **Stopping rule.** A task is done when the user's stated goal is closed AND no off-path work was added. If you find yourself hesitating because "I should also do Z" — Z is drift. Note it for follow-up, do not execute.
73
+ <!-- runtime-principles:section=goal-locked:end -->
74
+
75
+ ## No-workaround discipline
76
+ <!-- runtime-principles:section=no-workaround:begin -->
77
+
78
+ No `any`, no `@ts-ignore`, no silent `catch`, no hardcoded values, no helper scripts that bypass the root cause. Fix root causes; handle errors with user-visible state per the rule above.
79
+
80
+ **Permitted exceptions** (explicitly carved out):
81
+ - CSS fallback fonts, CDN failover, image placeholders — widely-accepted best practices.
82
+ - Codex CLI availability downgrade — the one documented silent fallback in this repo. Fires when the resolved engine is `auto` or `codex` (either via skill default or explicit `--engine` flag) and the Codex CLI is absent. Banner `engine downgraded: codex-unavailable` always prints; verdict identical to `--engine claude`. Any other silent fallback in skills code is a bug — file it against the skill that introduced it.
83
+ <!-- runtime-principles:section=no-workaround:end -->
84
+
85
+ ## Evidence over claim
86
+ <!-- runtime-principles:section=evidence:begin -->
87
+
88
+ Every finding cites concrete evidence. Vague claims are speculation; exclude them.
89
+
90
+ - **Code findings**: `file:line` you have opened.
91
+ - **Missing findings**: explicit "searched X and found no implementation" statement.
92
+ - **Doc findings**: quote of the stale text + section/line reference.
93
+ - **Browser findings**: screenshot reference + URL/route.
94
+
95
+ A finding without one of these forms is excluded. Vague findings produce vague fixes.
96
+ <!-- runtime-principles:section=evidence:end -->
97
+ <!-- runtime-principles:contract:end -->
98
+
99
+ <!-- runtime-principles:consumption:begin -->
100
+ ## Consumption (as of iter-0019.A)
101
+
102
+ **Consumers**:
103
+ - `auto-resolve/SKILL.md` `<harness_principles>` block points here as the contract source. Phase prompt bodies (`phase-1-build.md`, `phase-2-evaluate.md`, `phase-3-critic.md`) inline a compact operational excerpt derived from the contract — phase-specific rule_id mappings + the four section names — not the full text.
104
+ - `preflight/SKILL.md` PHASE 3 (Synthesize) and PHASE 3.5 (RND2) reference this file. Auditor prompts (`code-auditor.md`, `browser-auditor.md`) emit `principle.*` rule_ids derived from the rules above.
105
+
106
+ **Codex routing**: skills that route to Codex (auto-resolve fix-loop on `--engine auto`/`codex`, preflight code-auditor on `--engine auto`/`codex`) MUST inline the contract excerpt directly into the Codex prompt — Codex has no filesystem access under `read-only` sandbox.
107
+
108
+ **Non-consumers**:
109
+ - `ideate/SKILL.md` does NOT consume this file. Ideate is planning-layer; its CHALLENGE rubric (`references/challenge-rubric.md`) covers analogous concerns at planning scope, with deliberate one-shot Codex critic discipline.
110
+ <!-- runtime-principles:consumption:end -->