@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.34 → 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.
- package/bin/adl +2 -2
- package/bin/reconcile +4 -3
- package/package.json +1 -1
- package/packages/agent-hooks/README.md +14 -0
- package/packages/agent-hooks/assets/opencode-plugin.template.ts +140 -104
- package/packages/agent-hooks/deploy-profile.json +1 -1
- package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -18
- package/packages/agent-hooks/src/deploy/render.ts +12 -3
- package/packages/agent-hooks/src/pre-tool-use/handler.ts +2 -2
- package/packages/agent-hooks/src/rules/impl/adl-build-run-role.ts +116 -0
- package/packages/agent-hooks/src/rules/impl/adl-git-user-override-block.ts +159 -0
- package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +12 -6
- package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +9 -9
- package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +6 -5
- package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +15 -14
- package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
- package/packages/agent-hooks/src/rules/impl/tool-native-search.ts +8 -7
- package/packages/agent-hooks/src/rules/registry.ts +11 -2
- package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +9 -5
- package/packages/agent-settings/README.md +10 -4
- package/packages/agent-settings/assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md +40 -0
- package/packages/agent-settings/src/global-agents.ts +43 -8
- package/packages/agent-skills/assets/adl-mechanics/SKILL.md +38 -0
- package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +1 -1
- package/packages/agent-skills/assets/code-review/SKILL.md +84 -66
- package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +23 -2
- package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +1 -1
- package/packages/agent-skills/assets/explore-codebase/SKILL.md +3 -2
- package/packages/agent-skills/assets/finalize-feature/SKILL.md +63 -0
- package/packages/agent-skills/assets/fix-ci/SKILL.md +58 -0
- package/packages/agent-skills/assets/handoff/SKILL.md +1 -1
- package/packages/agent-skills/assets/implement-feature/SKILL.md +153 -0
- package/packages/agent-skills/assets/implement-feature/references/briefs.md +94 -0
- package/packages/agent-skills/assets/implement-item/SKILL.md +71 -0
- package/packages/agent-skills/assets/mechanics/SKILL.md +38 -0
- package/packages/agent-skills/assets/plan-feature/SKILL.md +141 -0
- package/packages/agent-skills/assets/prepare-promotion/SKILL.md +91 -0
- package/packages/agent-skills/assets/ralplan/SKILL.md +149 -0
- package/packages/agent-skills/assets/refactor-safely/SKILL.md +4 -3
- package/packages/agent-skills/assets/report-bug/SKILL.md +72 -0
- package/packages/agent-skills/assets/review-change/SKILL.md +82 -0
- package/packages/agent-skills/assets/review-plan/SKILL.md +124 -0
- package/packages/agent-skills/assets/to-feature/SKILL.md +1 -1
- package/packages/agent-skills/assets/to-workitems/SKILL.md +1 -1
- package/packages/agent-skills/assets/to-workitems/references/AZURE-DEVOPS-HIERARCHY.md +3 -0
- package/packages/agent-skills/assets/triage-workitems/SKILL.md +99 -0
- package/packages/agent-skills/assets/ultragoal/SKILL.md +77 -0
- package/packages/agent-skills/assets/validate-design/SKILL.md +70 -0
- package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
- package/packages/agent-skills/assets/workitems-process/SKILL.md +147 -0
- package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +194 -0
- package/packages/agent-skills/assets/workitems-process/references/evidence.md +74 -0
- package/packages/agent-skills/assets/workitems-process/references/workitem-states.md +92 -0
- package/packages/agent-skills/assets/write-feature/SKILL.md +95 -0
- package/packages/agent-skills/dist/deploy.d.ts +7 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +52 -5
- package/packages/agent-skills/dist/deploy.js.map +1 -1
- package/packages/agent-skills/src/deploy.ts +48 -4
- package/packages/azdo/scripts/probe-ado-rules.ts +1 -1
- package/packages/azdo/src/activity-log.ts +118 -0
- package/packages/azdo/src/azdo-attachments.ts +108 -0
- package/packages/azdo/src/azdo-pl.ts +100 -3
- package/packages/azdo/src/azdo-pr.ts +194 -11
- package/packages/azdo/src/azdo-wit.ts +329 -18
- package/packages/azdo/src/cli.ts +42 -2
- package/packages/azdo/src/lib/agent-identity.ts +1 -1
- package/packages/azdo/src/lib/client.ts +7 -1
- package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
- package/packages/azdo/src/mcp-server.ts +53 -5
- package/packages/azdo/src/tool-dispatch.ts +53 -4
- package/packages/azdo/src/workflow-ladder.csv +125 -107
- package/packages/azdo/src/workflow-rules.ts +254 -97
- package/packages/repo-controls/README.md +24 -11
- package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +5 -5
- package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +42 -13
- package/packages/repo-controls/assets/scaffold/root/AGENTS.md +60 -11
- package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +28 -0
- package/packages/repo-controls/assets/scaffold/root/INVARIANTS.md +17 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +0 -11
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +4 -6
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +1 -17
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +3 -22
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +39 -182
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +90 -321
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +31 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +282 -54
- 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
- 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
- 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
- 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
- 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
- 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
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +646 -53
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +1 -12
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +1 -1
- package/packages/repo-controls/src/cli.ts +23 -5
- package/packages/repo-controls/src/constants.ts +2 -15
- package/packages/repo-controls/src/install.ts +15 -6
|
@@ -1,22 +1,71 @@
|
|
|
1
1
|
# Agent entry point
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
repository. Edit it to describe what this repository is; the sections below
|
|
5
|
-
describe the layout the adl repo controls enforce.
|
|
3
|
+
First file a human or agent reads here. Keep it short.
|
|
6
4
|
|
|
7
|
-
##
|
|
5
|
+
## What this repository is
|
|
6
|
+
- <!-- Owner: one or two lines on what this repo is and who uses it. -->
|
|
7
|
+
|
|
8
|
+
## Layout (paths as declared in `[controls.compliance.trees]`)
|
|
8
9
|
|
|
9
10
|
| Path | What lives there |
|
|
10
11
|
| --- | --- |
|
|
11
12
|
| `docs/` | reference documentation, see `docs/AGENTS.md` |
|
|
12
13
|
| `plans/` | Features and the PBIs/Bugs under them, see `plans/AGENTS.md` |
|
|
13
14
|
| `workflows/` | CI pipelines and build/release scripts, see `workflows/AGENTS.md` |
|
|
14
|
-
| `.workflows/` | managed hook stubs and lock
|
|
15
|
-
|
|
16
|
-
## Rules
|
|
15
|
+
| `.workflows/` | managed hook stubs and lock; rebuilt by `adl repo controls install`, never hand-edit |
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- `
|
|
17
|
+
## Where to look next
|
|
18
|
+
- `CONTRIBUTING.md`: plans and work items, commits and PRs, release flow, tasks ledger.
|
|
19
|
+
- `INVARIANTS.md`: what must never regress, how each is verified, protected paths.
|
|
20
|
+
- Graded on every commit by `adl repo controls compliance run`; enabled gates are `[controls.compliance].enabled` in `adl-config.toml`. A failing gate is fixed, not skipped.
|
|
22
21
|
- Work-item status lives on the board, never in a file.
|
|
22
|
+
|
|
23
|
+
## Keep this guidance alive
|
|
24
|
+
- **When:** you were corrected by the owner, a gate or test failed for a reason not written here, or you learned a rule the hard way.
|
|
25
|
+
- **Where (one place only):**
|
|
26
|
+
- convention or workflow → `CONTRIBUTING.md`
|
|
27
|
+
- must-never-regress rule → `INVARIANTS.md`, only with its verifying gate/test/command; no check, no invariant
|
|
28
|
+
- orientation (what lives where) → `AGENTS.md`
|
|
29
|
+
- folder-specific → that folder's `AGENTS.md`
|
|
30
|
+
- applies to every repository → tell the owner; never edit global files
|
|
31
|
+
- **How:** put the edit in the same commit/PR that taught you it; edit the existing bullet instead of adding a near-duplicate; one bullet, at most 2 lines.
|
|
32
|
+
- **Never record:** secrets, ids/URLs of one-off items, session or progress state, unverified claims, rules a gate already enforces and explains in its own message.
|
|
33
|
+
|
|
34
|
+
## Working defaults for code
|
|
35
|
+
Tarun's global instructions cover communication, truthfulness, verification, scope and delegation. These are the coding defaults. Edit them for this repository, except "Truthfulness and verification in code", which a repository may add to but never relax.
|
|
36
|
+
|
|
37
|
+
### Truthfulness and verification in code
|
|
38
|
+
- No `|| true`, no swallowed errors, no skipped gates.
|
|
39
|
+
- Negative claims need source-level proof: grep the implementation, not a tool listing. Search the whole package first, then narrow.
|
|
40
|
+
- Before reporting what the repository contains, ask the remote (`git show origin/<branch>:<path>`); before reporting a shipped tool's behaviour, ask the installed binary.
|
|
41
|
+
- Name the ref. Where a change is in flight, two branches answer differently and both are right about themselves; say which one you asked.
|
|
42
|
+
- Deployed is not running: a long-lived server or daemon keeps the code it started with. When a fix is installed and behaviour has not changed, restart the process first.
|
|
43
|
+
|
|
44
|
+
### Checkouts
|
|
45
|
+
- Commit rules: `CONTRIBUTING.md`.
|
|
46
|
+
- At most 3 checkouts per repository, including the main folder; git refuses the 4th and names the command to use. Reconcile old worktrees first (merge, then `git worktree remove <path>`).
|
|
47
|
+
- Prefer the one checkout; parallel subagents share it, each with its own write scope. Only the orchestrator commits.
|
|
48
|
+
|
|
49
|
+
### Tests
|
|
50
|
+
- For features, bugfixes and non-trivial refactors: tests first, confirmed failing for the right reason; then minimal code to pass; then a separate review pass.
|
|
51
|
+
- Every code change ships a new test. Exempt: pure config, docs, trivial one-liners.
|
|
52
|
+
- A test changes only when the intended behaviour changes. Don't loosen, skip or delete an assertion, or set an expectation to whatever the code returns, to hide a failure. Red is honest.
|
|
53
|
+
- When a change deliberately alters behaviour, rework the tests that assert the old behaviour in the same change; name each one and the decision that changed it.
|
|
54
|
+
|
|
55
|
+
### Fixing things
|
|
56
|
+
- Fix at the source — the primitive, token or shared layer — so it propagates everywhere, not a narrow downstream patch.
|
|
57
|
+
- Consult official docs before coding against an unfamiliar SDK, framework or API.
|
|
58
|
+
|
|
59
|
+
### Context tooling
|
|
60
|
+
- Match the tool to the question (when installed; otherwise `rg`):
|
|
61
|
+
- Orient in unfamiliar code → `crg architecture`, `crg communities`.
|
|
62
|
+
- Symbols and semantic code queries → `crg search "<text>"`.
|
|
63
|
+
- Callers, callees, blast radius → `crg query callers_of|callees_of|tests_for|importers_of <target>`, `crg impact`.
|
|
64
|
+
- Unreferenced code → `crg dead-code`.
|
|
65
|
+
- A code shape rather than a string → `ast-grep`.
|
|
66
|
+
- Prose in documentation folders (`docs/`, `plans/`) → `search <text>` (hybrid semantic + keyword).
|
|
67
|
+
- Literal text or regex → `rg`. Never `grep`; never `find | grep`, which filters file names and misses the file whose contents match.
|
|
68
|
+
- Files by name or tree → the host's native file-listing tools.
|
|
69
|
+
|
|
70
|
+
### Temporary files
|
|
71
|
+
- Scratch and throwaway work goes in `.scratchpad/` at the repository root, never `/tmp` or `/private/tmp`. Create it if missing; it is gitignored. Real deliverables go to their proper home.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
How to contribute to THIS repository. Orientation is in `AGENTS.md`; what must never regress, and the protected paths, are in `INVARIANTS.md`. Tree paths are those declared in `[controls.compliance.trees]`; `plans/` is the default.
|
|
4
|
+
|
|
5
|
+
## Plans and work items
|
|
6
|
+
- A plan is a Feature; a backlog item is a PBI or a Bug. One file each; the board is the only place their state lives.
|
|
7
|
+
- Layout: `plans/features/az<feature id>-<slug>/` with `intent.md`, `requirements.md` (plan files `design.md`/`file-map-reference.md` optional), items under `items/az<id>-<slug>.md`.
|
|
8
|
+
- The id is the identity: allocate the item on the board first, then name the file. Until then it is a row in the plan's `## Items` table.
|
|
9
|
+
- Nothing moves between directories; the plan text is the work item's fields.
|
|
10
|
+
- An item names its `feature:` as the Feature's full board URL; a gap found along the way is filed through `report-bug`, never a register file.
|
|
11
|
+
|
|
12
|
+
## Commits and PRs
|
|
13
|
+
- Each commit is independently green and on one topic. Never `git stash`. Green wins over one topic, and say so.
|
|
14
|
+
- Subject: `<type>(<scope>): <lowercase subject>`; types `feat|fix|docs|refactor|perf|chore`; no `!`.
|
|
15
|
+
- Branch: `<type>/<kebab>`, same types, not configurable.
|
|
16
|
+
- `release:` is reserved for the promotion PR the pipeline raises; never write one by hand.
|
|
17
|
+
- A PR body needs non-empty `## Summary`, `## Why` and `## Changes`; the title follows the subject grammar.
|
|
18
|
+
|
|
19
|
+
## Release flow
|
|
20
|
+
- `<type>/<kebab>` branch → PR → `integration` (auto-completes when green) → pipeline opens `integration → main` → owner approves. No agent opens that PR.
|
|
21
|
+
- A repository that differs states it under "Release flow of this repository".
|
|
22
|
+
|
|
23
|
+
## Tasks ledger
|
|
24
|
+
- At the start and the end of a task, log with `adl activity add "<note>"` and read the log with `adl activity list`. The MCP tools `activity_add`/`activity_list` are the same storage; the CLI is the documented fallback.
|
|
25
|
+
- The log is `.adl/agent-activity.sqlite` — gitignored, local only. One ledger per repository; the tool fills time, surface, agent and repo, and only the note is caller-supplied.
|
|
26
|
+
|
|
27
|
+
## Release flow of this repository
|
|
28
|
+
- <!-- Owner: state any difference from the flow above; delete this section if none. -->
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Invariants
|
|
2
|
+
|
|
3
|
+
Must-never-regress rules. One bullet each: `rule` — verified by `gate/test/command`. No check, no invariant. Add yours under "This repository".
|
|
4
|
+
|
|
5
|
+
## Estate-wide
|
|
6
|
+
- The compliance gates enabled in `adl-config.toml` pass before every commit — verified by `adl repo controls` pre-commit and CI. A gate is switched off only by the owner, by removing it from `[controls.compliance] enabled`.
|
|
7
|
+
- A compliance exemption list only shrinks: remove a file once it passes; never add one — verified by the gate that reads the register.
|
|
8
|
+
- No multi-line comments in code; the explanation lives in `docs/` — verified by `repository.no-multiline-comments`.
|
|
9
|
+
- The standard trees `docs`, `plans`, `workflows` and `compliance` are those declared in `[controls.compliance.trees]`, matching the layout in `AGENTS.md` — verified by the layout and docs gates.
|
|
10
|
+
|
|
11
|
+
## Protected paths
|
|
12
|
+
- Landed only by the owner; an agent leaves them uncommitted and asks — verified by the pre-commit integrity gate.
|
|
13
|
+
- `adl-config.toml`, `compliance/` (or the compliance tree declared in `[controls.compliance.trees]`) and `workflows/`, plus any pattern this repository adds under `[controls.integrity].watched`.
|
|
14
|
+
- No dot folder is protected as a whole: `.workflows/` and the agent surface folders hold the repository's own hooks and skills.
|
|
15
|
+
|
|
16
|
+
## This repository
|
|
17
|
+
- <!-- Owner: `rule` — verified by `gate/test/command`. -->
|
|
@@ -92,17 +92,6 @@ def test_declared_dirs_and_files_stay_silent() -> None:
|
|
|
92
92
|
assert f"docs/{entry}: undeclared" not in text
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
def test_the_task_ledger_is_declared() -> None:
|
|
96
|
-
"""`docs/notes.md` is the repository's task ledger (global agent rule: one
|
|
97
|
-
line at the start and one at the finish of every task), so it is a
|
|
98
|
-
declared top-level file. The stray file next to it is still reported, which
|
|
99
|
-
proves the ledger is allowed by name and not because the gate went quiet."""
|
|
100
|
-
assert "docs/notes.md" in corpus_files(CORPUS)
|
|
101
|
-
messages = tuple(f.render() for f in findings_for(corpus_files(CORPUS)))
|
|
102
|
-
assert not any("docs/notes.md" in m for m in messages), messages
|
|
103
|
-
assert any("docs/stray-root-file.md" in m for m in messages), messages
|
|
104
|
-
|
|
105
|
-
|
|
106
95
|
def test_an_empty_set_grades_nothing() -> None:
|
|
107
96
|
"""Every undeclared entry is still on disk. A check that grades only what
|
|
108
97
|
it is handed must report none of them against an empty given set."""
|
|
@@ -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
|
|
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
|
|
151
|
-
"""`index.md` (the standing catalog name, also scope-excluded)
|
|
152
|
-
|
|
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
|
-
#
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
repository declares its own, as
|
|
9
|
-
open-plans=N` (`[controls.compliance.limits].
|
|
10
|
-
adl-config.toml). `
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
37
|
-
# `open-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
82
|
-
|
|
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
|
-
|
|
141
|
-
if
|
|
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"{
|
|
144
|
-
"
|
|
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,
|
|
110
|
+
body = self.text(ctx, intent)
|
|
147
111
|
if body is None:
|
|
148
112
|
raise CouldNotRun(
|
|
149
|
-
f"{
|
|
113
|
+
f"{intent} is missing; an unreadable plan state cannot be counted"
|
|
150
114
|
)
|
|
151
|
-
status = self._status(body,
|
|
152
|
-
if status in PlanMetadata.
|
|
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.
|
|
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.
|
|
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
|