Skip to content
Merged
118 changes: 118 additions & 0 deletions docs/agentic/PROJECT-MANAGEMENT-TEMPLATE-CATALOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Project-Management / Gantt Integration Source Catalog

**Status:** Reconsolidated research and implementation intake
**Updated:** 2026-09-21
**Authority:** Discovery catalog only. The repository-native dependency-phase plan remains authoritative.

## Purpose

This catalog consolidates the prior Gantt work in termux-monorepo, the user's forked Gantt repositories, and a fresh external search of project-management templates. The goal is pattern extraction + deterministic adapters, not replacing the repository's control plane with a third-party UI.

The existing architecture already establishes the key boundary: phases, dependencies, approvals, PR/check evidence, and GitHub Project reconciliation are authoritative; Gantt/Markdown/Mermaid/dashboard views are derived.

## Reconnaissance: user-owned Gantt forks

| Source | Role discovered | Reusable capability | Disposition |
|---|---|---|---|
| timerloggedout-spec/GanTTY_fork | Python terminal planner | interactive dependency editing, task state, compact terminal UX | reference only |
| timerloggedout-spec/ganttless_fork-agentic | Rust ASCII renderer | deterministic compact timeline from YAML/CLI input | renderer adapter |
| timerloggedout-spec/gantt-cli_fork_agentic | Rust TUI planner | parent/child tasks, dependencies, topological scheduling, JSON persistence, undo/redo | exporter/pattern adapter |
| timerloggedout-spec/Gantt-Chart-Code_fork | Rust program ingester | flat-record to hierarchy transformation | ingestion pattern |
| timerloggedout-spec/montt_fork | Rust Monte Carlo Gantt forecasting | resource/estimate DSL and probabilistic schedule concepts | deferred research seed |

These are forks of upstream projects rather than authoritative monorepo components. The current fork metadata identifies the upstreams as timeopochin/GanTTY, kyoheiu/ganttless, zhangjinshui-nerveee/gantt-cli, bytesandbalance/Gantt-Chart-Code, and simon-siggaard/montt.

## Prior repository work recovered

- docs/agentic/dependency-phases.json — canonical phase/dependency model and GitHub Project identifiers.
- scripts/agentic/dependency_phase_engine.py — validation, stable plan hashing, topological ordering, wave calculation, evidence evaluation, and derived Mermaid/Markdown rendering.
- docs/proposals/active/gantt-dependency-phases/ — proposal, work items, manifest, source, and acceptance contract.
- docs/agentic/TEMPLATE_CAPABILITY_ASSESSMENT.md — earlier source-reviewed candidate assessment.
- docs/agentic/template-candidates.yaml — machine-readable candidate registry.
- termux-multi-agent/templates/gantt_core.py — historical Gantt prototype; useful provenance, but it uses ambient wall-clock time and is not suitable as deterministic control-plane implementation.

The proposal explicitly defines the Gantt representation as a derived view that cannot authorize dispatch or completion. That remains the governing rule.

## Fresh external template/search corpus

| Source | Useful pattern | Integration class | Key caveat |
|---|---|---|---|
| Project Planner | dependency-first planning, Gantt + graph + Kanban, critical path, suggested dates, Markdown/Dataview persistence | data-model/reference | Obsidian plugin domain |
| task-cli | AI decomposition -> tasks -> Gantt -> report/Q&A CLI lifecycle | agent workflow reference | LLM planning must not become authority |
| GanttReady | CPM, EVM, resources, calendars, AI scheduling | scheduling research | .NET/SQLite application boundary |
| Agentic Project Management | persistent agent context, manager/worker/handoff model | orchestration reference | broader agent framework |
| Agent Kanban | agents as first-class actors, assignment/provenance/dependencies/review | agent-control reference | board product, not repository SSOT |
| DuneBoard | Markdown SSOT, task graph, parent/child, dependencies, readiness rules | schema/reference | local task-board product |
| Plandeck | durable file plans, deterministic completion gate, dependency unlocks, critical path | agent skill reference | CLI-oriented |
| It's a Plan | project/issue/cycle/timeline model, REST/MCP/webhooks, agents as project members | integration reference | AGPL core; active development |
| Taskboard | local PM + CLI + MCP, tickets/dependencies/teams, single-binary model | MCP integration reference | SQLite application boundary |
| Pith | MCP-first, CLI-native agent/human task management, subtask/progress APIs | MCP integration reference | external service model |
| kanban-mcp | persistent items, relationships/epics, blocking edges, semantic search | MCP/relationship reference | database-backed board |
| pm-gantt-chart | dependency-aware schedule, critical path, slack, Mermaid/HTML/SVG/CSV/JSON exports | Gantt exporter reference | preserve canonical IDs |
| gantts-app | WBS, CPM, baselines, resources, calendars, file-based interchange | scheduling/UI reference | browser-first application |
| GanttProject | hierarchy, dependencies, milestones, baselines, resources, costs, interoperability | mature PM reference | desktop application |

