Task-id allocation rule + next-id helper
TASK-IMP-105: Task-id allocation rule + next-id helper
Summary
task-author has no rule for choosing a task id - the model picks, and the only safety net is backlog-mutate's uniqueness gate, which fires at INSERT after the spec folder is already on disk. Two authoring runs against one module can therefore both pick the same id, write two folders, and leave an orphan when the second insert refuses. State the allocation rule in the skill and make it executable as backlog-mutate next-id <module>.
Problem
grep for an allocation rule in task-author/SKILL.md finds nothing: no "scan existing ids", no "next available". The uniqueness pre-image gate in backlog-mutate.mjs:258 refuses with exit 7 (row already present ... uniqueness pre-image violated) - correct, but late: the spec files exist by then, and the operator is left with a half-landed task and no instruction.
Authoring is usually serial, which is why this has not bitten. This run interleaved five batches and got lucky. The pattern this run keeps re-learning is that every mechanical rule left in prose gets re-derived wrongly eventually, and every one moved into a tool stops being a question.
Proposed Solution
Add the rule to task-author: allocate by scanning docs/tasks/<module>/ for the highest existing stem and taking the next, and re-scan immediately before writing rather than trusting an id chosen at PLAN time. Add backlog-mutate next-id <module> so the rule is executable rather than remembered - it reads the same corpus the insert gate reads, so allocation and admission cannot disagree. The gate stays exactly as it is: this narrows the window, it does not replace the check.
Alternatives Considered
- A lock around authoring. Rejected: heavier than the defect, and it would serialise an operation that is legitimately parallel across different modules.
- Random or timestamp ids. Rejected: the corpus's id-ascending ordering is load-bearing for the regenerator's row grammar and for humans reading the backlog.
- Rely on the insert gate alone (status quo). Rejected: it refuses correctly but only after the files exist, which converts a preventable collision into a cleanup.
Success Metrics
- Primary:
next-idreturns the correct next stem for a populated module, an empty module, and a module with gaps - suite-asserted. Baseline: no allocation rule exists at all. - Guardrail: the uniqueness gate still refuses a duplicate insert (this task must not weaken it), asserted by the existing exit-7 arm.
Scope
In scope: the allocation rule in task-author/SKILL.md, backlog-mutate next-id <module>, suite arms.
Out of scope / Non-Goals
- Any change to the uniqueness gate's behavior - it remains the authority.
- Cross-module id coordination (ids are per-module by construction).
- Reserving ids ahead of authoring (a queue this backlog does not need).
Dependencies
None logically.
Serialisation note: touches backlog-mutate.mjs (shared with TASK-IMP-108, which adds entered_via to the same writer). Parent-serialised per §11a.
AI Authorship Disclosure
- Tools used: Claude (Fable 5) running the CyberOS task-author skill inside Cowork.
- Scope: spec drafted from IMPROVEMENT_HANDOFF.md IMP-26, verified against task-author/SKILL.md and backlog-mutate.mjs on merged main; implementation under ship-tasks supervision.
- Human review: scope approved at the 2026-07-17 PLAN gate; both HITL gates are recorded human verdicts.
1. Description (normative)
- 1.1
task-author/SKILL.mdMUST state the allocation rule: the next id for a module is the highest existing stem indocs/tasks/<module>/plus one, and it MUST be re-scanned immediately before writing files rather than reused from PLAN time. - 1.2
backlog-mutateMUST exposenext-id <module>printing the next free stem to stdout and exiting 0. - 1.3
next-idMUST derive from the same corpus the insert gate reads, so an id it returns cannot be rejected by the gate for non-uniqueness in the same instant. - 1.4
next-idon a module with no tasks MUST return that module's first stem and exit 0 (an empty module is not an error). - 1.5
next-idMUST ignore gaps: it returns highest+1, never the lowest free number, because reusing a retired id makes two different tasks share a name in the history. - 1.6 The uniqueness pre-image gate MUST remain unchanged and MUST remain the authority on admission.
2. Acceptance criteria
- [ ] AC 1 (traces_to: #1.2, #1.3) -
next-id improvementon the live corpus returns the highest+1 stem and exits 0 - test:tools/install/tests/test_workflow_helpers.sh::t15_next_id_populated - [ ] AC 2 (traces_to: #1.4) -
next-id <empty-module>returns the first stem and exits 0 - test:tools/install/tests/test_workflow_helpers.sh::t16_next_id_empty_module - [ ] AC 3 (traces_to: #1.5) - a corpus with a gap yields highest+1, not the gap - test:
tools/install/tests/test_workflow_helpers.sh::t17_next_id_ignores_gaps - [ ] AC 4 (traces_to: #1.6) - the existing exit-7 uniqueness refusal still fires unchanged - test:
tools/install/tests/test_workflow_helpers.sh::t07_insert_uniqueness_refusal - [ ] AC 5 (traces_to: #1.1) - the skill states the rule including the re-scan-before-write requirement - verify: recorded grep in the gate log (prose contract; same rationale as TASK-IMP-090 AC 1).
3. Edge cases
- Folder present on disk but no BACKLOG row (a half-landed task from the exact collision this fixes):
next-idMUST count the folder - the folder is the task, the row is the index, and skipping it would hand out the colliding id again. - Malformed stem in the module (hand-created folder not matching the grammar): skip it with a note on stderr; one bad folder must not stop allocation.
- Two
next-idcalls racing: both return the same stem. This narrows the window, it does not close it - the gate remains the authority (1.6), and this is why 1.6 exists. - A module whose name does not yet exist as a directory: treated as empty per 1.4.
- Security-class: reads directory names, prints a stem. No untrusted content is executed; the module argument MUST be confined under
docs/tasks/on the samerelUnderRootrule the other helpers use, so a crafted../argument cannot walk out.
Audit
§1 - Verdict summary
Spec is 79 lines, 6 §1 clauses, 5 ACs, 5 edge cases. Gap verified on main: task-author states no allocation rule; backlog-mutate.mjs:258 is the late net. Passes after 6 findings.
§2 - Findings (all resolved)
ISS-001 - Allocation could race between PLAN and write
An id chosen at PLAN time may be taken by the time files land. Resolved: §1 #1.1 requires a re-scan immediately before writing; AC 5 verifies the rule is stated.
ISS-002 - Half-landed folder with no row would be skipped
Counting rows rather than folders re-issues the exact colliding id this task prevents. Resolved: §3 edge case makes the folder authoritative.
ISS-003 - Gap reuse would make two tasks share a name in history
Taking the lowest free number recycles a retired id. Resolved: §1 #1.5 requires highest+1; AC 3 asserts it against a gapped corpus.
ISS-004 - Could be read as replacing the uniqueness gate
It narrows the window; it does not close it. Resolved: §1 #1.6 keeps the gate authoritative, AC 4 asserts exit-7 still fires, and §3 names the residual race honestly.
ISS-005 - Module argument is a path component - a traversal surface
next-id ../../etc would walk out of the corpus. Resolved: §3 security-class requires the relUnderRoot confinement the batch-5 review forced onto task-reconcile.
ISS-006 - Empty module could be treated as an error
A module's first task must be allocatable. Resolved: §1 #1.4 makes empty legal; AC 2 covers it.
§3 - Resolution
All 6 concerns addressed. The machine floor (task-lint) ran FIRST and was clean before any judgment family was applied, per TASK-IMP-084. Score = 10/10.
End of TASK-IMP-105 audit.