Skip to content

feat(kanban): add phase 7 product artifacts#2566

Closed
MohAnghabo wants to merge 13 commits into
pingdotgg:mainfrom
MohAnghabo:feature/t3-kanban-phase-7-product-artifacts
Closed

feat(kanban): add phase 7 product artifacts#2566
MohAnghabo wants to merge 13 commits into
pingdotgg:mainfrom
MohAnghabo:feature/t3-kanban-phase-7-product-artifacts

Conversation

@MohAnghabo
Copy link
Copy Markdown

@MohAnghabo MohAnghabo commented May 6, 2026

Summary

  • Add shared product artifact contracts for browsing, reading, and guarded writes under docs/product/**/*.md.
  • Add a server-side Product Artifacts Provider with path confinement, clean/dirty/conflict status checks, protected-branch blocking, confirmed writes, and concise linked issue/PR comments.
  • Extend the Kanban console mock artifact view with edit, preview, guarded patch state, AR/EN strings, and browser coverage.

Linked Work

  • Refs MohAnghabo/ai-starter-pro#43
  • Spec: docs/tasks/t3-kanban-project-console.md
  • Completed: Phase 7 Product Artifacts.
  • Out of scope: PR/CI watcher, release pipeline, and production wiring beyond the provider slice.

Testing Guide

  1. bun run --cwd packages/contracts test -- kanbanConsole
  2. bun run --cwd apps/server test -- ProductArtifactsProvider
  3. bun run --cwd apps/web test -- kanbanConsoleMock
  4. bun run --cwd apps/web test:browser -- KanbanConsoleMock
  5. bun run fmt:check
  6. git diff --check
  7. bun check

Docs and Tests Impact

  • Docs updated: Phase 7 execution log in docs/tasks/t3-kanban-project-console.md.
  • Tests added/updated: contracts, server provider, web mock, and browser smoke coverage.

Risks and Rollback

  • Risk: The provider introduces local Markdown writes; mitigated by docs/product path confinement, explicit confirmation, clean target checks, protected branch checks, and test coverage.
  • Risk: GitHub comment posting can fail after a local write; mitigated by returning an explicit applied result that reports comment failure instead of hiding the local write.
  • Rollback: Revert this PR to remove Phase 7 artifact contracts, provider, mock UI wiring, tests, and execution-log updates.

Well-Architected / Compliance

  • Security/PDPL: no real PII, secrets, raw command output, or diffs are included in tests or generated GitHub comments.
  • Reliability: GitHub comment failure degrades explicitly after local artifact write; dirty/conflicting/protected branch states block writes.
  • Performance/cost/sustainability: recursive Markdown listing and per-file status checks are simple and bounded for v1; larger artifact trees are deferred to the Phase 12 performance pass.
  • i18n: new user-facing artifact strings include EN and AR entries.

PR Size Rationale

  • This PR is above the 400 LOC target because Phase 7 is a tightly coupled vertical slice: shared contracts, server provider, mock UI, browser coverage, and the durable execution log need to land together for a coherent reviewable feature boundary.
  • The bulk is focused tests and the new provider; production blast radius remains confined to the Kanban artifact slice.

Readiness Checklist

  • Relevant Markdown docs updated where needed
  • Tests added or updated for this change
  • Local validation passed with bun check
  • All required CI checks are green
  • GitHub issue linked
  • Durable spec path linked when applicable
  • Task execution log updated when applicable

Note

Medium Risk
Adds new GitHub Actions workflows (PR readiness + optional AI review/auto-fix router/executor) and new server-side GitHub CLI integrations, which can affect CI behavior and repo automation if misconfigured; most automation is gated/disabled by default and mutating operations require explicit confirmation.

Overview
Introduces a governance/agent runbook kit under .ai/ and .claude/ (PDPL + IFRS + security + PR-readiness + environment-topology rules) plus synced .codex/ wrappers and a Codex environment.toml for consistent agent execution.

Adds CI/automation scaffolding: a new pr-readiness workflow that enforces PR body checkboxes + required check-run names via scripts/check-pr-readiness.sh, plus optional AI review and closed-loop auto-fix workflows (.github/ai-loop.yml, ai-review, ai-fix-router, ai-fix-executor-claude) that are disabled by default.

