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

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 (84) hide show
  1. package/bin/adl +2 -2
  2. package/package.json +1 -1
  3. package/packages/agent-hooks/deploy-profile.json +1 -1
  4. package/packages/agent-hooks/src/pre-tool-use/handler.ts +2 -2
  5. package/packages/agent-hooks/src/rules/impl/adl-build-run-role.ts +116 -0
  6. package/packages/agent-hooks/src/rules/impl/adl-git-user-override-block.ts +159 -0
  7. package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +12 -6
  8. package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +9 -9
  9. package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +6 -5
  10. package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +15 -14
  11. package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
  12. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
  13. package/packages/agent-hooks/src/rules/registry.ts +11 -2
  14. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +8 -4
  15. package/packages/agent-skills/assets/code-review/SKILL.md +84 -66
  16. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +23 -2
  17. package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +1 -1
  18. package/packages/agent-skills/assets/explore-codebase/SKILL.md +3 -2
  19. package/packages/agent-skills/assets/finalize-feature/SKILL.md +63 -0
  20. package/packages/agent-skills/assets/fix-ci/SKILL.md +58 -0
  21. package/packages/agent-skills/assets/handoff/SKILL.md +1 -1
  22. package/packages/agent-skills/assets/implement-feature/SKILL.md +153 -0
  23. package/packages/agent-skills/assets/implement-feature/references/briefs.md +94 -0
  24. package/packages/agent-skills/assets/implement-item/SKILL.md +71 -0
  25. package/packages/agent-skills/assets/mechanics/SKILL.md +38 -0
  26. package/packages/agent-skills/assets/plan-feature/SKILL.md +141 -0
  27. package/packages/agent-skills/assets/prepare-promotion/SKILL.md +91 -0
  28. package/packages/agent-skills/assets/ralplan/SKILL.md +149 -0
  29. package/packages/agent-skills/assets/refactor-safely/SKILL.md +4 -3
  30. package/packages/agent-skills/assets/report-bug/SKILL.md +72 -0
  31. package/packages/agent-skills/assets/review-change/SKILL.md +82 -0
  32. package/packages/agent-skills/assets/review-plan/SKILL.md +124 -0
  33. package/packages/agent-skills/assets/triage-workitems/SKILL.md +99 -0
  34. package/packages/agent-skills/assets/ultragoal/SKILL.md +77 -0
  35. package/packages/agent-skills/assets/validate-design/SKILL.md +70 -0
  36. package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
  37. package/packages/agent-skills/assets/workitems-process/SKILL.md +147 -0
  38. package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +194 -0
  39. package/packages/agent-skills/assets/workitems-process/references/evidence.md +74 -0
  40. package/packages/agent-skills/assets/workitems-process/references/workitem-states.md +92 -0
  41. package/packages/agent-skills/assets/write-feature/SKILL.md +95 -0
  42. package/packages/agent-skills/dist/deploy.d.ts +7 -0
  43. package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
  44. package/packages/agent-skills/dist/deploy.js +52 -5
  45. package/packages/agent-skills/dist/deploy.js.map +1 -1
  46. package/packages/agent-skills/src/deploy.ts +48 -4
  47. package/packages/azdo/scripts/probe-ado-rules.ts +1 -1
  48. package/packages/azdo/src/activity-log.ts +118 -0
  49. package/packages/azdo/src/azdo-attachments.ts +108 -0
  50. package/packages/azdo/src/azdo-pl.ts +100 -3
  51. package/packages/azdo/src/azdo-pr.ts +194 -11
  52. package/packages/azdo/src/azdo-wit.ts +329 -18
  53. package/packages/azdo/src/cli.ts +42 -2
  54. package/packages/azdo/src/lib/client.ts +7 -1
  55. package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
  56. package/packages/azdo/src/mcp-server.ts +53 -5
  57. package/packages/azdo/src/tool-dispatch.ts +53 -4
  58. package/packages/azdo/src/workflow-ladder.csv +125 -107
  59. package/packages/azdo/src/workflow-rules.ts +254 -97
  60. package/packages/repo-controls/README.md +12 -4
  61. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +5 -5
  62. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +42 -13
  63. package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +5 -5
  64. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +0 -11
  65. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +4 -6
  66. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +15 -0
  67. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +1 -17
  68. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +3 -22
  69. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +39 -182
  70. 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
  71. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +90 -321
  72. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +31 -5
  73. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +282 -54
  74. 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
  75. 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
  76. 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
  77. 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
  78. 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
  79. 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
  80. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +646 -53
  81. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +3 -3
  82. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +1 -12
  83. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +1 -1
  84. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +1 -1