## Consolidated capability vocabulary

1. Stable identity: string IDs that survive reorder and rendering.
2. Dependency DAG: explicit predecessor relationships; cycles fail closed.
3. WBS / hierarchy: parent-child decomposition without conflating hierarchy with dependency.
4. Readiness: deterministic predicate derived from prerequisites and evidence.
5. Schedule projection: dates derived from dependencies, durations, calendars, and explicit anchors.
6. Critical path / float: analytical output, never authorization.
7. Resources / ownership: planning and workload metadata.
8. Progress / evidence: status tied to observable artifacts, not a chart state.
9. Human gates: approval is explicit evidence.
10. Agent provenance: actor, claim, run, PR, SHA, and plan hash are first-class.
11. Multiple projections: Mermaid, ASCII, JSON, HTML, dashboards, and GitHub Projects can consume one normalized model.
12. MCP/API adapters: external PM systems can be integration surfaces without becoming repository authority.

## Consolidation decision

### Keep as canonical

docs/agentic/dependency-phases.json + deterministic evaluator + repository proposal/check/PR evidence.

### Add now

A standard Gantt projection contract that converts the canonical phase/evaluation model into stable-ID schedule records. It supports dependency-derived relative waves, optional deterministic date anchoring, explicit duration input, critical-path calculation, JSON output for agents/tools, and Mermaid Gantt output for documentation. It never writes back to the canonical plan.

### Defer

- third-party Gantt submodules;
- database-backed PM systems as canonical state;
- autonomous AI scheduling that mutates phase dates;
- probabilistic forecasts until empirical duration history exists;
- visual boards in CI;
- automatic merge/closure.

## Adapter contract

Every future PM/Gantt adapter should consume and preserve: phase_id, title, depends_on, state, wave, duration_days, start, end, critical, plan_sha256.

An adapter may add presentation metadata but may not rewrite identity, dependency edges, approvals, or completion evidence.

## Licensing / provenance rule

Third-party code is not copied merely because a feature is useful. Before adopting implementation code, record upstream repository, revision, license, provenance, and fixture parity. Prefer small compatibility adapters and generated outputs over vendoring entire PM applications.

## Current implementation target

scripts/agentic/gantt_projection.py implements the first normalized projection. It is dependency-only when no date anchor is supplied, and becomes a deterministic date schedule only when the operator supplies an explicit start date and duration mapping. This avoids inventing calendar commitments from a visualization.

## Sources

- https://github.com/hmil1151/project-planner
- https://github.com/sunjiawe/task-cli
- https://github.com/fvftuu/GanttReady
- https://github.com/sdi2200262/agentic-project-management
- https://github.com/saltbo/agent-kanban
- https://github.com/KaEvDm/DuneBoard
- https://github.com/OthmanAdi/plandeck
- https://github.com/croffasia/itsaplan
- https://github.com/tcarac/taskboard
- https://github.com/SiluPanda/pith
- https://github.com/multidimensionalcats/kanban-mcp
- https://github.com/unbraind/pm-gantt-chart
- https://github.com/Synth88Labs/gantts-app
- https://github.com/bardsoftware/ganttproject
221 changes: 221 additions & 0 deletions scripts/agentic/gantt_projection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
#!/usr/bin/env python3
"""Project the canonical dependency-phase plan into a stable Gantt interchange model."""