Extends the server Kanban slice with GitHubProjectsProvider (read Projects via gh, update status, post comments) and AgentWorkflowLauncher (generate agent command recipes, build a redacted task context package, queue sessions with duplicate suppression, and post lifecycle comments), with tests emphasizing redaction and explicit confirmation for any write/comment action.

Reviewed by Cursor Bugbot for commit cba9403. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add phase 7 product artifacts for Kanban console including mock UI, contracts, preflight checks, and AI loop workflows

  • Introduces a fully functional mock Kanban console UI (KanbanConsoleMock.tsx) with drag-and-drop columns, RTL/LTR locale switching, and multi-view navigation; routed at /kanban
  • Adds Effect-based service providers for git status, GitHub Projects, product artifacts, and agent workflow launching under apps/server/src/kanban/
  • Defines runtime-validated Effect Schema contracts for all Kanban Console domain types in packages/contracts/src/kanbanConsole.ts
  • Adds a preflight check framework (scripts/preflight/) covering Doppler, Neon, Convex, Vercel, Better Auth, environment topology, and auto-fix routines
  • Introduces AI loop GitHub Actions workflows for automated code review, event routing, and Claude-powered autofix execution on PRs
  • Adds Claude and Codex command runbooks for orchestration, planning, IFRS/PDPL audits, PR workflows, and agent lifecycle management under .claude/commands/ and .codex/commands/
  • Risk: The AI autofix executor workflow can commit and push to PR branches when enabled; it is disabled by default via .github/ai-loop.yml
📊 Macroscope summarized cba9403. 38 files reviewed, 13 issues evaluated, 5 issues filtered, 3 comments posted

🗂️ Filtered Issues

apps/server/src/kanban/GitHubProjectsProvider.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 191: In fieldValue, the priority of the nullish coalescing on line 191 is reversed. When the GitHub CLI returns field values with structure { "field": { "name": "Status" }, "name": "In Progress" }, the expression trim(field.name) ?? trim(objectValue(field.field)?.name) evaluates field.name first, which yields the value ("In Progress") rather than the field name ("Status"). Since "in progress" is not in the wanted set for field names like "Status", the field is skipped and the lookup falls through to the fallback. The expression should be trim(objectValue(field.field)?.name) ?? trim(field.name) to prioritize the nested field.field.name (the actual field name) over field.name (which may hold the value). [ Failed validation ]
apps/server/src/kanban/ProductArtifactsProvider.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 117: statusFromPorcelain fails to detect DD (both deleted) and AA (both added) merge conflicts. Git porcelain v1 uses these status codes for unmerged entries that don't contain 'U': DD means both deleted, AA means both added. The check line.slice(0, 2).includes("U") will return false for these, causing the function to incorrectly return "dirty" instead of "conflict". This could allow writeArtifact to proceed on a file that is actually in a merge conflict state. [ Failed validation ]
scripts/ai-loop/config.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 53: The config file path defaults to .github/ai-loop.yml (a YAML file), but the code uses JSON.parse() to parse it. YAML is a superset of JSON, meaning valid JSON is valid YAML, but not vice versa. If the config file uses any YAML-specific syntax (like unquoted strings, multi-line strings with |, anchors, etc.), JSON.parse() will throw a SyntaxError. Either the file extension should be .json, or a YAML parser library should be used. [ Out of scope (triage) ]
scripts/ai-loop/router-logic.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 56: The function checks state.last_result_fingerprint but the calling code stores the fingerprint in state.last_signal_fingerprint. In router.ts, after computing findingSetFingerprint, it is assigned to last_signal_fingerprint (e.g., last_signal_fingerprint: findingSetFingerprint). However, shouldBlockRepeatedFindingSet compares against last_result_fingerprint, which is a different field that is never populated with the finding fingerprint. This means the repeated-finding-set detection will never trigger, defeating the intended blocking behavior. [ Failed validation ]
scripts/preflight/checks/integrations.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 181: The doppler/yaml check provides a misleading hint even when the check passes. When files.length > 0 && !hasPlaceholder evaluates to true (status = "pass"), the hint is still set to "Replace placeholder Doppler project names." because the ternary at lines 182-185 only distinguishes between files.length === 0 and files.length > 0, not whether the check passed. Other checks in this file correctly use undefined for the hint when passing (e.g., line 221, line 250), but this one always provides a string hint. [ Cross-file consolidated ]

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 156db5e9-3044-4d6d-90de-e452efd1bca0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@MohAnghabo
Copy link
Copy Markdown
Author

Opened against upstream by mistake from the product fork workflow. Closing and recreating in MohAnghabo/kanban-console.

@MohAnghabo MohAnghabo closed this May 6, 2026
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels May 6, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cba9403. Configure here.

shell: bash
run: |
findings_file="$RUNNER_TEMP/ai-loop-findings.json"
printf '%s' '${{ inputs.findings_b64 }}' | base64 --decode > "$findings_file"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shell injection via unsanitized workflow dispatch input

Medium Severity

The ${{ inputs.findings_b64 }} value is interpolated directly into a shell command inside single quotes. If the input contains a single quote character, it breaks the quoting and enables arbitrary command injection. The safe pattern is to pass it through an environment variable (e.g., env: FINDINGS_B64: ${{ inputs.findings_b64 }}) and reference "$FINDINGS_B64" in the script instead. This violates the project's blocking rule against unsafe GitHub Actions patterns using event input in shell commands.

Fix in Cursor Fix in Web

Triggered by project rule: Bugbot Project Brief

Reviewed by Cursor Bugbot for commit cba9403. Configure here.

};
}

if (request.toColumn === "blocked") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low src/kanbanConsoleMock.ts:683

The transition to "blocked" always returns requiresConfirmation: true regardless of whether request.confirmed is already true. Unlike the "done" case which checks && !request.confirmed, the "blocked" case has no such guard. This means users who have already confirmed will still be prompted again, creating an infinite confirmation loop.

-  if (request.toColumn === "blocked") {
+  if (request.toColumn === "blocked" && !request.confirmed) {
Also found in 1 other location(s)

scripts/preflight/checks/integrations.ts:181

The doppler/yaml check provides a misleading hint even when the check passes. When files.length > 0 && !hasPlaceholder evaluates to true (status = "pass"), the hint is still set to "Replace placeholder Doppler project names." because the ternary at lines 182-185 only distinguishes between files.length === 0 and files.length > 0, not whether the check passed. Other checks in this file correctly use undefined for the hint when passing (e.g., line 221, line 250), but this one always provides a string hint.

🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/kanbanConsoleMock.ts around line 683:

The transition to `"blocked"` always returns `requiresConfirmation: true` regardless of whether `request.confirmed` is already `true`. Unlike the `"done"` case which checks `&& !request.confirmed`, the `"blocked"` case has no such guard. This means users who have already confirmed will still be prompted again, creating an infinite confirmation loop.

Evidence trail:
apps/web/src/kanbanConsoleMock.ts lines 657-703 at REVIEWED_COMMIT: previewTaskTransition function. Line 672: `if (request.toColumn === "done" && !request.confirmed)` guards with `!request.confirmed`. Line 683: `if (request.toColumn === "blocked")` has no such guard. Line 700: default case uses `requiresConfirmation: !request.confirmed`. The asymmetry shows the "blocked" case is missing the `!request.confirmed` check.

Also found in 1 other location(s):
- scripts/preflight/checks/integrations.ts:181 -- The `doppler/yaml` check provides a misleading hint even when the check passes. When `files.length > 0 && !hasPlaceholder` evaluates to true (status = `"pass"`), the hint is still set to `"Replace placeholder Doppler project names."` because the ternary at lines 182-185 only distinguishes between `files.length === 0` and `files.length > 0`, not whether the check passed. Other checks in this file correctly use `undefined` for the hint when passing (e.g., line 221, line 250), but this one always provides a string hint.

Comment on lines +34 to +46
if (
checks.some((check) => check.id === "stack-b/convex-deployment" && check.status === "error")
) {
fixes.push(await runProviderCli(context, deps, "convex-dev"));
}

if (hasFixableIssue(checks, "stack-b/vercel-link")) {
fixes.push(await runProviderCli(context, deps, "vercel-link"));
}

if (checks.some((check) => check.id === "stack-a/neon-url" && check.status === "error")) {
fixes.push(await runProviderCli(context, deps, "neon-project-create"));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low fix/apply.ts:34

The conditions for stack-b/convex-deployment (line 35) and stack-a/neon-url (line 44) check only status === "error" without verifying fixable, unlike other fixes that use hasFixableIssue. This causes runProviderCli to execute even when a check explicitly reports fixable: false, running potentially expensive or destructive CLI commands for issues marked as non-auto-fixable.

-  if (
-    checks.some((check) => check.id === "stack-b/convex-deployment" && check.status === "error")
-  ) {
+  if (hasFixableIssue(checks, "stack-b/convex-deployment")) {
     fixes.push(await runProviderCli(context, deps, "convex-dev"));
   }
 
   if (hasFixableIssue(checks, "stack-b/vercel-link")) {
     fixes.push(await runProviderCli(context, deps, "vercel-link"));
   }
 
-  if (checks.some((check) => check.id === "stack-a/neon-url" && check.status === "error")) {
+  if (hasFixableIssue(checks, "stack-a/neon-url")) {
     fixes.push(await runProviderCli(context, deps, "neon-project-create"));
   }
🤖 Copy this AI Prompt to have your agent fix this:
In file scripts/preflight/fix/apply.ts around lines 34-46:

The conditions for `stack-b/convex-deployment` (line 35) and `stack-a/neon-url` (line 44) check only `status === "error"` without verifying `fixable`, unlike other fixes that use `hasFixableIssue`. This causes `runProviderCli` to execute even when a check explicitly reports `fixable: false`, running potentially expensive or destructive CLI commands for issues marked as non-auto-fixable.

Evidence trail:
scripts/preflight/fix/apply.ts lines 10-14 (hasFixableIssue checks fixable), lines 34-38 (convex-deployment bypasses fixable), lines 44-45 (neon-url bypasses fixable), lines 26,30,40 (other fixes use hasFixableIssue). scripts/preflight/checks/support.ts line 37 (fixable defaults to false). scripts/preflight/checks/stack.ts lines 99-107 (convex-deployment check never sets fixable), lines 21-23 (neon-url error check never sets fixable), line 140 (vercel-link explicitly sets fixable: !linked).

Comment on lines +535 to +541
input.targetTag
? readOptionalGitOutput(git, {
cwd: input.cwd,
operation: "KanbanGitStatusProvider.targetTag",
args: ["tag", "--list", input.targetTag],
})
: Effect.succeed(""),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium kanban/GitStatusProvider.ts:535

When input.targetTag is undefined and targetTag is derived from the branch name, targetTagOutput is set to an empty string (via Effect.succeed("") on line 541) instead of checking if the derived tag exists. This causes the gate-tag-readiness gate to incorrectly report "passing" even when the derived tag already exists in the repository.

-      input.targetTag
-        ? readOptionalGitOutput(git, {
-            cwd: input.cwd,
-            operation: "KanbanGitStatusProvider.targetTag",
-            args: ["tag", "--list", input.targetTag],
-          })
-        : Effect.succeed(""),
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/kanban/GitStatusProvider.ts around lines 535-541:

When `input.targetTag` is undefined and `targetTag` is derived from the branch name, `targetTagOutput` is set to an empty string (via `Effect.succeed("")` on line 541) instead of checking if the derived tag exists. This causes the `gate-tag-readiness` gate to incorrectly report "passing" even when the derived tag already exists in the repository.

Evidence trail:
apps/server/src/kanban/GitStatusProvider.ts lines 535-541 (targetTagOutput set to empty string when input.targetTag is falsy), line 554 (targetTag derived from branch via releaseTagFromBranch), lines 494-498 (releaseTagFromBranch returns a tag string for release/ branches), lines 582-586 (gate logic uses derived targetTag with unchecked targetTagOutput)

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented May 6, 2026

Approvability

Verdict: Needs human review

1 blocking correctness issue found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant