@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.35 → 0.1.46

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 (128) hide show
  1. package/bin/adl +2 -2
  2. package/bin/reconcile +3 -11
  3. package/bin/setup +15 -15
  4. package/package.json +1 -1
  5. package/packages/agent-hooks/deploy-profile.json +1 -1
  6. package/packages/agent-hooks/package.json +1 -1
  7. package/packages/agent-hooks/src/main.ts +23 -2
  8. package/packages/agent-hooks/src/pre-tool-use/handler.ts +2 -2
  9. package/packages/agent-hooks/src/rules/impl/adl-build-run-role.ts +116 -0
  10. package/packages/agent-hooks/src/rules/impl/adl-git-user-override-block.ts +159 -0
  11. package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +22 -12
  12. package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +9 -9
  13. package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +6 -5
  14. package/packages/agent-hooks/src/rules/impl/adl-run-required.ts +11 -5
  15. package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +18 -5
  16. package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +30 -25
  17. package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
  18. package/packages/agent-hooks/src/rules/impl/git-target.ts +5 -0
  19. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +4 -2
  20. package/packages/agent-hooks/src/rules/impl/tool-frontend.ts +230 -11
  21. package/packages/agent-hooks/src/rules/registry.ts +11 -2
  22. package/packages/agent-hooks/src/rules/shell/git-identity.ts +1 -1
  23. package/packages/agent-hooks/src/rules/shell/invocation.ts +14 -2
  24. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +10 -7
  25. package/packages/agent-settings/assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md +2 -2
  26. package/packages/agent-settings/package.json +1 -1
  27. package/packages/agent-settings/src/deploy/agents-deploy.ts +19 -1
  28. package/packages/agent-settings/src/deploy/index.ts +1 -0
  29. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +4 -6
  30. package/packages/agent-skills/assets/code-review/SKILL.md +84 -66
  31. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +23 -2
  32. package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +1 -1
  33. package/packages/agent-skills/assets/explore-codebase/SKILL.md +3 -2
  34. package/packages/agent-skills/assets/finalize-feature/SKILL.md +63 -0
  35. package/packages/agent-skills/assets/fix-ci/SKILL.md +58 -0
  36. package/packages/agent-skills/assets/handoff/SKILL.md +1 -1
  37. package/packages/agent-skills/assets/implement-feature/SKILL.md +150 -0
  38. package/packages/agent-skills/assets/implement-feature/references/briefs.md +94 -0
  39. package/packages/agent-skills/assets/implement-item/SKILL.md +71 -0
  40. package/packages/agent-skills/assets/mechanics/SKILL.md +38 -0
  41. package/packages/agent-skills/assets/plan-feature/SKILL.md +141 -0
  42. package/packages/agent-skills/assets/prepare-promotion/SKILL.md +90 -0
  43. package/packages/agent-skills/assets/ralplan/SKILL.md +149 -0
  44. package/packages/agent-skills/assets/refactor-safely/SKILL.md +4 -3
  45. package/packages/agent-skills/assets/report-bug/SKILL.md +72 -0
  46. package/packages/agent-skills/assets/review-change/SKILL.md +82 -0
  47. package/packages/agent-skills/assets/review-plan/SKILL.md +124 -0
  48. package/packages/agent-skills/assets/triage-workitems/SKILL.md +99 -0
  49. package/packages/agent-skills/assets/ultragoal/SKILL.md +77 -0
  50. package/packages/agent-skills/assets/validate-design/SKILL.md +64 -0
  51. package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
  52. package/packages/agent-skills/assets/wayfinder/SKILL.md +1 -1
  53. package/packages/agent-skills/assets/workitems-process/SKILL.md +145 -0
  54. package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +227 -0
  55. package/packages/agent-skills/assets/workitems-process/references/evidence.md +74 -0
  56. package/packages/agent-skills/assets/workitems-process/references/workitem-states.md +92 -0
  57. package/packages/agent-skills/assets/write-feature/SKILL.md +94 -0
  58. package/packages/agent-skills/dist/deploy.d.ts +52 -0
  59. package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
  60. package/packages/agent-skills/dist/deploy.js +218 -7
  61. package/packages/agent-skills/dist/deploy.js.map +1 -1
  62. package/packages/agent-skills/dist/index.d.ts +1 -1
  63. package/packages/agent-skills/dist/index.d.ts.map +1 -1
  64. package/packages/agent-skills/dist/index.js +1 -1
  65. package/packages/agent-skills/dist/index.js.map +1 -1
  66. package/packages/agent-skills/package.json +1 -1
  67. package/packages/agent-skills/src/deploy.ts +288 -6
  68. package/packages/agent-skills/src/index.ts +10 -1
  69. package/packages/azdo/README.md +13 -4
  70. package/packages/azdo/bunfig.toml +6 -0
  71. package/packages/azdo/package.json +1 -1
  72. package/packages/azdo/scripts/probe-ado-rules.ts +1 -1
  73. package/packages/azdo/src/activity-log.ts +118 -0
  74. package/packages/azdo/src/azdo-attachments.ts +109 -0
  75. package/packages/azdo/src/azdo-pl.ts +100 -3
  76. package/packages/azdo/src/azdo-pr.ts +376 -17
  77. package/packages/azdo/src/azdo-wit.ts +594 -34
  78. package/packages/azdo/src/cli.ts +119 -12
  79. package/packages/azdo/src/lib/client.ts +95 -17
  80. package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
  81. package/packages/azdo/src/mcp-server.ts +121 -8
  82. package/packages/azdo/src/tool-dispatch.ts +107 -23
  83. package/packages/azdo/src/workflow-ladder.csv +125 -107
  84. package/packages/azdo/src/workflow-rules.ts +254 -97
  85. package/packages/fleet/src/install-adl-package.ts +4 -3
  86. package/packages/repo-controls/README.md +12 -4
  87. package/packages/repo-controls/assets/promote.ts +1 -0
  88. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +5 -5
  89. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +42 -13
  90. package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +5 -5
  91. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +0 -11
  92. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +4 -6
  93. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +15 -0
  94. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +1 -17
  95. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/no-type.md +1 -1
  96. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-experiment.md +1 -1
  97. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-research.md +1 -1
  98. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-key.md +1 -1
  99. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-keys.md +1 -1
  100. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-created.md +1 -1
  101. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-description.md +1 -1
  102. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-last-update.md +1 -1
  103. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-status.md +1 -1
  104. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-title.md +1 -1
  105. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/optional-keys.md +1 -1
  106. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/research-ok.md +1 -1
  107. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +3 -22
  108. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +39 -182
  109. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/fixtures/open-work-cap-clean/plans/features/az601-clean/intent.md +17 -0
  110. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +90 -321
  111. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +31 -5
  112. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +282 -54
  113. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/design.md +35 -0
  114. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/file-map-reference.md +19 -0
  115. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/intent.md +25 -0
  116. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/items/az351-restore-entrypoint.md +25 -0
  117. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/requirements.md +44 -0
  118. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-violation/plans/features/az347-test-coverage/requirements.md +44 -0
  119. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +646 -53
  120. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +3 -3
  121. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +1 -12
  122. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +1 -1
  123. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +1 -1
  124. package/packages/repo-controls/package.json +1 -1
  125. package/packages/repo-controls/src/config.ts +21 -2
  126. package/packages/repo-controls/src/doctor.ts +9 -14
  127. package/packages/repo-controls/src/findings-ratchet.ts +12 -0
  128. package/packages/repo-controls/src/hook.ts +61 -4
@@ -3,12 +3,13 @@ work item it is named for.
3
3
 
4
4
  plans/
5
5
  ├── AGENTS.md
6
- ├── templates/
7
- │ ├── feature.md
8
- │ └── backlog-item.md
6
+ ├── LEDGER.md
9
7
  └── features/
10
8
  └── az<id>-<slug>/
11
- ├── spec.md
9
+ ├── intent.md
10
+ ├── requirements.md
11
+ ├── design.md
12
+ ├── file-map-reference.md
12
13
  └── items/
13
14
  └── az<id>-<slug>.md
14
15
 
@@ -25,6 +26,16 @@ and never renamed. A filename whose id disagrees with its own `work_item:` is
25
26
  the one way this rots, and it rots silently: every tool keeps working, and the
26
27
  file simply describes a different work item than its name claims.
27
28
 
29
+ R1–R7 (ADL process handover, 2026-10-12): `intent.md` is the one required plan
30
+ file — a Feature with only `intent.md` passes until its planning run writes
31
+ `requirements.md`, `design.md` and `file-map-reference.md` (R2, R3, R6). Plan
32
+ files mirror the Feature's five board states; item files keep the twelve item
33
+ states (R1). `GAPS-AND-FINDINGS.md` is retired from the plans root (R7). Every
34
+ plan file and item file carries its required-section list, `requirements.md`
35
+ acceptance criteria are EARS sentences, and every item file names its Feature's
36
+ board URL in `feature:` (R4). The file contracts ship in the workitems-process
37
+ skill's references, so the fix hints point there (R5).
38
+
28
39
  Replaces the earlier `.plans/a_new|b_committed|c_done` lifecycle tree. That
29
40
  design mirrored a work item's state in a directory name, which meant two
30
41
  authorities for one fact. State now lives only on the board and files do not
@@ -58,7 +69,19 @@ class PlansLayout(Check):
58
69
  FEATURES: ClassVar[str] = "features"
59
70
  TEMPLATES: ClassVar[str] = "templates"
60
71
  ITEMS: ClassVar[str] = "items"
61
- SPEC: ClassVar[str] = "spec.md"
72
+
73
+ # The four plan files. `intent.md` is required — it lands with the Feature
74
+ # itself — and the other three arrive with the planning run (R2).
75
+ INTENT: ClassVar[str] = "intent.md"
76
+ REQUIREMENTS: ClassVar[str] = "requirements.md"
77
+ DESIGN: ClassVar[str] = "design.md"
78
+ FILE_MAP: ClassVar[str] = "file-map-reference.md"
79
+ PLAN_FILES: ClassVar[tuple[str, ...]] = (
80
+ INTENT,
81
+ REQUIREMENTS,
82
+ DESIGN,
83
+ FILE_MAP,
84
+ )
62
85
 
63
86
  # Supporting documents, named by ROLE and not by topic. The topic is already
64
87
  # the directory (`az355-channels-humans-and-agents`), so a file called
@@ -67,15 +90,9 @@ class PlansLayout(Check):
67
90
  # you can tell what a file is from its name, in every feature, without opening
68
91
  # it. Adding a name here later is cheap; leaving the set open is how a tree
69
92
  # ends up with HANDOVER.md, handover-v2.md and channels-notes.md side by side.
70
- SUPPORTING: ClassVar[tuple[str, ...]] = ("handoff.md", "intent.md", "review.md")
93
+ SUPPORTING: ClassVar[tuple[str, ...]] = ("handoff.md", "review.md")
71
94
 
72
95
  ROOT_DIRS: ClassVar[tuple[str, ...]] = ("features", "templates")
73
- # `GAPS-AND-FINDINGS.md` sits here because its own rows say where they go:
74
- # each becomes an item file "parented under the same Feature as the plan it
75
- # was found from". It is a register OF plans work, so it belongs beside the
76
- # plans, not at the repository root. `root_shape.REGISTERS` still permits a
77
- # register at root for repositories that keep one there -- that is an
78
- # allowlist, not a requirement, so nothing breaks by leaving.
79
96
  # `LEDGER.md`, not `LEDGER.yaml`. Owner ruling 2026-09-17: the plan-event
80
97
  # ledger is Markdown, because the estate already has ast-grep markdown and
81
98
  # yq frontmatter tooling to scan it, and a YAML register beside a tree of
@@ -86,8 +103,11 @@ class PlansLayout(Check):
86
103
  # Markdown form as "legacy", and this list allowed only the YAML. A
87
104
  # repository could not satisfy all three at once -- a managed repository's conversion
88
105
  # failed here the moment it succeeded elsewhere.
106
+ #
107
+ # R7 (2026-10-12): `GAPS-AND-FINDINGS.md` is retired with the register it
108
+ # held; findings enter only through report-bug.
89
109
  ROOT_ALLOWED_FILES: ClassVar[tuple[str, ...]] = (
90
- "AGENTS.md", "CLAUDE.md", "LEDGER.md", "GAPS-AND-FINDINGS.md",
110
+ "AGENTS.md", "CLAUDE.md", "LEDGER.md",
91
111
  )
92
112
  IGNORED: ClassVar[str] = ".DS_Store"
93
113
  KEEP: ClassVar[str] = ".gitkeep"
@@ -97,23 +117,71 @@ class PlansLayout(Check):
97
117
  ITEM_FILE: ClassVar[re.Pattern[str]] = re.compile(r"^az([0-9]+)-[a-z0-9]+(-[a-z0-9]+)*\.md$")
98
118
 
99
119
  WORK_ITEM_LINE: ClassVar[re.Pattern[str]] = PlanMetadata.WORK_ITEM_LINE
120
+ FEATURE_LINE: ClassVar[re.Pattern[str]] = PlanMetadata.FEATURE_LINE
100
121
  EPIC_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^epic:\s*(.+)$")
101
122
  TYPE_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^type:\s*(.+)$")
102
123
  TITLE_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^title:\s*(.+)$")
103
124
  STATUS_LINE: ClassVar[re.Pattern[str]] = PlanMetadata.STATUS_LINE
104
125
  MODIFIED_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^last_update:\s*(.+)$")
105
- SOURCES_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^sources:\s*$")
106
126
 
107
- ITEM_TYPES: ClassVar[tuple[str, ...]] = ("pbi", "bug")
127
+ # The frontmatter `type:` each plan file declares.
128
+ INTENT_TYPE: ClassVar[str] = "intent"
108
129
  FEATURE_TYPE: ClassVar[str] = "feature"
130
+ DESIGN_TYPE: ClassVar[str] = "design"
131
+ FILE_MAP_TYPE: ClassVar[str] = "file-map"
132
+
133
+ ITEM_TYPES: ClassVar[tuple[str, ...]] = ("pbi", "bug")
109
134
 
110
135
  # The board's own states. `status` here mirrors the work item, not a
111
- # document lifecycle, so `draft` and `stable` are wrong in this tree.
136
+ # document lifecycle, so `draft` and `stable` are wrong in this tree. Plan
137
+ # files mirror the FEATURE's state, item files the ITEM's (R1).
112
138
  #
113
139
  # This gate checks the value is a REAL state; it cannot check the value is
114
140
  # the CURRENT one, because that needs the board. A file whose status has
115
141
  # gone stale is the known cost of keeping the field at all.
116
- STATUSES: ClassVar[tuple[str, ...]] = PlanMetadata.WORK_ITEM_STATUSES
142
+ FEATURE_STATUSES: ClassVar[tuple[str, ...]] = PlanMetadata.FEATURE_STATUSES
143
+ ITEM_STATUSES: ClassVar[tuple[str, ...]] = PlanMetadata.ITEM_STATUSES
144
+
145
+ # Required sections, per process-design.md § Plan files / § Item files
146
+ # (R4). One finding per missing section, named so the author can fix the
147
+ # file without opening the check.
148
+ INTENT_SECTIONS: ClassVar[tuple[str, ...]] = (
149
+ "## Why", "## Outcome", "## Constraints", "## Non-goals",
150
+ )
151
+ REQUIREMENTS_SECTIONS: ClassVar[tuple[str, ...]] = (
152
+ "## Problem", "## Acceptance criteria", "## Scope", "## Predecessors",
153
+ "## Items", "## Open questions for G1", "## Revision log",
154
+ )
155
+ DESIGN_SECTIONS: ClassVar[tuple[str, ...]] = (
156
+ "## Solution", "## Decisions", "## Verification", "## Docs impact",
157
+ "## Size estimate", "## Risks",
158
+ )
159
+ FILE_MAP_SECTIONS: ClassVar[tuple[str, ...]] = ("## Baseline", "## Map")
160
+ # A type: bug item file is a Fixes Bug item file -- Bugs are never planned,
161
+ # so the bugfix shape is the only bug shape there is.
162
+ PBI_SECTIONS: ClassVar[tuple[str, ...]] = ("## What it delivers", "## Verification")
163
+ BUG_SECTIONS: ClassVar[tuple[str, ...]] = (
164
+ "## Current behaviour", "## Expected behaviour",
165
+ "## Behaviour that must not change", "## Responsible Feature",
166
+ "## Predecessors", "## Verification",
167
+ )
168
+
169
+ # EARS (R4): every numbered acceptance criterion is one sentence of the
170
+ # form "THE SYSTEM SHALL …", optionally opened by WHEN, WHILE, IF … THEN
171
+ # or WHERE. One finding per offending criterion.
172
+ # A criterion is an AC-n reference followed by a sentence delimiter; prose
173
+ # that merely mentions AC-1 ("AC-1 is the load-bearing criterion") is not
174
+ # one and is not EARS-checked.
175
+ AC_LINE: ClassVar[re.Pattern[str]] = re.compile(r"^\s*(?:[-*]\s+)?AC-([0-9]+)[.:)]\s+(.+?)\s*$")
176
+ EARS: ClassVar[re.Pattern[str]] = re.compile(
177
+ r"^(?:(?:WHEN|WHILE|WHERE)\b.+?|IF\b.+?THEN\b.+?)?THE\s+SYSTEM\s+SHALL\b",
178
+ re.IGNORECASE,
179
+ )
180
+
181
+ # The board's own states, as `sources:` list entries. `requirements.md`
182
+ # derives from `intent.md` (listed first); `design.md` derives from
183
+ # `requirements.md`.
184
+ SOURCE_ITEM: ClassVar[re.Pattern[str]] = re.compile(r"^\s*-\s+(.+?)\s*$")
117
185
 
118
186
  # A work item reference is the URL you can click, not a bare id: the id is
119
187
  # already in the filename, so repeating it as a number adds nothing, while
@@ -220,7 +288,7 @@ class PlansLayout(Check):
220
288
  # Derived from the constant, never restated. The hardcoded
221
289
  # version had drifted twice over: it named `LEDGER.yaml`,
222
290
  # which is no longer allowed, and omitted
223
- # `GAPS-AND-FINDINGS.md`, which is -- so it was wrong about
291
+ # `GAPS-AND-FINDINGS.md`, which was -- so it was wrong about
224
292
  # both ends of the same four-item list it was describing.
225
293
  f"direct plans files must be one of: {', '.join(self.ROOT_ALLOWED_FILES)}",
226
294
  f"move {entry} into its Feature directory under "
@@ -289,28 +357,45 @@ class PlansLayout(Check):
289
357
  def _feature_contents(
290
358
  self, ctx: Context, feature_dir: str, feature_id: int, given: set[str]
291
359
  ) -> Iterator[Finding]:
292
- spec = f"{feature_dir}/{self.SPEC}"
293
- if not ctx.path(spec).is_file():
360
+ intent = f"{feature_dir}/{self.INTENT}"
361
+ if not ctx.path(intent).is_file():
294
362
  yield self.report(
295
- spec,
296
- "Feature directory must hold a spec.md",
297
- f"add {spec} from plans/templates/feature.md",
363
+ intent,
364
+ "Feature directory must hold an intent.md",
365
+ # R5: the file contracts ship in the workitems-process
366
+ # skill's references, not a per-repo plans/templates/.
367
+ f"add {intent} per the workitems-process skill's "
368
+ "references/ado-and-git.md § 4",
298
369
  )
299
370
  else:
300
- yield from self._check_spec(ctx, spec, feature_id)
371
+ yield from self._check_intent(ctx, intent, feature_id)
372
+
373
+ # requirements.md, design.md and file-map-reference.md arrive with
374
+ # the planning run (R2); each is graded only once it exists.
375
+ for name, check in (
376
+ (self.REQUIREMENTS, self._check_requirements),
377
+ (self.DESIGN, self._check_design),
378
+ (self.FILE_MAP, self._check_file_map),
379
+ ):
380
+ path = f"{feature_dir}/{name}"
381
+ if ctx.path(path).is_file():
382
+ yield from check(ctx, path, feature_id)
301
383
 
302
384
  for entry in sorted(self._under(feature_dir, given)):
303
385
  name = entry.rsplit("/", 1)[-1]
304
- if name in (self.IGNORED, self.SPEC, self.ITEMS):
386
+ if name in (self.IGNORED, *self.PLAN_FILES, self.ITEMS):
305
387
  continue
306
388
  if name in self.SUPPORTING:
307
389
  # A supporting document carries the same frontmatter as everything
308
390
  # else in this tree. Without it a handoff is an undated wall of prose
309
391
  # that names no work item -- readable today, unattributable in a month,
310
392
  # which is the state CHANNELS-HANDOVER.md was in at the repository root.
311
- yield from self._common(ctx, entry, self.text(ctx, entry) or "", feature_id)
393
+ yield from self._common(
394
+ ctx, entry, self.text(ctx, entry) or "", feature_id,
395
+ self.ITEM_STATUSES,
396
+ )
312
397
  continue
313
- allowed = ", ".join((self.SPEC, *self.SUPPORTING, f"{self.ITEMS}/"))
398
+ allowed = ", ".join((*self.PLAN_FILES, *self.SUPPORTING, f"{self.ITEMS}/"))
314
399
  yield self.report(
315
400
  entry,
316
401
  f"a Feature directory holds {allowed} only",
@@ -339,11 +424,34 @@ class PlansLayout(Check):
339
424
  def _field(body: str, pattern: re.Pattern[str]) -> str | None:
340
425
  return PlanMetadata.field(body, pattern)
341
426
 
342
- def _work_item_id(self, body: str) -> tuple[int | None, str | None]:
343
- """The id in `work_item:`, and why it could not be read."""
344
- return PlanMetadata.work_item_id(body)
345
-
346
- def _common(self, ctx: Context, path: str, body: str, expected_id: int) -> Iterator[Finding]:
427
+ def _sources(self, body: str) -> list[str] | None:
428
+ """The frontmatter `sources:` list, or None when the key is absent."""
429
+ lines = body.splitlines()
430
+ if not lines or lines[0].strip() != "---":
431
+ return None
432
+ items: list[str] = []
433
+ in_sources = False
434
+ for line in lines[1:]:
435
+ if line.strip() == "---":
436
+ break
437
+ if in_sources:
438
+ found = self.SOURCE_ITEM.match(line)
439
+ if found:
440
+ items.append(found.group(1).strip().strip("\"'"))
441
+ elif line.strip():
442
+ break
443
+ elif re.match(r"^sources:\s*$", line):
444
+ in_sources = True
445
+ return items if in_sources else None
446
+
447
+ def _common(
448
+ self,
449
+ ctx: Context,
450
+ path: str,
451
+ body: str,
452
+ expected_id: int,
453
+ statuses: tuple[str, ...],
454
+ ) -> Iterator[Finding]:
347
455
  """The frontmatter every document in this tree carries."""
348
456
  if self._field(body, self.TITLE_LINE) is None:
349
457
  yield self.report(path, "missing title", f"add `title:` to {path}")
@@ -353,13 +461,13 @@ class PlansLayout(Check):
353
461
  yield self.report(
354
462
  path,
355
463
  "missing status",
356
- f"add `status:` to {path}, one of: {', '.join(self.STATUSES)}",
464
+ f"add `status:` to {path}, one of: {', '.join(statuses)}",
357
465
  )
358
- elif status not in self.STATUSES:
466
+ elif status not in statuses:
359
467
  yield self.report(
360
468
  path,
361
469
  f"status {status!r} is not a work item state",
362
- f"set status in {path} to one of: {', '.join(self.STATUSES)}; "
470
+ f"set status in {path} to one of: {', '.join(statuses)}; "
363
471
  "it mirrors the board, not a document lifecycle",
364
472
  )
365
473
 
@@ -375,7 +483,7 @@ class PlansLayout(Check):
375
483
  f"set last_update in {path} to YYYY-MM-DDTHH:MM:SSZ",
376
484
  )
377
485
 
378
- found_id, why = self._work_item_id(body)
486
+ found_id, why = PlanMetadata.work_item_id(body)
379
487
  if why == "missing":
380
488
  yield self.report(
381
489
  path,
@@ -398,46 +506,136 @@ class PlansLayout(Check):
398
506
  f"az{found_id}",
399
507
  )
400
508
 
401
- def _check_spec(self, ctx: Context, spec: str, feature_id: int) -> Iterator[Finding]:
402
- body = self.text(ctx, spec) or ""
403
- yield from self._common(ctx, spec, body, feature_id)
509
+ def _sections(
510
+ self, path: str, body: str, required: tuple[str, ...]
511
+ ) -> Iterator[Finding]:
512
+ """One finding per required section the document does not carry."""
513
+ headings = {line.strip() for line in body.splitlines()}
514
+ for section in required:
515
+ if section not in headings:
516
+ yield self.report(
517
+ path,
518
+ f"missing section {section}",
519
+ f"add a '{section}' section to {path}",
520
+ )
404
521
 
522
+ def _check_type(self, path: str, body: str, want: str) -> Iterator[Finding]:
405
523
  declared = self._field(body, self.TYPE_LINE)
406
- if declared != self.FEATURE_TYPE:
524
+ if declared != want:
407
525
  yield self.report(
408
- spec,
409
- f"type must be {self.FEATURE_TYPE!r} (got {declared!r})",
410
- f"set `type: {self.FEATURE_TYPE}` in {spec}",
526
+ path,
527
+ f"type must be {want!r} (got {declared!r})",
528
+ f"set `type: {want}` in {path}",
411
529
  )
412
530
 
531
+ # -------------------------------------------------------------- plan files
532
+
533
+ def _check_intent(
534
+ self, ctx: Context, intent: str, feature_id: int
535
+ ) -> Iterator[Finding]:
536
+ body = self.text(ctx, intent) or ""
537
+ yield from self._common(ctx, intent, body, feature_id, self.FEATURE_STATUSES)
538
+ yield from self._check_type(intent, body, self.INTENT_TYPE)
539
+ yield from self._sections(intent, body, self.INTENT_SECTIONS)
540
+
541
+ def _check_requirements(
542
+ self, ctx: Context, requirements: str, feature_id: int
543
+ ) -> Iterator[Finding]:
544
+ body = self.text(ctx, requirements) or ""
545
+ yield from self._common(
546
+ ctx, requirements, body, feature_id, self.FEATURE_STATUSES
547
+ )
548
+ yield from self._check_type(requirements, body, self.FEATURE_TYPE)
549
+
413
550
  epic = self._field(body, self.EPIC_LINE)
414
551
  if epic is None:
415
552
  yield self.report(
416
- spec,
417
- "spec.md must name the Epic its Feature hangs under",
418
- f"add `epic:` to {spec} as the board URL; it is authored, never "
553
+ requirements,
554
+ "requirements.md must name the Epic its Feature hangs under",
555
+ f"add `epic:` to {requirements} as the board URL; it is authored, never "
419
556
  "tool-created, so a wrong id fails loudly at parenting time",
420
557
  )
421
558
  elif not self.WORK_ITEM_URL.match(epic):
422
559
  yield self.report(
423
- spec,
560
+ requirements,
424
561
  "epic must be a board URL, not a bare id",
425
- f"set epic in {spec} to https://dev.azure.com/.../_workitems/edit/<id>",
562
+ f"set epic in {requirements} to https://dev.azure.com/.../_workitems/edit/<id>",
426
563
  )
427
564
 
428
- if not any(self.SOURCES_LINE.match(line) for line in body.splitlines()):
565
+ sources = self._sources(body)
566
+ if sources is None:
429
567
  yield self.report(
430
- spec,
431
- "spec.md must link the documents it derives from",
432
- f"add a `sources:` list to {spec} pointing at the docs/ pages "
433
- "behind it; a spec with none cannot have its reasoning retraced",
568
+ requirements,
569
+ "requirements.md must link the documents it derives from",
570
+ f"add a `sources:` list to {requirements} pointing at intent.md; "
571
+ "a requirements file with none cannot have its reasoning retraced",
434
572
  )
573
+ elif not sources or "intent.md" not in sources[0]:
574
+ yield self.report(
575
+ requirements,
576
+ "requirements.md must list intent.md first in sources",
577
+ f"set the first `sources:` entry of {requirements} to intent.md",
578
+ )
579
+
580
+ yield from self._sections(requirements, body, self.REQUIREMENTS_SECTIONS)
581
+ yield from self._check_ears(requirements, body)
582
+
583
+ def _check_design(
584
+ self, ctx: Context, design: str, feature_id: int
585
+ ) -> Iterator[Finding]:
586
+ body = self.text(ctx, design) or ""
587
+ yield from self._common(ctx, design, body, feature_id, self.FEATURE_STATUSES)
588
+ yield from self._check_type(design, body, self.DESIGN_TYPE)
589
+
590
+ sources = self._sources(body)
591
+ if sources is None:
592
+ yield self.report(
593
+ design,
594
+ "design.md must link the documents it derives from",
595
+ f"add a `sources:` list to {design} pointing at requirements.md",
596
+ )
597
+ elif not any("requirements.md" in source for source in sources):
598
+ yield self.report(
599
+ design,
600
+ "design.md must list requirements.md in sources",
601
+ f"add requirements.md to the `sources:` list of {design}",
602
+ )
603
+
604
+ yield from self._sections(design, body, self.DESIGN_SECTIONS)
605
+
606
+ def _check_file_map(
607
+ self, ctx: Context, file_map: str, feature_id: int
608
+ ) -> Iterator[Finding]:
609
+ body = self.text(ctx, file_map) or ""
610
+ yield from self._common(ctx, file_map, body, feature_id, self.FEATURE_STATUSES)
611
+ yield from self._check_type(file_map, body, self.FILE_MAP_TYPE)
612
+ yield from self._sections(file_map, body, self.FILE_MAP_SECTIONS)
613
+
614
+ # -------------------------------------------------------------------- EARS
615
+
616
+ def _check_ears(self, requirements: str, body: str) -> Iterator[Finding]:
617
+ for number, line in enumerate(body.splitlines(), start=1):
618
+ found = self.AC_LINE.match(line)
619
+ if not found:
620
+ continue
621
+ if self.EARS.match(found.group(2)):
622
+ continue
623
+ yield self.report(
624
+ requirements,
625
+ f"AC-{found.group(1)} is not an EARS sentence",
626
+ f"rewrite AC-{found.group(1)} in {requirements} as "
627
+ "'THE SYSTEM SHALL …', optionally opened by WHEN / WHILE / "
628
+ "IF … THEN / WHERE",
629
+ line=number,
630
+ )
631
+
632
+ # -------------------------------------------------------------- item files
435
633
 
436
634
  def _check_item(
437
635
  self, ctx: Context, item: str, item_id: int, feature_id: int
438
636
  ) -> Iterator[Finding]:
439
637
  body = self.text(ctx, item) or ""
440
- yield from self._common(ctx, item, body, item_id)
638
+ yield from self._common(ctx, item, body, item_id, self.ITEM_STATUSES)
441
639
 
442
640
  declared = self._field(body, self.TYPE_LINE)
443
641
  if declared is None:
@@ -452,3 +650,33 @@ class PlansLayout(Check):
452
650
  f"type {declared!r} is not pbi or bug",
453
651
  f"set type in {item} to pbi (Product Backlog Item) or bug",
454
652
  )
653
+
654
+ # R4: the item names its Feature's full board URL; the check requires
655
+ # the key and verifies it against the folder id.
656
+ found_feature, why = PlanMetadata.feature_id(body)
657
+ if why == "missing":
658
+ yield self.report(
659
+ item,
660
+ "missing feature link",
661
+ f"add `feature:` to {item} as the Feature's full board URL "
662
+ f"(.../_workitems/edit/{feature_id})",
663
+ )
664
+ elif why:
665
+ yield self.report(
666
+ item,
667
+ "feature must be a board URL, not a bare id",
668
+ f"set feature in {item} to "
669
+ f"https://dev.azure.com/<org>/<project>/_workitems/edit/{feature_id}",
670
+ )
671
+ elif found_feature != feature_id:
672
+ yield self.report(
673
+ item,
674
+ f"feature {found_feature} disagrees with az{feature_id}",
675
+ f"point feature at {feature_id} in {item}, or move the file "
676
+ f"under az{found_feature}",
677
+ )
678
+
679
+ if declared == "bug":
680
+ yield from self._sections(item, body, self.BUG_SECTIONS)
681
+ else:
682
+ yield from self._sections(item, body, self.PBI_SECTIONS)
@@ -0,0 +1,35 @@
1
+ ---
2
+ type: design
3
+ title: "Test coverage"
4
+ status: New
5
+ work_item: https://dev.azure.com/org/proj/_workitems/edit/347
6
+ last_update: 2026-09-15T03:20:00Z
7
+ sources:
8
+ - requirements.md
9
+ ---
10
+
11
+ # Test coverage
12
+
13
+ ## Solution
14
+
15
+ A coverage job per gate.
16
+
17
+ ## Decisions
18
+
19
+ The job runs in CI; the alternative, a local wrapper, was rejected.
20
+
21
+ ## Verification
22
+
23
+ AC-1: the suite covering the coverage job.
24
+
25
+ ## Docs impact
26
+
27
+ `docs/operations/compliance-controls.md`.
28
+
29
+ ## Size estimate
30
+
31
+ One item, two touchpoints, about 40 lines; the job reuses the existing runner.
32
+
33
+ ## Risks
34
+
35
+ Flaky timing -- the job pins the runner version.
@@ -0,0 +1,19 @@
1
+ ---
2
+ type: file-map
3
+ title: "Test coverage"
4
+ status: New
5
+ work_item: https://dev.azure.com/org/proj/_workitems/edit/347
6
+ last_update: 2026-09-15T03:20:00Z
7
+ ---
8
+
9
+ # Test coverage
10
+
11
+ ## Baseline
12
+
13
+ 0000000000000000000000000000000000000000
14
+
15
+ ## Map
16
+
17
+ | Path | Kind | Why | Items |
18
+ | --- | --- | --- | --- |
19
+ | workflows/ | read | the CI layout | 351 |
@@ -0,0 +1,25 @@
1
+ ---
2
+ type: intent
3
+ title: "Test coverage"
4
+ status: New
5
+ work_item: https://dev.azure.com/org/proj/_workitems/edit/347
6
+ last_update: 2026-09-15T03:20:00Z
7
+ ---
8
+
9
+ # Test coverage
10
+
11
+ ## Why
12
+
13
+ Coverage tells us which gates are exercised.
14
+
15
+ ## Outcome
16
+
17
+ Every gate has a measured coverage figure.
18
+
19
+ ## Constraints
20
+
21
+ None.
22
+
23
+ ## Non-goals
24
+
25
+ Branch coverage.
@@ -3,7 +3,32 @@ type: bug
3
3
  title: "Restore the entrypoint"
4
4
  status: Done
5
5
  work_item: https://dev.azure.com/org/proj/_workitems/edit/351
6
+ feature: https://dev.azure.com/org/proj/_workitems/edit/347
6
7
  last_update: 2026-09-15T03:20:00Z
7
8
  ---
8
9
 
9
10
  # Restore the entrypoint
11
+
12
+ ## Current behaviour
13
+
14
+ The entrypoint is gone.
15
+
16
+ ## Expected behaviour
17
+
18
+ THE SYSTEM SHALL restore the entrypoint.
19
+
20
+ ## Behaviour that must not change
21
+
22
+ The CLI still parses arguments.
23
+
24
+ ## Responsible Feature
25
+
26
+ az347 test coverage.
27
+
28
+ ## Predecessors
29
+
30
+ none
31
+
32
+ ## Verification
33
+
34
+ The entrypoint suite passes.
@@ -0,0 +1,44 @@
1
+ ---
2
+ type: feature
3
+ title: "Test coverage"
4
+ status: New
5
+ epic: https://dev.azure.com/org/proj/_workitems/edit/320
6
+ work_item: https://dev.azure.com/org/proj/_workitems/edit/347
7
+ last_update: 2026-09-15T03:20:00Z
8
+ sources:
9
+ - intent.md
10
+ ---
11
+
12
+ # Test coverage
13
+
14
+ ## Problem
15
+
16
+ Gate coverage is unmeasured.
17
+
18
+ ## Acceptance criteria
19
+
20
+ - AC-1: THE SYSTEM SHALL measure coverage per gate.
21
+
22
+ ## Scope
23
+
24
+ In: the measurement job. Non-goals: branch coverage.
25
+
26
+ ## Predecessors
27
+
28
+ none
29
+
30
+ ## Items
31
+
32
+ | Id | Title | Type | Delivers | Verification |
33
+ | --- | --- | --- | --- | --- |
34
+ | 351 | Restore the entrypoint | pbi | AC-1 | suite |
35
+
36
+ ## Open questions for G1
37
+
38
+ none
39
+
40
+ ## Revision log
41
+
42
+ | Rev | Date | Baseline | Trigger | Changes |
43
+ | --- | --- | --- | --- | --- |
44
+ | 1 | 2026-09-15 | — | G1 | initial |