from __future__ import annotations

import argparse
import json
from datetime import date, timedelta
from pathlib import Path
from typing import Any

from dependency_phase_engine import compute_waves, plan_digest, topological_order, validate_plan


def _load_json(path: Path) -> dict[str, Any]:
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise ValueError(f"{path}: expected a JSON object")
return value


def _parse_date(value: str) -> date:
try:
return date.fromisoformat(value)
except ValueError as exc:
raise ValueError(f"invalid --start-date: {value!r}; expected YYYY-MM-DD") from exc


def _durations(phases: list[dict[str, Any]], default_days: int, mapping: dict[str, Any] | None) -> dict[str, int]:
if default_days < 1:
raise ValueError("--duration-days must be >= 1")
mapping = mapping or {}
phase_ids = {str(p["phase_id"]) for p in phases}
result: dict[str, int] = {}
for phase in phases:
phase_id = str(phase["phase_id"])
raw = mapping.get(phase_id, default_days)
if isinstance(raw, bool) or not isinstance(raw, int) or raw < 1:
raise ValueError(f"duration for {phase_id} must be a positive integer")
result[phase_id] = raw
unknown = sorted(set(mapping) - phase_ids)
if unknown:
raise ValueError("duration mapping contains unknown phase IDs: " + ", ".join(unknown))
return result


def _critical_path(phases: list[dict[str, Any]], durations: dict[str, int]) -> set[str]:
by_id = {str(p["phase_id"]): p for p in phases}
best_end: dict[str, int] = {}
best_chain: dict[str, list[str]] = {}
for phase_id in topological_order(phases):
deps = [str(d) for d in by_id[phase_id].get("depends_on", [])]
if not deps:
best_end[phase_id] = durations[phase_id]
best_chain[phase_id] = [phase_id]
continue
predecessor = max(deps, key=lambda dep: (best_end[dep], dep))
best_end[phase_id] = best_end[predecessor] + durations[phase_id]
best_chain[phase_id] = best_chain[predecessor] + [phase_id]
if not best_chain:
return set()
sink = max(best_chain, key=lambda phase_id: (best_end[phase_id], phase_id))
return set(best_chain[sink])


def _validate_report(report: dict[str, Any], phases: list[dict[str, Any]], plan: dict[str, Any]) -> None:
evaluations = report.get("evaluations")
if not isinstance(evaluations, list):
raise ValueError("report.evaluations must be a list")

phase_ids = {str(phase["phase_id"]) for phase in phases}
seen: set[str] = set()
required = {
"idempotency_key",
"phase_id",
"project_item_id",
"project_status",
"pull_requests",
"reason",
"state",
}
allowed_states = {"complete", "ready", "waiting", "blocked", "unknown"}

for index, item in enumerate(evaluations):
if not isinstance(item, dict):
raise ValueError(f"report.evaluations[{index}] must be an object")
missing = sorted(required - set(item))
if missing:
raise ValueError(
f"report.evaluations[{index}] missing required fields: " + ", ".join(missing)
)
phase_id = item["phase_id"]
if not isinstance(phase_id, str) or not phase_id:
raise ValueError(f"report.evaluations[{index}].phase_id must be a non-empty string")
if phase_id not in phase_ids:
raise ValueError(f"report.evaluations[{index}].phase_id is unknown: {phase_id}")
if phase_id in seen:
raise ValueError(f"report.evaluations contains duplicate phase_id: {phase_id}")
seen.add(phase_id)
if not isinstance(item["idempotency_key"], str) or not item["idempotency_key"]:
raise ValueError(f"report.evaluations[{index}].idempotency_key must be a non-empty string")
if not isinstance(item["pull_requests"], list) or any(
isinstance(pr, bool) or not isinstance(pr, int) for pr in item["pull_requests"]
):
raise ValueError(f"report.evaluations[{index}].pull_requests must be a list of integers")
if not isinstance(item["reason"], str):
raise ValueError(f"report.evaluations[{index}].reason must be a string")
if item["state"] not in allowed_states:
raise ValueError(f"report.evaluations[{index}].state is invalid: {item['state']!r}")

if report.get("plan_id") not in (None, plan["plan_id"]):
raise ValueError("report.plan_id does not match the supplied plan")
if report.get("plan_sha256") not in (None, plan_digest(plan)):
raise ValueError("report.plan_sha256 does not match the supplied plan")
missing_phase_ids = sorted(phase_ids - seen)
if missing_phase_ids:
raise ValueError("report.evaluations is missing phase IDs: " + ", ".join(missing_phase_ids))


def project(
plan: dict[str, Any],
report: dict[str, Any] | None = None,
start_date: date | None = None,
default_duration_days: int = 1,
duration_mapping: dict[str, Any] | None = None,
) -> dict[str, Any]:
errors = validate_plan(plan)
if errors:
raise ValueError("plan validation failed:\n- " + "\n- ".join(errors))

phases = plan["phases"]
waves = compute_waves(phases)
durations = _durations(phases, default_duration_days, duration_mapping)
critical = _critical_path(phases, durations)

evaluation_by_id: dict[str, dict[str, Any]] = {}
if report:
_validate_report(report, phases, plan)
evaluation_by_id = {str(item["phase_id"]): item for item in report["evaluations"]}

by_id = {str(p["phase_id"]): p for p in phases}
tasks: list[dict[str, Any]] = []
computed_end: dict[str, date] = {}

for phase_id in topological_order(phases):
phase = by_id[phase_id]
deps = [str(d) for d in phase.get("depends_on", [])]
task: dict[str, Any] = {
"id": phase_id,
"title": str(phase["title"]),
"type": "phase",
"state": evaluation_by_id.get(phase_id, {}).get("state", "unknown"),
"dependencies": deps,
"wave": waves[phase_id],
"duration_days": durations[phase_id],
"critical": phase_id in critical,
}
if start_date:
start = max((computed_end[d] for d in deps), default=start_date)
end = start + timedelta(days=durations[phase_id] - 1)
computed_end[phase_id] = end + timedelta(days=1)
task["start"], task["end"] = start.isoformat(), end.isoformat()
tasks.append(task)

return {
"schema_version": 1,
"projection": "gantt.interchange.v1",
"authority": "derived",
"schedule_mode": "dependency-derived" if start_date else "relative-wave",
"plan_id": plan["plan_id"],
"plan_sha256": plan_digest(plan),
"tasks": tasks,
}


def render_mermaid_gantt(projection: dict[str, Any]) -> str:
lines = [
"gantt",
f" title {projection['plan_id']} (derived)",
" dateFormat YYYY-MM-DD",
" axisFormat %Y-%m-%d",
]
if projection["schedule_mode"] != "dependency-derived":
lines.append(" %% No calendar anchor supplied; use JSON wave data for relative planning.")
return "\n".join(lines) + "\n"

for task in projection["tasks"]:
task_id = task["id"].replace("-", "_")
title = " ".join(str(task["title"]).split()).replace(":", " - ")
marker = "crit, " if task["critical"] else ""
lines.append(f" {title} :{marker}{task_id}, {task['start']}, {task['duration_days']}d")
return "\n".join(lines) + "\n"


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--plan", default="docs/agentic/dependency-phases.json")
parser.add_argument("--report")
parser.add_argument("--start-date")
parser.add_argument("--duration-days", type=int, default=1)
parser.add_argument("--durations-json")
parser.add_argument("--format", choices=("json", "mermaid"), default="json")
parser.add_argument("--output")
args = parser.parse_args()

plan = _load_json(Path(args.plan))
report = _load_json(Path(args.report)) if args.report else None
start = _parse_date(args.start_date) if args.start_date else None
mapping = _load_json(Path(args.durations_json)) if args.durations_json else None
projection = project(plan, report, start, args.duration_days, mapping)
output = render_mermaid_gantt(projection) if args.format == "mermaid" else json.dumps(projection, indent=2, sort_keys=True) + "\n"

if args.output:
Path(args.output).write_text(output, encoding="utf-8")
else:
print(output, end="")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading