task-lint, a deterministic machine floor under the task-audit rubric
TASK-IMP-084: task-lint, a deterministic machine floor under the task-audit rubric
Summary
audit_rubric@2.0 calls itself machine-checkable, but nothing executes it - every FM enum, required section, and traceability citation is re-derived by a model on each audit. Ship task-lint.mjs in docs-tools: a zero-dependency node CLI that checks the mechanical rule families deterministically, emits rule_id-tagged findings, and exits non-zero on error severity. The task-audit skill runs it first and judges only what needs judgment (QA semantics, SAFE content, TRACE meaning). Consumer repos inherit the tool through the payload's docs-tools copy.
Problem
On the 2026-07-16 sachviet run, six spec audits were performed entirely by the model. The findings that mattered were judgment calls, but the majority of rubric surface walked each time was mechanical: frontmatter enums, title length, snake_case keys, required H2s, conditional sections, clause-to-AC citation presence, test-path resolution. A model re-checking those is spending its diligence budget where a 300-line script is strictly better - byte-stable, instant, and immune to fatigue. The rubric's own header makes the promise:
<untrusted_content source="modules/skill/task-audit/RUBRIC.md:1">
audit_rubric@2.0 - machine-checkable Task rubric
</untrusted_content>
Proposed Solution
node .cyberos/docs-tools/task-lint.mjs <spec.md ... | dir> (in the platform repo: tools/install/docs-tools/task-lint.mjs). Node stdlib only. It parses frontmatter with a strict minimal YAML reader (scalars, lists, quoted strings - the subset the template uses; anything beyond that is itself an FM-001 finding), walks the body headings, and checks the mechanical families. Output: one line per finding, SEVERITY rule_id file:line message, sorted; --json for machines; exit 0 only when zero error-severity findings. The task-audit skill's loop gains one normative step: run the lint first when present, seed the report's mechanical findings from it, then perform the judgment families. The lint never replaces the audit; it floors it.
Alternatives Considered
- A real YAML dependency (js-yaml) for parsing. Rejected: docs-tools is stdlib-only by convention (md.mjs, render-status-hub.mjs), payload stays dependency-free, and the template's frontmatter subset is small; a strict subset parser that FAILS LOUDLY on exotic YAML is safer than silently accepting what the audit families never defined.
- Extending an existing tool (repair_task_yaml.py). Rejected: that script mutates (repairs); the lint must be read-only and rule_id-faithful, and mixing repair with verdicts blurs who changed what.
- Wiring the lint as a hard gate in run-gates.sh now. Rejected for this task: gates belong to per-repo config; the skill-level wiring gets the value everywhere immediately, and repos can add it to config.yaml gates themselves. Revisit after adoption.
Success Metrics
- Primary: the mechanical rule families run in milliseconds with zero model involvement - lint verdict on the three batch-1 specs and a green corpus sample matches the model audits' mechanical findings (no false errors). Baseline: 0 percent of rubric surface machine-executed. Deadline: this task's final acceptance.
- Guardrail: seeded violation fixtures (one per family) each produce exactly their rule_id and a non-zero exit, on every suite run.
Scope
In scope: the lint CLI, its fixture-driven test suite, one normative wiring line in task-audit SKILL.md, payload presence gating.
Out of scope / Non-Goals
- Judgment families: QA-001..009 semantics (vanity metrics, risk-class dodging), SAFE-003 content scanning beyond marker presence, TRACE semantic sufficiency, XCHAIN/STALE manifest cross-checks.
- Auto-fixing anything (read-only tool).
- engineering-spec@1 profile rules (the lint targets template: task@1 detection; other profiles report
template_ambiguousand stop, mirroring RUBRIC §10). - run-gates.sh wiring (per-repo choice, later item).
Dependencies
- None upstream. Cone-disjoint from TASK-IMP-082 (renderer) and TASK-IMP-083 (install.sh); ships in the same parallel batch. Downstream: IMPROVEMENT_HANDOFF IMP-04's helpers can reuse its frontmatter reader.
AI Authorship Disclosure
- Tools used: Claude (Fable 5) running the CyberOS task-author skill inside Cowork.
- Scope: spec drafted by the model from IMPROVEMENT_HANDOFF.md IMP-03 plus rubric source mapping; implementation follows under ship-tasks supervision.
- Human review: PLAN approved by the operator on 2026-07-16; spec audit and both HITL acceptance gates are recorded human verdicts.
1. Description (normative)
- 1.1 The lint MUST accept one or more spec paths or directories (directories recurse to
*/spec.md) and MUST run on node stdlib alone. - 1.2 The lint MUST implement the FM family mechanically: FM-001 (fences + parseable frontmatter per the strict subset), FM-002 (snake_case keys), FM-003 (duplicate keys), FM-004 (template equals task@1), FM-101 (title 1-72 after trim), FM-102 (author regex), FM-103/104/105/107/108/109 (closed enums, with FM-109 rejecting
unacceptable), FM-106 (ISO 8601 with timezone), FM-110 (semver-or-quarter when present), FM-111 (real YAML boolean), FM-112 (no# UNREVIEWEDmarker), FM-113 (duplicate_of iff status duplicate, resolving to an existing task folder), FM-114 (severity iff type bug). - 1.3 The lint MUST implement SEC mechanically: SEC-001..007 required H2s present, SEC-008 each non-empty (at least one non-blank body line before the next heading), SEC-009 single H1 and no skipped heading levels (warning severity).
- 1.4 The lint MUST implement COND triggers mechanically: COND-001/002 sections when client_visible true, COND-003 with its three H3s in order when risk class is limited or high, COND-004 with the three labeled bullets when ai_authorship is not none.
- 1.5 The lint MUST implement the structural TRACE halves: TRACE-001 presence form (every numbered §1 clause containing a BCP-14 keyword is cited by at least one AC via
§1 #Nortraces_to), TRACE-002 presence form (every AC line carries atest:orverify:entry), TRACE-003 (everytest:path segment before::is listed in frontmatter new_files or exists on disk). Semantic sufficiency stays with the model audit. - 1.6 Findings MUST be emitted one per line as
SEVERITY rule_id file:line message, sorted bytewise, with--jsonemitting the same findings as a JSON array; two runs on identical input MUST be byte-identical. - 1.7 Exit code MUST be 0 with no error-severity findings, 2 otherwise; unreadable input or non-task@1 template detection MUST report
template_ambiguousat error severity rather than guessing. - 1.8
modules/skill/task-audit/SKILL.mdMUST gain one normative loop step: when the lint is present (.cyberos/docs-tools/task-lint.mjsin installed repos,tools/install/docs-tools/task-lint.mjsin the platform repo), run it FIRST and seed the report's mechanical findings from its output; the model then audits the judgment families only. - 1.9 A gating suite MUST land at
tools/install/tests/test_task_lint.sh: the tool passes on this batch's three specs; one fixture per family (FM, SEC, COND, TRACE) produces exactly its rule_id and exit 2; the assembled payload carriesdocs-tools/task-lint.mjsand a scratch install lays it into.cyberos/docs-tools/.
2. Acceptance criteria
- [ ] AC 1 (traces_to: §1 #1.1, #1.6, #1.7) - CLI shape, deterministic sorted output, exit codes - test:
tools/install/tests/test_task_lint.sh::t01_cli_and_determinism - [ ] AC 2 (traces_to: §1 #1.2) - FM family fixtures each yield their rule_id - test:
tools/install/tests/test_task_lint.sh::t02_fm_family - [ ] AC 3 (traces_to: §1 #1.3) - SEC family fixtures - test:
tools/install/tests/test_task_lint.sh::t03_sec_family - [ ] AC 4 (traces_to: §1 #1.4) - COND trigger fixtures - test:
tools/install/tests/test_task_lint.sh::t04_cond_family - [ ] AC 5 (traces_to: §1 #1.5) - TRACE structural fixtures (uncited clause, AC without test, dangling test path) - test:
tools/install/tests/test_task_lint.sh::t05_trace_family - [ ] AC 6 (traces_to: §1 #1.9) - green corpus: batch-1 specs lint clean - test:
tools/install/tests/test_task_lint.sh::t06_green_corpus - [ ] AC 7 (traces_to: §1 #1.9) - payload carries the tool and install lays it down - test:
tools/install/tests/test_task_lint.sh::t07_payload_and_install - [ ] AC 8 (traces_to: §1 #1.8) - the skill wiring line exists and names the lint-first order - test:
tools/install/tests/test_task_lint.sh::t08_skill_wiring_present
3. Edge cases
- Frontmatter using YAML the strict subset does not parse (anchors, multiline blocks): FM-001 error naming the line - loud, never a silent skip (t02 includes one).
- A spec with zero numbered §1 clauses (pure PRD shape): TRACE-001 has nothing to check - the lint reports an info-level note, not an error (template allows judgment there; the model audit decides).
verify:ACs (justified ops verification) satisfy TRACE-002's structural half by design - the lint checks presence, the model checks the justification (t05 includes one passing verify case).- CRLF files, BOM, trailing whitespace in headings - normalized for heading matching, reported nowhere (content bytes are the corpus's business).
- Huge corpora: per-file processing, findings streamed after sort; no cross-file state except FM-113 resolution, which scans task folder names only.
- Unicode titles: length counted in code points after trim (documented; FM-101's 72 is a display bound, not a byte bound).
- Security-class: the lint reads files and never executes content;
--jsonoutput is data, not code. The injection-marker scan (SAFE-003) stays with the model by scope - noted so nobody assumes the lint covers it.
4. Out of scope / non-goals
Duplicated intentionally with ## Scope for template conformance: judgment families, auto-fix, other template profiles, and gate wiring are excluded.
5. Protected invariants this task must not weaken
- The lint floors the audit, never replaces it - the skill text keeps the model responsible for judgment families and the 10/10 verdict.
- docs-tools stays node-stdlib-only.
- Payload sync doctrine: rebuild dist, version-sync, full suite before commit.
- HITL: both human-acceptance gates are recorded verdicts; the agent never sets done.
End of TASK-IMP-084.
Audit
TASK-IMP-084-task-lint-machine-floor spec audit - audit_rubric@2.0
Families walked: FM (all pass - task@1, title <=72, closed enums, ISO created_at, no UNREVIEWED markers, corpus extras additive), SEC (seven H2s present and non-empty, one H1), COND (COND-004 three labeled bullets present; others not triggered), QA (metrics carry baseline/target/deadline grounded in run evidence; >=3 distinct alternatives; Out-of-scope subsection; no unsourced numeric targets; no cross-team claims), SAFE (one sourced block, closed, unnested, clean scan), TRACE (every numbered clause cited by >=1 AC; every AC names a test or a justified ops verification; every test path is in new_files or exists on disk; draft status exempts TRACE-004; no deferred slices).
ISSUE ISS-001 (QA-007, wontfix-info): the strict YAML-subset parser is a design decision marked in Proposed Solution (loud FM-001 on exotic YAML beats silent acceptance); reviewer approves at the review gate. ISSUE ISS-002 (SAFE-004, wontfix-info): the spec explicitly notes SAFE-003 content scanning stays with the model so nobody assumes lint coverage - recorded as a scope guard.
SUMMARY verdict: pass issues_open: 0 issues_human: 0 iterations: 2 next_action: ship
§gate-log
Populated during implementation (ship-tasks testing phase).