@@ -45,7 +45,7 @@ VIOLATING = (
45
45
  )
46
46
 
47
47
  # In the corpus, never findings: exempt guide names (root and nested),
48
- # the standing catalog/ledger names, non-Markdown assets (including one
48
+ # the standing catalog name, non-Markdown assets (including one
49
49
  # with a deliberately violating stem), and a path that merely lookalikes
50
50
  # the docs/ prefix.
51
51
  NOT_FINDINGS = (
@@ -54,7 +54,6 @@ NOT_FINDINGS = (
54
54
  "docs/CLAUDE.md",
55
55
  "docs/architecture/AGENTS.md",
56
56
  "docs/index.md",
57
- "docs/notes.md",
58
57
  "docs/reference.html",
59
58
  "docs/assets/diagram.png",
60
59
  "docs/assets/UI-CONTRACT.png",
@@ -147,12 +146,11 @@ def test_guide_names_are_exempt_at_any_depth() -> None:
147
146
  assert "docs/architecture/AGENTS.md" not in text
148
147
 
149
148
 
150
- def test_the_standing_page_names_are_silent() -> None:
151
- """`index.md` (the standing catalog name, also scope-excluded) and
152
- `notes.md` (the task ledger) carry conforming stems; both stay silent."""
149
+ def test_the_standing_catalog_name_is_silent() -> None:
150
+ """`index.md` (the standing catalog name, also scope-excluded) carries a
151
+ conforming stem and stays silent."""
153
152
  text = "\n".join(messages_for(corpus_files(CORPUS)))
154
153
  assert "docs/index.md" not in text
155
- assert "docs/notes.md" not in text
156
154
 
157
155
 
158
156
  def test_non_markdown_files_are_not_graded() -> None:
@@ -282,6 +282,21 @@ def test_a_ref_with_an_anchor_onto_a_real_path_passes(tmp_path: pathlib.Path) ->
282
282
  assert verdict.outcome is Outcome.CLEAN, verdict.render()
283
283
 
284
284
 
285
+ # ---------------------------------------------------------------------- R9 (§4a)
286
+
287
+ def test_a_dispatch_order_table_beside_the_event_table_is_not_graded(
288
+ tmp_path: pathlib.Path,
289
+ ) -> None:
290
+ """R9: the LEDGER holds the event table only. Order comes from Feature
291
+ predecessor links on the board, so a leftover `## Next` table -- any table
292
+ whose columns are not the event columns -- is not ledger content and must
293
+ not be graded, reported or required."""
294
+ body = table("| 2026-01-01 | an-entry | docs | something happened | — |")
295
+ body += "\n## Next\n\n| Order | Feature |\n| --- | --- |\n| 1 | az999-next |\n"
296
+ verdict = run_body(tmp_path, body)
297
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
298
+
299
+
285
300
  # -------------------------------------------------------------- the inverted half
286
301
 
287
302
 
@@ -42,7 +42,7 @@ common_optional:
42
42
  - last_update
43
43
  - related
44
44
 
45
- # Five profiles, deliberately (Compliance is disabled, see below).
45
+ # Four profiles, deliberately (Compliance is disabled, see below).
46
46
  #
47
47
  # There were eight. Benchmark, Evaluation, Field Note and Specification declared
48
48
  # trees that do not exist -- docs/research/benchmarks, docs/research/evaluations,
@@ -64,7 +64,6 @@ common_optional:
64
64
  # Architecture what the system is docs/architecture/**
65
65
  # ADR why it is that way docs/architecture/decisions/**, docs/decisions/**
66
66
  # Operations how it is operated docs/operations/**
67
- # Task ledger what each agent task did docs/notes.md
68
67
  # Research what is being found out docs/research/**
69
68
  # (Compliance what we must show docs/compliance/** -- disabled, see below)
70
69
  #
@@ -142,21 +141,6 @@ profiles:
142
141
  - ["Credentials", "Access", "Sign"]
143
142
  - ["Verification", "Evidence", "Check"]
144
143
 
145
- # The task ledger: one append-only list of dated lines, one per task start
146
- # and finish, that the global agent rule requires in every repository
147
- # (docs/notes.md, declared in policy/docs_shape.py). It is an operations
148
- # document, but not a procedure: it has no Purpose/Procedure/Credentials
149
- # sections and no `related` field, so it gets its own profile rather than
150
- # the Operations one above. It is graded for what it really has: OKF
151
- # frontmatter with `sources`, and its `## Lines` section.
152
- - type: Operations
153
- paths:
154
- - docs/notes.md
155
- required:
156
- - sources
157
- headings:
158
- - ["Lines"]
159
-
160
144
  # Research: inputs, findings and experiment plans under docs/research/. No
161
145
  # `type` -- the pages there differ (Research, Experiment) -- and no headings;
162
146
  # see the header. Title, description and status come from common_required.
@@ -5,9 +5,9 @@ Architecture page, and one with an invalid `status` value. Each corpus carries
5
5
  its own cut-down profile register, passed as the `profiles` input override —
6
6
  the packaged production register is not what these two scenarios are about.
7
7
 
8
- The tests below them grade the PACKAGED register: the task-ledger profile, and
9
- the Research profile for `docs/research/**` together with the engine rule it
10
- needs (a profile may declare no `type`, leaving the value unconstrained).
8
+ The tests below them grade the PACKAGED register: the Research profile for
9
+ `docs/research/**` together with the engine rule it needs (a profile may
10
+ declare no `type`, leaving the value unconstrained).
11
11
 
12
12
  The last section grades the closed key set (2026-10-02): a document may carry
13
13
  only the keys its profile admits — `common_required`, `common_optional`, and
@@ -72,25 +72,6 @@ def run_packaged(case: str, only: tuple[str, ...] | None = None) -> object:
72
72
  return runner.run(ROW, directory, given)
73
73
 
74
74
 
75
- def test_the_task_ledger_matches_a_packaged_profile() -> None:
76
- """`docs/notes.md` is the repository's task ledger (the global agent rule:
77
- one line at the start and one at the finish of every task). It carries
78
- OKF frontmatter and a `## Lines` section and nothing else a profile could
79
- demand, so the packaged register must select a profile for it."""
80
- verdict = run_packaged("okf-ledger-clean")
81
- assert verdict.outcome is Outcome.CLEAN, verdict.render()
82
-
83
-
84
- def test_the_task_ledger_profile_requires_the_lines_section() -> None:
85
- """Without this, the clean case above also passes against a profile that
86
- asserts nothing about the ledger."""
87
- verdict = run_packaged("okf-ledger-no-lines")
88
- assert verdict.outcome is Outcome.FINDINGS
89
- assert any("missing one of headings: Lines" in f.message for f in verdict.findings), (
90
- verdict.render()
91
- )
92
-
93
-
94
75
  def messages(verdict: object, relative: str) -> list[str]:
95
76
  """Every message reported against one file, sorted, so a test can assert the
96
77
  EXACT set rather than that some finding somewhere mentioned a field."""
@@ -1,17 +1,20 @@
1
- """LAY-016 — at most ten open plans and at most ten open gaps, by default.
2
-
3
- Plans and linked gap items use the exact board-state vocabulary shared with
4
- LAY-009. An unraised gap and a gap linked to active work both remain open. A
5
- gap linked to terminal work is stale register content and must be deleted.
6
-
7
- The two caps are independent and each is `OPEN_LIMIT` (10) unless the
8
- repository declares its own, as `--declare open-gaps=N` / `--declare
9
- open-plans=N` (`[controls.compliance.limits].open_gaps` / `.open_plans` in its
10
- adl-config.toml). `open_gaps_limit` / `open_plans_limit` resolve them: absent
11
- is the default, and a value that is not a base-10 positive integer is
12
- could-not-run, never a quiet fall back to the default. The engine default is
13
- not raised; a repository states the count it carries today and ratchets it
14
- down.
1
+ """LAY-016 — at most ten open plans, by default.
2
+
3
+ Open plans use the Feature board-state vocabulary shared with LAY-009 (R1):
4
+ a Feature folder's `intent.md` is the one plan file that exists from the
5
+ Feature's first day (R2), so it is the state this cap counts. New, In Progress
6
+ and Awaiting Release are open; Done and Removed are closed.
7
+
8
+ The cap is `OPEN_LIMIT` (10) unless the repository declares its own, as
9
+ `--declare open-plans=N` (`[controls.compliance.limits].open_plans` in its
10
+ adl-config.toml). `open_plans_limit` resolves it: absent is the default, and a
11
+ value that is not a base-10 positive integer is could-not-run, never a quiet
12
+ fall back to the default. The engine default is not raised; a repository
13
+ states the count it carries today and ratchets it down.
14
+
15
+ R7 (ADL process handover, 2026-10-12) retired the register half of this gate
16
+ with `plans/GAPS-AND-FINDINGS.md` itself: the open-gaps cap, the register
17
+ columns and the `open-gaps` declaration are gone. The open-plans cap stays.
15
18
  """
16
19
 
17
20
  from __future__ import annotations
@@ -33,63 +36,30 @@ class OpenWorkCap(Check):
33
36
  inputs: ClassVar[dict[str, str]] = {"plans_root": "<plans>"}
34
37
  whole_scope: ClassVar[bool] = True
35
38
 
36
- # The engine default for each cap. A repository states its own with the
37
- # `open-gaps` / `open-plans` declarations; see `open_gaps_limit`.
39
+ # The engine default. A repository states its own with the
40
+ # `open-plans` declaration; see `open_plans_limit`.
38
41
  OPEN_LIMIT: ClassVar[int] = 10
39
- OPEN_GAPS_DECLARATION: ClassVar[str] = "open-gaps"
40
42
  OPEN_PLANS_DECLARATION: ClassVar[str] = "open-plans"
41
43
  FEATURES: ClassVar[str] = "features"
42
- SPEC: ClassVar[str] = "spec.md"
43
- REGISTER: ClassVar[str] = "GAPS-AND-FINDINGS.md"
44
+ INTENT: ClassVar[str] = "intent.md"
44
45
  IGNORED: ClassVar[str] = ".DS_Store"
45
- COLUMNS: ClassVar[tuple[str, ...]] = (
46
- "Found",
47
- "Finding",
48
- "From plan",
49
- "Item file",
50
- "Work item",
51
- )
52
- DIVIDER_CELL: ClassVar[re.Pattern[str]] = re.compile(r"^:?-{3,}:?$")
53
- WORK_ITEM_LINK: ClassVar[re.Pattern[str]] = re.compile(
54
- r"^\[([0-9]+)\]"
55
- r"\((https://dev\.azure\.com/\S+/_workitems/edit/([0-9]+)/?)\)$"
56
- )
57
- UNRAISED: ClassVar[frozenset[str]] = frozenset(("", "-", "—", "TBD"))
58
- EXCERPT: ClassVar[int] = 60
59
-
60
- def open_gaps_limit(self, ctx: Context) -> int:
61
- """The open-gap cap in force: the declared one, else `OPEN_LIMIT`."""
62
- return self._declared_limit(ctx, self.OPEN_GAPS_DECLARATION, "open_gaps")
63
46
 
64
47
  def open_plans_limit(self, ctx: Context) -> int:
65
48
  """The open-plan cap in force: the declared one, else `OPEN_LIMIT`."""
66
- return self._declared_limit(ctx, self.OPEN_PLANS_DECLARATION, "open_plans")
67
-
68
- def _declared_limit(self, ctx: Context, declaration: str, key: str) -> int:
69
- """One cap's declared value, else `OPEN_LIMIT`.
70
-
71
- ASCII digits only, so `+20`, `1_0`, ` 20` and full-width digits are
72
- refused, which `int()` alone would accept. A declaration that cannot be
73
- read as a positive integer is could-not-run: falling back to the default
74
- would grade the repository against a cap it explicitly replaced.
75
- """
76
- declared = ctx.declarations.get(declaration)
49
+ declared = ctx.declarations.get(self.OPEN_PLANS_DECLARATION)
77
50
  if declared is None:
78
51
  return self.OPEN_LIMIT
79
52
  if re.fullmatch(r"[0-9]+", declared) is None or int(declared) < 1:
80
53
  raise CouldNotRun(
81
- f"declaration {declaration}={declared!r} is not a base-10 positive "
82
- f"integer; set [controls.compliance.limits].{key} to a whole number "
54
+ f"declaration {self.OPEN_PLANS_DECLARATION}={declared!r} is not a "
55
+ "base-10 positive integer; set "
56
+ "[controls.compliance.limits].open_plans to a whole number "
83
57
  "of at least 1"
84
58
  )
85
59
  return int(declared)
86
60
 
87
61
  def graded(self, ctx: Context, files: FileSet) -> Iterable[Finding]:
88
- # Both caps are resolved before either is graded, so an unreadable
89
- # declaration is could-not-run even when the cap it names is not the
90
- # one being exceeded.
91
62
  plans_limit = self.open_plans_limit(ctx)
92
- gaps_limit = self.open_gaps_limit(ctx)
93
63
 
94
64
  yield from self._cap(
95
65
  "plans",
@@ -99,17 +69,6 @@ class OpenWorkCap(Check):
99
69
  f"finish or remove open Features until no more than {plans_limit} remain",
100
70
  )
101
71
 
102
- open_gaps, stale_gaps = self._gaps(ctx, set(files))
103
- yield from stale_gaps
104
- yield from self._cap(
105
- "gaps",
106
- f"{self._plans(ctx)}/{self.REGISTER}",
107
- open_gaps,
108
- gaps_limit,
109
- "finish linked work and delete its gap row, or resolve unraised gaps, "
110
- f"until no more than {gaps_limit} remain",
111
- )
112
-
113
72
  def _cap(
114
73
  self, subject: str, path: str, open_items: list[str], limit: int, fix: str
115
74
  ) -> Iterator[Finding]:
@@ -137,135 +96,33 @@ class OpenWorkCap(Check):
137
96
  for name in sorted(names):
138
97
  if name == self.IGNORED or not ctx.path(f"{features}/{name}").is_dir():
139
98
  continue
140
- spec = f"{features}/{name}/{self.SPEC}"
141
- if spec not in given:
99
+ intent = f"{features}/{name}/{self.INTENT}"
100
+ if not ctx.path(intent).is_file():
101
+ # A Feature whose intent.md does not exist yet is mid-migration;
102
+ # LAY-009 reports the missing file. Counting it either way would
103
+ # grade a state nobody established.
104
+ continue
105
+ if intent not in given:
142
106
  raise CouldNotRun(
143
- f"{spec} is not in the widened Git-visible file set; an on-disk "
144
- "spec outside that set cannot establish plan state"
107
+ f"{intent} is not in the widened Git-visible file set; an on-disk "
108
+ "intent outside that set cannot establish plan state"
145
109
  )
146
- body = self.text(ctx, spec)
110
+ body = self.text(ctx, intent)
147
111
  if body is None:
148
112
  raise CouldNotRun(
149
- f"{spec} is missing; an unreadable plan state cannot be counted"
113
+ f"{intent} is missing; an unreadable plan state cannot be counted"
150
114
  )
151
- status = self._status(body, spec)
152
- if status in PlanMetadata.ACTIVE_STATUSES:
115
+ status = self._status(body, intent)
116
+ if status in PlanMetadata.FEATURE_ACTIVE_STATUSES:
153
117
  open_plans.append(name)
154
118
  return open_plans
155
119
 
156
120
  def _status(self, body: str, path: str) -> str:
157
121
  status = PlanMetadata.field(body, PlanMetadata.STATUS_LINE)
158
- if status not in PlanMetadata.WORK_ITEM_STATUSES:
122
+ if status not in PlanMetadata.FEATURE_STATUSES:
159
123
  shown = "missing" if status is None else repr(status)
160
124
  raise CouldNotRun(
161
125
  f"{path} has {shown} status; expected an exact work item state: "
162
- f"{', '.join(PlanMetadata.WORK_ITEM_STATUSES)}"
126
+ f"{', '.join(PlanMetadata.FEATURE_STATUSES)}"
163
127
  )
164
128
  return status
165
-
166
- def _gaps(self, ctx: Context, given: set[str]) -> tuple[list[str], list[Finding]]:
167
- register = f"{self._plans(ctx)}/{self.REGISTER}"
168
- if register not in given:
169
- return [], []
170
- body = self.text(ctx, register)
171
- if body is None:
172
- raise CouldNotRun(
173
- f"{register} is in the widened Git-visible file set but cannot be read"
174
- )
175
-
176
- rows = [
177
- (number, self._cells(line))
178
- for number, line in enumerate(body.splitlines(), start=1)
179
- if line.lstrip().startswith("|")
180
- ]
181
- if not rows:
182
- raise CouldNotRun(f"{register} exists but holds no Markdown table")
183
- if tuple(rows[0][1]) != self.COLUMNS:
184
- raise CouldNotRun(
185
- f"{register} has columns {rows[0][1]}, expected {list(self.COLUMNS)}"
186
- )
187
-
188
- open_gaps: list[str] = []
189
- stale: list[Finding] = []
190
- for number, cells in rows[1:]:
191
- if len(cells) != len(self.COLUMNS):
192
- raise CouldNotRun(
193
- f"{register}:{number} has {len(cells)} cells; expected "
194
- f"{len(self.COLUMNS)}, so its work-item column cannot be identified"
195
- )
196
- if all(self.DIVIDER_CELL.match(cell) for cell in cells):
197
- continue
198
-
199
- label = f"line {number}: {cells[0]} {self._excerpt(cells[1])}"
200
- work_item_cell = self._plain(cells[4])
201
- if work_item_cell in self.UNRAISED:
202
- open_gaps.append(label)
203
- continue
204
-
205
- linked = self.WORK_ITEM_LINK.match(work_item_cell)
206
- if linked is None or linked.group(1) != linked.group(3):
207
- raise CouldNotRun(
208
- f"{register}:{number} has malformed work item link "
209
- f"{work_item_cell!r}; use [id](https://dev.azure.com/.../_workitems/edit/id)"
210
- )
211
- linked_id = int(linked.group(1))
212
- item_path = self._plain(cells[3])
213
- item_match = re.fullmatch(
214
- rf"{re.escape(self._plans(ctx))}/{self.FEATURES}/"
215
- r"az[0-9]+-[a-z0-9]+(?:-[a-z0-9]+)*/items/"
216
- r"az([0-9]+)-[a-z0-9]+(?:-[a-z0-9]+)*\.md",
217
- item_path,
218
- )
219
- if item_match is None or int(item_match.group(1)) != linked_id:
220
- raise CouldNotRun(
221
- f"{register}:{number} item file {item_path!r} must be a normalized "
222
- f"repository-relative {self._plans(ctx)}/features/az<feature>-<slug>/"
223
- f"items/az{linked_id}-<slug>.md path"
224
- )
225
- if item_path not in given:
226
- raise CouldNotRun(
227
- f"{register}:{number} item file {item_path!r} is not in the widened "
228
- "Git-visible file set; an on-disk path outside that set cannot "
229
- "establish linked work state"
230
- )
231
- item_body = self.text(ctx, item_path)
232
- if item_body is None:
233
- raise CouldNotRun(
234
- f"{register}:{number} names item file {item_path!r}, but it is missing"
235
- )
236
- found_id, why = PlanMetadata.work_item_id(item_body)
237
- if why or found_id != linked_id:
238
- raise CouldNotRun(
239
- f"{item_path} must carry a work_item link for {linked_id}; "
240
- f"found {why or found_id}"
241
- )
242
- status = self._status(item_body, item_path)
243
- if status in PlanMetadata.ACTIVE_STATUSES:
244
- open_gaps.append(label)
245
- elif status in PlanMetadata.TERMINAL_STATUSES:
246
- stale.append(
247
- self.report(
248
- register,
249
- f"closed gap remains in the register at {label}; linked item "
250
- f"{linked_id} is {status}",
251
- f"delete line {number} from {register}; closed gaps no longer remain",
252
- line=number,
253
- )
254
- )
255
- return open_gaps, stale
256
-
257
- @staticmethod
258
- def _plain(value: str) -> str:
259
- stripped = value.strip()
260
- if len(stripped) >= 2 and stripped.startswith("`") and stripped.endswith("`"):
261
- return stripped[1:-1].strip()
262
- return stripped
263
-
264
- @staticmethod
265
- def _cells(line: str) -> list[str]:
266
- inner = line.strip().removeprefix("|").removesuffix("|")
267
- return [cell.strip().replace(r"\|", "|") for cell in re.split(r"(?<!\\)\|", inner)]
268
-
269
- def _excerpt(self, value: str) -> str:
270
- flat = " ".join(value.split())
271
- return flat if len(flat) <= self.EXCERPT else f"{flat[: self.EXCERPT]}…"
@@ -0,0 +1,17 @@
1
+ ---
2
+ type: intent
3
+ title: "A clean feature"
4
+ status: New
5
+ work_item: https://dev.azure.com/org/project/_workitems/edit/601
6
+ last_update: 2026-09-17T00:00:00Z
7
+ ---
8
+
9
+ # A clean feature
10
+
11
+ ## Why
12
+
13
+ ## Outcome
14
+
15
+ ## Constraints
16
+
17
+ ## Non-goals