loki-mode 6.17.1 → 6.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +796 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v6.
|
|
6
|
+
# Loki Mode v6.18.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -267,4 +267,4 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
267
267
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
268
268
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
269
269
|
|
|
270
|
-
**v6.
|
|
270
|
+
**v6.18.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.
|
|
1
|
+
6.18.0
|
package/autonomy/loki
CHANGED
|
@@ -401,6 +401,7 @@ show_help() {
|
|
|
401
401
|
echo " pause Pause after current session"
|
|
402
402
|
echo " resume Resume paused execution"
|
|
403
403
|
echo " status [--json] Show current status (--json for machine-readable)"
|
|
404
|
+
echo " stats [flags] Session statistics (--json, --efficiency)"
|
|
404
405
|
echo " logs Show recent log output"
|
|
405
406
|
echo " dashboard [cmd] Dashboard server (start|stop|status|url|open)"
|
|
406
407
|
echo " provider [cmd] Manage AI provider (show|set|list|info)"
|
|
@@ -434,6 +435,7 @@ show_help() {
|
|
|
434
435
|
echo " agent [cmd] Agent type dispatch (list|info|run|start|review)"
|
|
435
436
|
echo " remote [PRD] Start remote session (connect from phone/browser, Claude Pro/Max)"
|
|
436
437
|
echo " trigger Event-driven autonomous execution (schedules, webhooks)"
|
|
438
|
+
echo " plan <PRD> Dry-run PRD analysis: complexity, cost, and execution plan"
|
|
437
439
|
echo " version Show version"
|
|
438
440
|
echo " help Show this help"
|
|
439
441
|
echo ""
|
|
@@ -1713,6 +1715,293 @@ print(json.dumps(result, indent=2))
|
|
|
1713
1715
|
fi
|
|
1714
1716
|
}
|
|
1715
1717
|
|
|
1718
|
+
# Session statistics
|
|
1719
|
+
cmd_stats() {
|
|
1720
|
+
local show_json=false
|
|
1721
|
+
local show_efficiency=false
|
|
1722
|
+
|
|
1723
|
+
while [[ $# -gt 0 ]]; do
|
|
1724
|
+
case "$1" in
|
|
1725
|
+
--json) show_json=true; shift ;;
|
|
1726
|
+
--efficiency) show_efficiency=true; shift ;;
|
|
1727
|
+
*) shift ;;
|
|
1728
|
+
esac
|
|
1729
|
+
done
|
|
1730
|
+
|
|
1731
|
+
if [ ! -d "$LOKI_DIR" ]; then
|
|
1732
|
+
if [ "$show_json" = true ]; then
|
|
1733
|
+
echo '{"error": "No active session"}'
|
|
1734
|
+
else
|
|
1735
|
+
echo -e "${YELLOW}No active session found.${NC}"
|
|
1736
|
+
echo "Start a session with: loki start <prd>"
|
|
1737
|
+
fi
|
|
1738
|
+
return 0
|
|
1739
|
+
fi
|
|
1740
|
+
|
|
1741
|
+
local loki_dir="$LOKI_DIR"
|
|
1742
|
+
local eff_flag="$show_efficiency"
|
|
1743
|
+
local json_flag="$show_json"
|
|
1744
|
+
|
|
1745
|
+
python3 -c "
|
|
1746
|
+
import json, os, sys, glob
|
|
1747
|
+
|
|
1748
|
+
loki_dir = sys.argv[1]
|
|
1749
|
+
show_efficiency = sys.argv[2] == 'true'
|
|
1750
|
+
show_json = sys.argv[3] == 'true'
|
|
1751
|
+
|
|
1752
|
+
# --- Gather data ---
|
|
1753
|
+
|
|
1754
|
+
# Session state (orchestrator)
|
|
1755
|
+
phase = 'N/A'
|
|
1756
|
+
iteration_count = 0
|
|
1757
|
+
orch_file = os.path.join(loki_dir, 'state', 'orchestrator.json')
|
|
1758
|
+
if os.path.isfile(orch_file):
|
|
1759
|
+
try:
|
|
1760
|
+
with open(orch_file) as f:
|
|
1761
|
+
orch = json.load(f)
|
|
1762
|
+
phase = orch.get('currentPhase', 'N/A')
|
|
1763
|
+
iteration_count = orch.get('currentIteration', 0)
|
|
1764
|
+
except Exception:
|
|
1765
|
+
pass
|
|
1766
|
+
|
|
1767
|
+
# Per-iteration metrics
|
|
1768
|
+
eff_dir = os.path.join(loki_dir, 'metrics', 'efficiency')
|
|
1769
|
+
iterations = []
|
|
1770
|
+
if os.path.isdir(eff_dir):
|
|
1771
|
+
for path in sorted(glob.glob(os.path.join(eff_dir, 'iteration-*.json'))):
|
|
1772
|
+
try:
|
|
1773
|
+
with open(path) as f:
|
|
1774
|
+
iterations.append(json.load(f))
|
|
1775
|
+
except Exception:
|
|
1776
|
+
pass
|
|
1777
|
+
|
|
1778
|
+
# If we have iteration files, use their count (more accurate)
|
|
1779
|
+
if iterations:
|
|
1780
|
+
iteration_count = max(iteration_count, len(iterations))
|
|
1781
|
+
|
|
1782
|
+
total_input = sum(it.get('input_tokens', 0) for it in iterations)
|
|
1783
|
+
total_output = sum(it.get('output_tokens', 0) for it in iterations)
|
|
1784
|
+
total_tokens = total_input + total_output
|
|
1785
|
+
total_cost = sum(it.get('cost_usd', 0) for it in iterations)
|
|
1786
|
+
total_duration = sum(it.get('duration_seconds', 0) for it in iterations)
|
|
1787
|
+
|
|
1788
|
+
# Budget
|
|
1789
|
+
budget_limit = 0
|
|
1790
|
+
budget_used = 0
|
|
1791
|
+
budget_file = os.path.join(loki_dir, 'metrics', 'budget.json')
|
|
1792
|
+
if os.path.isfile(budget_file):
|
|
1793
|
+
try:
|
|
1794
|
+
with open(budget_file) as f:
|
|
1795
|
+
bd = json.load(f)
|
|
1796
|
+
budget_limit = bd.get('budget_limit', 0)
|
|
1797
|
+
budget_used = bd.get('budget_used', 0)
|
|
1798
|
+
except Exception:
|
|
1799
|
+
pass
|
|
1800
|
+
|
|
1801
|
+
# Quality gates
|
|
1802
|
+
gates_passed = 0
|
|
1803
|
+
gates_total = 0
|
|
1804
|
+
gates_file = os.path.join(loki_dir, 'state', 'quality-gates.json')
|
|
1805
|
+
if os.path.isfile(gates_file):
|
|
1806
|
+
try:
|
|
1807
|
+
with open(gates_file) as f:
|
|
1808
|
+
gates = json.load(f)
|
|
1809
|
+
if isinstance(gates, dict):
|
|
1810
|
+
for k, v in gates.items():
|
|
1811
|
+
if isinstance(v, dict):
|
|
1812
|
+
gates_total += 1
|
|
1813
|
+
if v.get('passed') or v.get('status') == 'passed':
|
|
1814
|
+
gates_passed += 1
|
|
1815
|
+
elif isinstance(v, bool):
|
|
1816
|
+
gates_total += 1
|
|
1817
|
+
if v:
|
|
1818
|
+
gates_passed += 1
|
|
1819
|
+
elif isinstance(gates, list):
|
|
1820
|
+
for g in gates:
|
|
1821
|
+
gates_total += 1
|
|
1822
|
+
if isinstance(g, dict) and (g.get('passed') or g.get('status') == 'passed'):
|
|
1823
|
+
gates_passed += 1
|
|
1824
|
+
elif g is True:
|
|
1825
|
+
gates_passed += 1
|
|
1826
|
+
except Exception:
|
|
1827
|
+
pass
|
|
1828
|
+
|
|
1829
|
+
# Gate failures
|
|
1830
|
+
gate_failures = {}
|
|
1831
|
+
gf_file = os.path.join(loki_dir, 'quality', 'gate-failure-count.json')
|
|
1832
|
+
if os.path.isfile(gf_file):
|
|
1833
|
+
try:
|
|
1834
|
+
with open(gf_file) as f:
|
|
1835
|
+
gate_failures = json.load(f)
|
|
1836
|
+
if not isinstance(gate_failures, dict):
|
|
1837
|
+
gate_failures = {}
|
|
1838
|
+
except Exception:
|
|
1839
|
+
pass
|
|
1840
|
+
|
|
1841
|
+
# Code reviews
|
|
1842
|
+
reviews_total = 0
|
|
1843
|
+
reviews_approved = 0
|
|
1844
|
+
reviews_revision = 0
|
|
1845
|
+
quality_dir = os.path.join(loki_dir, 'quality')
|
|
1846
|
+
if os.path.isdir(quality_dir):
|
|
1847
|
+
for fname in os.listdir(quality_dir):
|
|
1848
|
+
if not fname.endswith('.json') or fname == 'gate-failure-count.json':
|
|
1849
|
+
continue
|
|
1850
|
+
fpath = os.path.join(quality_dir, fname)
|
|
1851
|
+
try:
|
|
1852
|
+
with open(fpath) as f:
|
|
1853
|
+
rev = json.load(f)
|
|
1854
|
+
if isinstance(rev, dict) and ('verdict' in rev or 'approved' in rev or 'reviewers' in rev):
|
|
1855
|
+
reviews_total += 1
|
|
1856
|
+
verdict = rev.get('verdict', '').lower()
|
|
1857
|
+
if rev.get('approved') or verdict in ('approved', 'approve', 'pass'):
|
|
1858
|
+
reviews_approved += 1
|
|
1859
|
+
elif verdict in ('revision', 'revise', 'changes_requested', 'reject'):
|
|
1860
|
+
reviews_revision += 1
|
|
1861
|
+
except Exception:
|
|
1862
|
+
pass
|
|
1863
|
+
|
|
1864
|
+
# --- Format duration helper ---
|
|
1865
|
+
def fmt_duration(secs):
|
|
1866
|
+
secs = int(secs)
|
|
1867
|
+
if secs < 60:
|
|
1868
|
+
return f'{secs}s'
|
|
1869
|
+
hours = secs // 3600
|
|
1870
|
+
mins = (secs % 3600) // 60
|
|
1871
|
+
secs_rem = secs % 60
|
|
1872
|
+
if hours > 0:
|
|
1873
|
+
return f'{hours}h {mins:02d}m'
|
|
1874
|
+
return f'{mins}m {secs_rem:02d}s'
|
|
1875
|
+
|
|
1876
|
+
def fmt_number(n):
|
|
1877
|
+
return f'{n:,}'
|
|
1878
|
+
|
|
1879
|
+
# --- JSON output ---
|
|
1880
|
+
if show_json:
|
|
1881
|
+
output = {
|
|
1882
|
+
'session': {
|
|
1883
|
+
'iterations': iteration_count,
|
|
1884
|
+
'duration_seconds': total_duration,
|
|
1885
|
+
'phase': phase
|
|
1886
|
+
},
|
|
1887
|
+
'tokens': {
|
|
1888
|
+
'input': total_input,
|
|
1889
|
+
'output': total_output,
|
|
1890
|
+
'total': total_tokens,
|
|
1891
|
+
'cost_usd': round(total_cost, 2)
|
|
1892
|
+
},
|
|
1893
|
+
'quality': {
|
|
1894
|
+
'gates_passed': gates_passed,
|
|
1895
|
+
'gates_total': gates_total,
|
|
1896
|
+
'reviews_total': reviews_total,
|
|
1897
|
+
'reviews_approved': reviews_approved,
|
|
1898
|
+
'reviews_revision': reviews_revision,
|
|
1899
|
+
'gate_failures': gate_failures
|
|
1900
|
+
},
|
|
1901
|
+
'efficiency': {
|
|
1902
|
+
'avg_tokens_per_iteration': round(total_tokens / iteration_count) if iteration_count > 0 else 0,
|
|
1903
|
+
'avg_cost_per_iteration': round(total_cost / iteration_count, 2) if iteration_count > 0 else 0,
|
|
1904
|
+
'avg_duration_per_iteration': round(total_duration / iteration_count, 1) if iteration_count > 0 else 0
|
|
1905
|
+
},
|
|
1906
|
+
'budget': {
|
|
1907
|
+
'used': round(budget_used, 2),
|
|
1908
|
+
'limit': budget_limit,
|
|
1909
|
+
'percent': round((budget_used / budget_limit) * 100, 1) if budget_limit > 0 else 0
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
if show_efficiency:
|
|
1913
|
+
output['iterations'] = []
|
|
1914
|
+
for i, it in enumerate(iterations, 1):
|
|
1915
|
+
output['iterations'].append({
|
|
1916
|
+
'number': i,
|
|
1917
|
+
'input_tokens': it.get('input_tokens', 0),
|
|
1918
|
+
'output_tokens': it.get('output_tokens', 0),
|
|
1919
|
+
'cost_usd': round(it.get('cost_usd', 0), 2),
|
|
1920
|
+
'duration_seconds': it.get('duration_seconds', 0)
|
|
1921
|
+
})
|
|
1922
|
+
print(json.dumps(output, indent=2))
|
|
1923
|
+
sys.exit(0)
|
|
1924
|
+
|
|
1925
|
+
# --- Text output ---
|
|
1926
|
+
print('Loki Mode Session Statistics')
|
|
1927
|
+
print('============================')
|
|
1928
|
+
print()
|
|
1929
|
+
|
|
1930
|
+
# Session
|
|
1931
|
+
print('Session')
|
|
1932
|
+
print(f' Iterations completed: {iteration_count}')
|
|
1933
|
+
print(f' Duration: {fmt_duration(total_duration)}')
|
|
1934
|
+
print(f' Current phase: {phase}')
|
|
1935
|
+
print()
|
|
1936
|
+
|
|
1937
|
+
# Token Usage
|
|
1938
|
+
print('Token Usage')
|
|
1939
|
+
if iterations:
|
|
1940
|
+
print(f' Input tokens: {fmt_number(total_input)}')
|
|
1941
|
+
print(f' Output tokens: {fmt_number(total_output)}')
|
|
1942
|
+
print(f' Total tokens: {fmt_number(total_tokens)}')
|
|
1943
|
+
print(f' Estimated cost: \${total_cost:.2f}')
|
|
1944
|
+
else:
|
|
1945
|
+
print(' N/A (no iteration metrics found)')
|
|
1946
|
+
print()
|
|
1947
|
+
|
|
1948
|
+
# Quality Gates
|
|
1949
|
+
print('Quality Gates')
|
|
1950
|
+
if gates_total > 0:
|
|
1951
|
+
pct = round((gates_passed / gates_total) * 100) if gates_total > 0 else 0
|
|
1952
|
+
print(f' Gates passed: {gates_passed}/{gates_total} ({pct}%)')
|
|
1953
|
+
else:
|
|
1954
|
+
print(' Gates passed: N/A')
|
|
1955
|
+
if reviews_total > 0:
|
|
1956
|
+
parts = []
|
|
1957
|
+
if reviews_approved > 0:
|
|
1958
|
+
parts.append(f'{reviews_approved} approved')
|
|
1959
|
+
if reviews_revision > 0:
|
|
1960
|
+
parts.append(f'{reviews_revision} revision requested')
|
|
1961
|
+
detail = ', '.join(parts) if parts else 'N/A'
|
|
1962
|
+
print(f' Code reviews: {reviews_total} ({detail})')
|
|
1963
|
+
if gate_failures:
|
|
1964
|
+
failure_parts = [f'{k} ({v})' for k, v in gate_failures.items() if v > 0]
|
|
1965
|
+
if failure_parts:
|
|
1966
|
+
print(f' Gate failures: {', '.join(failure_parts)}')
|
|
1967
|
+
print()
|
|
1968
|
+
|
|
1969
|
+
# Efficiency
|
|
1970
|
+
print('Efficiency')
|
|
1971
|
+
if iteration_count > 0 and iterations:
|
|
1972
|
+
avg_tokens = round(total_tokens / iteration_count)
|
|
1973
|
+
avg_cost = total_cost / iteration_count
|
|
1974
|
+
avg_dur = total_duration / iteration_count
|
|
1975
|
+
print(f' Avg tokens/iteration: {fmt_number(avg_tokens)}')
|
|
1976
|
+
print(f' Avg cost/iteration: \${avg_cost:.2f}')
|
|
1977
|
+
print(f' Avg duration/iteration: {fmt_duration(avg_dur)}')
|
|
1978
|
+
else:
|
|
1979
|
+
print(' N/A (no iteration metrics found)')
|
|
1980
|
+
print()
|
|
1981
|
+
|
|
1982
|
+
# Budget
|
|
1983
|
+
print('Budget')
|
|
1984
|
+
if budget_limit > 0:
|
|
1985
|
+
pct = round((budget_used / budget_limit) * 100, 1)
|
|
1986
|
+
print(f' Used: \${budget_used:.2f} / \${budget_limit:.2f} ({pct}%)')
|
|
1987
|
+
elif budget_used > 0:
|
|
1988
|
+
print(f' Used: \${budget_used:.2f} (no limit set)')
|
|
1989
|
+
else:
|
|
1990
|
+
print(' N/A')
|
|
1991
|
+
|
|
1992
|
+
# Per-iteration breakdown
|
|
1993
|
+
if show_efficiency and iterations:
|
|
1994
|
+
print()
|
|
1995
|
+
print('Per-Iteration Breakdown')
|
|
1996
|
+
for i, it in enumerate(iterations, 1):
|
|
1997
|
+
inp = fmt_number(it.get('input_tokens', 0))
|
|
1998
|
+
out = fmt_number(it.get('output_tokens', 0))
|
|
1999
|
+
cost = it.get('cost_usd', 0)
|
|
2000
|
+
dur = fmt_duration(it.get('duration_seconds', 0))
|
|
2001
|
+
print(f' #{i:<3} input: {inp:<10} output: {out:<10} cost: \${cost:.2f} time: {dur}')
|
|
2002
|
+
" "$loki_dir" "$eff_flag" "$json_flag"
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1716
2005
|
# Provider management
|
|
1717
2006
|
cmd_provider() {
|
|
1718
2007
|
local subcommand="${1:-show}"
|
|
@@ -8010,6 +8299,507 @@ for line in sys.stdin:
|
|
|
8010
8299
|
esac
|
|
8011
8300
|
}
|
|
8012
8301
|
|
|
8302
|
+
# Dry-run PRD analysis and cost estimation (v6.18.0)
|
|
8303
|
+
cmd_plan() {
|
|
8304
|
+
local prd_file=""
|
|
8305
|
+
local show_json=false
|
|
8306
|
+
local show_verbose=false
|
|
8307
|
+
|
|
8308
|
+
while [[ $# -gt 0 ]]; do
|
|
8309
|
+
case "$1" in
|
|
8310
|
+
--help|-h)
|
|
8311
|
+
echo -e "${BOLD}loki plan${NC} - Dry-run PRD analysis and cost estimation"
|
|
8312
|
+
echo ""
|
|
8313
|
+
echo "Usage: loki plan <PRD> [options]"
|
|
8314
|
+
echo ""
|
|
8315
|
+
echo "Analyzes a PRD file without executing anything. Outputs complexity,"
|
|
8316
|
+
echo "estimated iterations, token usage, cost, and execution plan."
|
|
8317
|
+
echo ""
|
|
8318
|
+
echo "Options:"
|
|
8319
|
+
echo " --json Machine-readable JSON output"
|
|
8320
|
+
echo " --verbose Show detailed per-iteration breakdown"
|
|
8321
|
+
echo " --help, -h Show this help"
|
|
8322
|
+
echo ""
|
|
8323
|
+
echo "Examples:"
|
|
8324
|
+
echo " loki plan ./prd.md"
|
|
8325
|
+
echo " loki plan ./prd.md --json"
|
|
8326
|
+
echo " loki plan ./prd.md --verbose"
|
|
8327
|
+
return 0
|
|
8328
|
+
;;
|
|
8329
|
+
--json) show_json=true; shift ;;
|
|
8330
|
+
--verbose) show_verbose=true; shift ;;
|
|
8331
|
+
*)
|
|
8332
|
+
if [ -z "$prd_file" ]; then
|
|
8333
|
+
prd_file="$1"
|
|
8334
|
+
fi
|
|
8335
|
+
shift
|
|
8336
|
+
;;
|
|
8337
|
+
esac
|
|
8338
|
+
done
|
|
8339
|
+
|
|
8340
|
+
if [ -z "$prd_file" ]; then
|
|
8341
|
+
echo -e "${RED}Usage: loki plan <PRD file>${NC}"
|
|
8342
|
+
echo "Run 'loki plan --help' for usage."
|
|
8343
|
+
return 1
|
|
8344
|
+
fi
|
|
8345
|
+
|
|
8346
|
+
if [ ! -f "$prd_file" ]; then
|
|
8347
|
+
echo -e "${RED}PRD file not found: $prd_file${NC}"
|
|
8348
|
+
return 1
|
|
8349
|
+
fi
|
|
8350
|
+
|
|
8351
|
+
local prd_path
|
|
8352
|
+
prd_path="$(cd "$(dirname "$prd_file")" && pwd)/$(basename "$prd_file")"
|
|
8353
|
+
|
|
8354
|
+
python3 -c "
|
|
8355
|
+
import json, sys, os, re, math
|
|
8356
|
+
|
|
8357
|
+
prd_path = sys.argv[1]
|
|
8358
|
+
show_json = sys.argv[2] == 'true'
|
|
8359
|
+
show_verbose = sys.argv[3] == 'true'
|
|
8360
|
+
|
|
8361
|
+
# Colors (disabled for JSON mode)
|
|
8362
|
+
if show_json:
|
|
8363
|
+
RED = GREEN = YELLOW = BLUE = CYAN = BOLD = DIM = NC = ''
|
|
8364
|
+
else:
|
|
8365
|
+
RED = '\033[0;31m'
|
|
8366
|
+
GREEN = '\033[0;32m'
|
|
8367
|
+
YELLOW = '\033[1;33m'
|
|
8368
|
+
BLUE = '\033[0;34m'
|
|
8369
|
+
CYAN = '\033[0;36m'
|
|
8370
|
+
BOLD = '\033[1m'
|
|
8371
|
+
DIM = '\033[2m'
|
|
8372
|
+
NC = '\033[0m'
|
|
8373
|
+
|
|
8374
|
+
# --- Read and analyze PRD ---
|
|
8375
|
+
with open(prd_path, 'r') as f:
|
|
8376
|
+
content = f.read()
|
|
8377
|
+
|
|
8378
|
+
prd_words = len(content.split())
|
|
8379
|
+
prd_lines = content.count('\n') + 1
|
|
8380
|
+
|
|
8381
|
+
# Detect PRD format
|
|
8382
|
+
is_json_prd = prd_path.endswith('.json')
|
|
8383
|
+
sections = []
|
|
8384
|
+
features = []
|
|
8385
|
+
endpoints = []
|
|
8386
|
+
integrations = []
|
|
8387
|
+
databases = []
|
|
8388
|
+
ui_components = []
|
|
8389
|
+
|
|
8390
|
+
if is_json_prd:
|
|
8391
|
+
try:
|
|
8392
|
+
prd_data = json.loads(content)
|
|
8393
|
+
features = prd_data.get('features', prd_data.get('requirements', []))
|
|
8394
|
+
if isinstance(features, list):
|
|
8395
|
+
features = [f.get('title', f.get('name', str(f))) if isinstance(f, dict) else str(f) for f in features]
|
|
8396
|
+
else:
|
|
8397
|
+
features = []
|
|
8398
|
+
except Exception:
|
|
8399
|
+
features = []
|
|
8400
|
+
else:
|
|
8401
|
+
# Markdown analysis
|
|
8402
|
+
for line in content.split('\n'):
|
|
8403
|
+
stripped = line.strip()
|
|
8404
|
+
if stripped.startswith('## '):
|
|
8405
|
+
sections.append(stripped[3:].strip())
|
|
8406
|
+
elif stripped.startswith('### '):
|
|
8407
|
+
sections.append(stripped[4:].strip())
|
|
8408
|
+
|
|
8409
|
+
# Count features: headers + checkboxes
|
|
8410
|
+
feature_patterns = re.findall(r'^(?:##?\s+|[-*]\s+\[.\]\s+)(.+)', content, re.MULTILINE)
|
|
8411
|
+
features = feature_patterns
|
|
8412
|
+
|
|
8413
|
+
# Count API endpoints
|
|
8414
|
+
endpoint_patterns = re.findall(
|
|
8415
|
+
r'(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+[/\w{}:.-]+',
|
|
8416
|
+
content, re.IGNORECASE
|
|
8417
|
+
)
|
|
8418
|
+
endpoints = endpoint_patterns
|
|
8419
|
+
|
|
8420
|
+
# Count external integrations
|
|
8421
|
+
integration_keywords = [
|
|
8422
|
+
'oauth', 'saml', 'oidc', 'stripe', 'twilio', 'sendgrid',
|
|
8423
|
+
'aws', 'gcp', 'azure', 'firebase', 's3', 'redis', 'kafka',
|
|
8424
|
+
'elasticsearch', 'rabbitmq', 'webhook', 'socket.io', 'websocket',
|
|
8425
|
+
'graphql', 'grpc', 'docker', 'kubernetes', 'k8s'
|
|
8426
|
+
]
|
|
8427
|
+
content_lower = content.lower()
|
|
8428
|
+
for kw in integration_keywords:
|
|
8429
|
+
if kw in content_lower:
|
|
8430
|
+
integrations.append(kw)
|
|
8431
|
+
|
|
8432
|
+
# Count database mentions
|
|
8433
|
+
db_keywords = ['postgresql', 'postgres', 'mysql', 'mongodb', 'sqlite',
|
|
8434
|
+
'dynamodb', 'cassandra', 'redis', 'prisma', 'typeorm',
|
|
8435
|
+
'sequelize', 'drizzle', 'knex', 'database', 'migration']
|
|
8436
|
+
for kw in db_keywords:
|
|
8437
|
+
if kw in content_lower:
|
|
8438
|
+
databases.append(kw)
|
|
8439
|
+
|
|
8440
|
+
# Count UI components
|
|
8441
|
+
ui_keywords = ['dashboard', 'form', 'table', 'modal', 'navbar',
|
|
8442
|
+
'sidebar', 'chart', 'graph', 'calendar', 'kanban',
|
|
8443
|
+
'drag.and.drop', 'responsive', 'mobile', 'animation']
|
|
8444
|
+
for kw in ui_keywords:
|
|
8445
|
+
if re.search(kw, content_lower):
|
|
8446
|
+
ui_components.append(kw)
|
|
8447
|
+
|
|
8448
|
+
feature_count = len(features)
|
|
8449
|
+
endpoint_count = len(endpoints)
|
|
8450
|
+
integration_count = len(integrations)
|
|
8451
|
+
db_count = len(databases)
|
|
8452
|
+
ui_count = len(ui_components)
|
|
8453
|
+
section_count = len(sections)
|
|
8454
|
+
|
|
8455
|
+
# --- Determine complexity ---
|
|
8456
|
+
complexity_score = 0
|
|
8457
|
+
complexity_reasons = []
|
|
8458
|
+
|
|
8459
|
+
if prd_words < 200 and feature_count < 5:
|
|
8460
|
+
complexity = 'simple'
|
|
8461
|
+
complexity_reasons.append('Short PRD (<200 words, <5 features)')
|
|
8462
|
+
elif prd_words > 3000 or feature_count > 30:
|
|
8463
|
+
complexity_score += 3
|
|
8464
|
+
complexity_reasons.append(f'Large PRD ({prd_words} words, {feature_count} features)')
|
|
8465
|
+
elif prd_words > 1000 or feature_count > 15:
|
|
8466
|
+
complexity_score += 2
|
|
8467
|
+
complexity_reasons.append(f'Medium PRD ({prd_words} words, {feature_count} features)')
|
|
8468
|
+
else:
|
|
8469
|
+
complexity_score += 1
|
|
8470
|
+
complexity_reasons.append(f'Standard PRD ({prd_words} words, {feature_count} features)')
|
|
8471
|
+
|
|
8472
|
+
if endpoint_count > 20:
|
|
8473
|
+
complexity_score += 2
|
|
8474
|
+
complexity_reasons.append(f'{endpoint_count} API endpoints detected')
|
|
8475
|
+
elif endpoint_count > 5:
|
|
8476
|
+
complexity_score += 1
|
|
8477
|
+
complexity_reasons.append(f'{endpoint_count} API endpoints detected')
|
|
8478
|
+
|
|
8479
|
+
if integration_count > 3:
|
|
8480
|
+
complexity_score += 2
|
|
8481
|
+
complexity_reasons.append(str(integration_count) + ' external integrations: ' + ', '.join(integrations[:5]))
|
|
8482
|
+
elif integration_count > 0:
|
|
8483
|
+
complexity_score += 1
|
|
8484
|
+
complexity_reasons.append(str(integration_count) + ' external integrations: ' + ', '.join(integrations))
|
|
8485
|
+
|
|
8486
|
+
if db_count > 1:
|
|
8487
|
+
complexity_score += 1
|
|
8488
|
+
complexity_reasons.append('Multiple data stores: ' + ', '.join(databases[:4]))
|
|
8489
|
+
|
|
8490
|
+
if ui_count > 5:
|
|
8491
|
+
complexity_score += 1
|
|
8492
|
+
complexity_reasons.append(f'{ui_count} UI components detected')
|
|
8493
|
+
|
|
8494
|
+
# Map score to tier
|
|
8495
|
+
if complexity_score <= 1:
|
|
8496
|
+
complexity = 'simple'
|
|
8497
|
+
elif complexity_score <= 3:
|
|
8498
|
+
complexity = 'moderate'
|
|
8499
|
+
elif complexity_score <= 5:
|
|
8500
|
+
complexity = 'complex'
|
|
8501
|
+
else:
|
|
8502
|
+
complexity = 'enterprise'
|
|
8503
|
+
|
|
8504
|
+
# --- Estimate iterations ---
|
|
8505
|
+
iteration_map = {
|
|
8506
|
+
'simple': (3, 5),
|
|
8507
|
+
'moderate': (6, 12),
|
|
8508
|
+
'complex': (12, 24),
|
|
8509
|
+
'enterprise': (24, 48),
|
|
8510
|
+
}
|
|
8511
|
+
min_iter, max_iter = iteration_map[complexity]
|
|
8512
|
+
|
|
8513
|
+
# Adjust based on specific counts
|
|
8514
|
+
if endpoint_count > 10:
|
|
8515
|
+
max_iter = max(max_iter, max_iter + endpoint_count // 5)
|
|
8516
|
+
if integration_count > 2:
|
|
8517
|
+
max_iter += integration_count
|
|
8518
|
+
|
|
8519
|
+
estimated_iterations = (min_iter + max_iter) // 2
|
|
8520
|
+
|
|
8521
|
+
# --- Token estimation per iteration by RARV tier ---
|
|
8522
|
+
# RARV cycle: 4 iterations per cycle (plan, develop, develop, test)
|
|
8523
|
+
# Tokens estimated per iteration step
|
|
8524
|
+
tokens_per_tier = {
|
|
8525
|
+
'planning': {'input': 50000, 'output': 8000, 'model': 'Opus'},
|
|
8526
|
+
'development': {'input': 80000, 'output': 15000, 'model': 'Sonnet'},
|
|
8527
|
+
'fast': {'input': 30000, 'output': 5000, 'model': 'Haiku'},
|
|
8528
|
+
}
|
|
8529
|
+
|
|
8530
|
+
# Pricing per 1M tokens
|
|
8531
|
+
pricing = {
|
|
8532
|
+
'Opus': {'input': 15.00, 'output': 75.00},
|
|
8533
|
+
'Sonnet': {'input': 3.00, 'output': 15.00},
|
|
8534
|
+
'Haiku': {'input': 0.25, 'output': 1.25},
|
|
8535
|
+
}
|
|
8536
|
+
|
|
8537
|
+
# Build per-iteration plan
|
|
8538
|
+
iteration_plan = []
|
|
8539
|
+
total_input_tokens = 0
|
|
8540
|
+
total_output_tokens = 0
|
|
8541
|
+
total_cost = 0.0
|
|
8542
|
+
tier_totals = {'Opus': 0.0, 'Sonnet': 0.0, 'Haiku': 0.0}
|
|
8543
|
+
tier_iterations = {'Opus': 0, 'Sonnet': 0, 'Haiku': 0}
|
|
8544
|
+
|
|
8545
|
+
for i in range(estimated_iterations):
|
|
8546
|
+
rarv_step = i % 4
|
|
8547
|
+
if rarv_step == 0:
|
|
8548
|
+
tier = 'planning'
|
|
8549
|
+
phase_label = 'Reason (Planning)'
|
|
8550
|
+
elif rarv_step == 1:
|
|
8551
|
+
tier = 'development'
|
|
8552
|
+
phase_label = 'Act (Implementation)'
|
|
8553
|
+
elif rarv_step == 2:
|
|
8554
|
+
tier = 'development'
|
|
8555
|
+
phase_label = 'Reflect (Review)'
|
|
8556
|
+
else:
|
|
8557
|
+
tier = 'fast'
|
|
8558
|
+
phase_label = 'Verify (Testing)'
|
|
8559
|
+
|
|
8560
|
+
info = tokens_per_tier[tier]
|
|
8561
|
+
model = info['model']
|
|
8562
|
+
inp = info['input']
|
|
8563
|
+
out = info['output']
|
|
8564
|
+
|
|
8565
|
+
cost = (inp / 1_000_000) * pricing[model]['input'] + \
|
|
8566
|
+
(out / 1_000_000) * pricing[model]['output']
|
|
8567
|
+
|
|
8568
|
+
total_input_tokens += inp
|
|
8569
|
+
total_output_tokens += out
|
|
8570
|
+
total_cost += cost
|
|
8571
|
+
tier_totals[model] += cost
|
|
8572
|
+
tier_iterations[model] += 1
|
|
8573
|
+
|
|
8574
|
+
iteration_plan.append({
|
|
8575
|
+
'iteration': i + 1,
|
|
8576
|
+
'phase': phase_label,
|
|
8577
|
+
'model': model,
|
|
8578
|
+
'input_tokens': inp,
|
|
8579
|
+
'output_tokens': out,
|
|
8580
|
+
'cost_usd': round(cost, 4),
|
|
8581
|
+
})
|
|
8582
|
+
|
|
8583
|
+
# --- Execution plan (what each cycle tackles) ---
|
|
8584
|
+
execution_phases = []
|
|
8585
|
+
cycle_count = math.ceil(estimated_iterations / 4)
|
|
8586
|
+
phase_names = []
|
|
8587
|
+
|
|
8588
|
+
# Generate phase names based on PRD content
|
|
8589
|
+
if sections:
|
|
8590
|
+
phase_names = sections[:cycle_count]
|
|
8591
|
+
else:
|
|
8592
|
+
# Generic phases based on complexity
|
|
8593
|
+
generic = [
|
|
8594
|
+
'Project setup and scaffolding',
|
|
8595
|
+
'Core data models and database schema',
|
|
8596
|
+
'API endpoints and business logic',
|
|
8597
|
+
'Frontend components and pages',
|
|
8598
|
+
'Authentication and authorization',
|
|
8599
|
+
'External integrations',
|
|
8600
|
+
'Testing and quality assurance',
|
|
8601
|
+
'Performance optimization',
|
|
8602
|
+
'Documentation and deployment',
|
|
8603
|
+
'Edge cases and error handling',
|
|
8604
|
+
'Security hardening',
|
|
8605
|
+
'Final polish and review',
|
|
8606
|
+
]
|
|
8607
|
+
phase_names = generic[:cycle_count]
|
|
8608
|
+
|
|
8609
|
+
for idx, name in enumerate(phase_names):
|
|
8610
|
+
execution_phases.append({
|
|
8611
|
+
'cycle': idx + 1,
|
|
8612
|
+
'iterations': f'{idx * 4 + 1}-{min((idx + 1) * 4, estimated_iterations)}',
|
|
8613
|
+
'focus': name,
|
|
8614
|
+
})
|
|
8615
|
+
|
|
8616
|
+
# --- Time estimation ---
|
|
8617
|
+
# Average ~3.5 minutes per iteration (API calls + processing)
|
|
8618
|
+
minutes_per_iteration = 3.5
|
|
8619
|
+
total_minutes = estimated_iterations * minutes_per_iteration
|
|
8620
|
+
if total_minutes < 60:
|
|
8621
|
+
time_estimate = f'{int(total_minutes)} minutes'
|
|
8622
|
+
elif total_minutes < 120:
|
|
8623
|
+
time_estimate = f'{int(total_minutes // 60)} hour {int(total_minutes % 60)} minutes'
|
|
8624
|
+
else:
|
|
8625
|
+
time_estimate = f'{total_minutes / 60:.1f} hours'
|
|
8626
|
+
|
|
8627
|
+
min_time = min_iter * minutes_per_iteration
|
|
8628
|
+
max_time = max_iter * minutes_per_iteration
|
|
8629
|
+
|
|
8630
|
+
# --- Quality gates ---
|
|
8631
|
+
quality_gates = [
|
|
8632
|
+
{'gate': 'Static Analysis', 'trigger': 'Every iteration', 'applies': True},
|
|
8633
|
+
{'gate': 'Unit Tests', 'trigger': 'Verify phase (every 4th iteration)', 'applies': True},
|
|
8634
|
+
{'gate': 'Code Review (3-reviewer)', 'trigger': 'After implementation cycles', 'applies': complexity in ('complex', 'enterprise')},
|
|
8635
|
+
{'gate': 'Anti-sycophancy Check', 'trigger': 'On unanimous review approval', 'applies': complexity in ('complex', 'enterprise')},
|
|
8636
|
+
{'gate': 'Integration Tests', 'trigger': 'Mid-project and final', 'applies': integration_count > 0 or endpoint_count > 3},
|
|
8637
|
+
{'gate': 'Coverage Gate (>80%)', 'trigger': 'Final verification', 'applies': True},
|
|
8638
|
+
{'gate': 'Security Scan', 'trigger': 'Before completion', 'applies': integration_count > 0},
|
|
8639
|
+
{'gate': 'Performance Benchmark', 'trigger': 'Before completion', 'applies': complexity in ('complex', 'enterprise')},
|
|
8640
|
+
{'gate': 'Completion Council', 'trigger': 'After all phases', 'applies': True},
|
|
8641
|
+
]
|
|
8642
|
+
active_gates = [g for g in quality_gates if g['applies']]
|
|
8643
|
+
|
|
8644
|
+
# --- Provider recommendation ---
|
|
8645
|
+
if integration_count > 3 or complexity in ('complex', 'enterprise'):
|
|
8646
|
+
recommended_provider = 'Claude'
|
|
8647
|
+
provider_reason = 'Full feature support needed (subagents, parallel, MCP)'
|
|
8648
|
+
elif complexity == 'simple' and endpoint_count == 0:
|
|
8649
|
+
recommended_provider = 'Any (Claude/Codex/Gemini)'
|
|
8650
|
+
provider_reason = 'Simple project works with all providers'
|
|
8651
|
+
else:
|
|
8652
|
+
recommended_provider = 'Claude'
|
|
8653
|
+
provider_reason = 'Best iteration quality and tool support'
|
|
8654
|
+
|
|
8655
|
+
# --- Output ---
|
|
8656
|
+
if show_json:
|
|
8657
|
+
result = {
|
|
8658
|
+
'prd_file': prd_path,
|
|
8659
|
+
'prd_stats': {
|
|
8660
|
+
'words': prd_words,
|
|
8661
|
+
'lines': prd_lines,
|
|
8662
|
+
'sections': section_count,
|
|
8663
|
+
'features': feature_count,
|
|
8664
|
+
'endpoints': endpoint_count,
|
|
8665
|
+
'integrations': integrations,
|
|
8666
|
+
'databases': databases,
|
|
8667
|
+
'ui_components': ui_count,
|
|
8668
|
+
},
|
|
8669
|
+
'complexity': {
|
|
8670
|
+
'tier': complexity,
|
|
8671
|
+
'score': complexity_score,
|
|
8672
|
+
'reasons': complexity_reasons,
|
|
8673
|
+
},
|
|
8674
|
+
'iterations': {
|
|
8675
|
+
'estimated': estimated_iterations,
|
|
8676
|
+
'range': [min_iter, max_iter],
|
|
8677
|
+
'rarv_cycles': cycle_count,
|
|
8678
|
+
},
|
|
8679
|
+
'tokens': {
|
|
8680
|
+
'total_input': total_input_tokens,
|
|
8681
|
+
'total_output': total_output_tokens,
|
|
8682
|
+
'total': total_input_tokens + total_output_tokens,
|
|
8683
|
+
},
|
|
8684
|
+
'cost': {
|
|
8685
|
+
'total_usd': round(total_cost, 2),
|
|
8686
|
+
'by_model': {k: round(v, 2) for k, v in tier_totals.items()},
|
|
8687
|
+
'iterations_by_model': tier_iterations,
|
|
8688
|
+
},
|
|
8689
|
+
'time': {
|
|
8690
|
+
'estimated': time_estimate,
|
|
8691
|
+
'minutes': round(total_minutes, 1),
|
|
8692
|
+
'range_minutes': [round(min_time, 1), round(max_time, 1)],
|
|
8693
|
+
},
|
|
8694
|
+
'execution_plan': execution_phases,
|
|
8695
|
+
'quality_gates': [g['gate'] for g in active_gates],
|
|
8696
|
+
'provider': {
|
|
8697
|
+
'recommended': recommended_provider,
|
|
8698
|
+
'reason': provider_reason,
|
|
8699
|
+
},
|
|
8700
|
+
}
|
|
8701
|
+
if show_verbose:
|
|
8702
|
+
result['iteration_details'] = iteration_plan
|
|
8703
|
+
print(json.dumps(result, indent=2))
|
|
8704
|
+
sys.exit(0)
|
|
8705
|
+
|
|
8706
|
+
# --- Formatted output ---
|
|
8707
|
+
print()
|
|
8708
|
+
print(f'{BOLD}PRD Analysis: {os.path.basename(prd_path)}{NC}')
|
|
8709
|
+
print(f'{DIM}' + '=' * 60 + f'{NC}')
|
|
8710
|
+
|
|
8711
|
+
# PRD Stats
|
|
8712
|
+
print(f'\n{CYAN}PRD Statistics{NC}')
|
|
8713
|
+
print(f' Words: {prd_words} | Lines: {prd_lines} | Sections: {section_count}')
|
|
8714
|
+
print(f' Features: {feature_count} | Endpoints: {endpoint_count} | Integrations: {integration_count}')
|
|
8715
|
+
if databases:
|
|
8716
|
+
print(' Data stores: ' + ', '.join(databases[:4]))
|
|
8717
|
+
if ui_count > 0:
|
|
8718
|
+
print(f' UI components: {ui_count}')
|
|
8719
|
+
|
|
8720
|
+
# Complexity
|
|
8721
|
+
color = {'simple': GREEN, 'moderate': YELLOW, 'complex': RED, 'enterprise': RED}
|
|
8722
|
+
cx_color = color.get(complexity, NC)
|
|
8723
|
+
print(f'\n{CYAN}Complexity{NC}')
|
|
8724
|
+
print(f' Tier: {cx_color}{BOLD}{complexity.upper()}{NC} (score: {complexity_score})')
|
|
8725
|
+
for reason in complexity_reasons:
|
|
8726
|
+
print(f' {DIM}- {reason}{NC}')
|
|
8727
|
+
|
|
8728
|
+
# Iterations
|
|
8729
|
+
print(f'\n{CYAN}Estimated Iterations{NC}')
|
|
8730
|
+
print(f' Count: {BOLD}{estimated_iterations}{NC} (range: {min_iter}-{max_iter})')
|
|
8731
|
+
print(f' RARV cycles: {cycle_count} (4 iterations per cycle)')
|
|
8732
|
+
opus_n = tier_iterations.get('Opus', 0)
|
|
8733
|
+
sonnet_n = tier_iterations.get('Sonnet', 0)
|
|
8734
|
+
haiku_n = tier_iterations.get('Haiku', 0)
|
|
8735
|
+
print(f' Model distribution: Opus x{opus_n} | Sonnet x{sonnet_n} | Haiku x{haiku_n}')
|
|
8736
|
+
|
|
8737
|
+
# Tokens
|
|
8738
|
+
total_tok = total_input_tokens + total_output_tokens
|
|
8739
|
+
print(f'\n{CYAN}Token Usage Estimate{NC}')
|
|
8740
|
+
print(f' Input: {total_input_tokens:>12,} tokens')
|
|
8741
|
+
print(f' Output: {total_output_tokens:>12,} tokens')
|
|
8742
|
+
print(f' Total: {total_tok:>12,} tokens')
|
|
8743
|
+
|
|
8744
|
+
# Cost
|
|
8745
|
+
ds = chr(36) # dollar sign
|
|
8746
|
+
print(f'\n{CYAN}Cost Estimate{NC}')
|
|
8747
|
+
print(f' {BOLD}Total: {ds}{total_cost:.2f}{NC}')
|
|
8748
|
+
for model in ['Opus', 'Sonnet', 'Haiku']:
|
|
8749
|
+
pct = (tier_totals[model] / total_cost * 100) if total_cost > 0 else 0
|
|
8750
|
+
bar_len = int(pct / 5)
|
|
8751
|
+
bar = '#' * bar_len + '.' * (20 - bar_len)
|
|
8752
|
+
mc = tier_totals[model]
|
|
8753
|
+
print(' {:>6}: {}{:>7.2f} ({:4.1f}%) [{}]'.format(model, ds, mc, pct, bar))
|
|
8754
|
+
|
|
8755
|
+
# Time
|
|
8756
|
+
print(f'\n{CYAN}Time Estimate{NC}')
|
|
8757
|
+
print(f' Estimated: {BOLD}{time_estimate}{NC}')
|
|
8758
|
+
min_t_str = f'{int(min_time)} min' if min_time < 60 else f'{min_time/60:.1f} hr'
|
|
8759
|
+
max_t_str = f'{int(max_time)} min' if max_time < 60 else f'{max_time/60:.1f} hr'
|
|
8760
|
+
print(f' Range: {min_t_str} - {max_t_str}')
|
|
8761
|
+
|
|
8762
|
+
# Execution plan
|
|
8763
|
+
print(f'\n{CYAN}Execution Plan{NC}')
|
|
8764
|
+
for phase in execution_phases:
|
|
8765
|
+
c_num = phase.get('cycle', '')
|
|
8766
|
+
c_iters = phase.get('iterations', '')
|
|
8767
|
+
c_focus = phase.get('focus', '')
|
|
8768
|
+
print(f' {BOLD}Cycle {c_num}{NC} (iterations {c_iters}): {c_focus}')
|
|
8769
|
+
|
|
8770
|
+
# Quality gates
|
|
8771
|
+
print(f'\n{CYAN}Quality Gates ({len(active_gates)} active){NC}')
|
|
8772
|
+
for g in active_gates:
|
|
8773
|
+
g_name = g.get('gate', '')
|
|
8774
|
+
g_trig = g.get('trigger', '')
|
|
8775
|
+
print(f' {GREEN}[*]{NC} {g_name} -- {DIM}{g_trig}{NC}')
|
|
8776
|
+
|
|
8777
|
+
# Provider recommendation
|
|
8778
|
+
print(f'\n{CYAN}Recommended Provider{NC}')
|
|
8779
|
+
print(f' {BOLD}{recommended_provider}{NC}')
|
|
8780
|
+
print(f' {DIM}{provider_reason}{NC}')
|
|
8781
|
+
|
|
8782
|
+
# Verbose: per-iteration breakdown
|
|
8783
|
+
if show_verbose:
|
|
8784
|
+
print(f'\n{CYAN}Per-Iteration Breakdown{NC}')
|
|
8785
|
+
hdr = ' ' + DIM + '{:>4} {:<24} {:<7} {:>8} {:>8} {:>8}'.format('Iter', 'Phase', 'Model', 'Input', 'Output', 'Cost') + NC
|
|
8786
|
+
print(hdr)
|
|
8787
|
+
print(' ' + DIM + '-' * 65 + NC)
|
|
8788
|
+
for it in iteration_plan:
|
|
8789
|
+
i_num = it.get('iteration', 0)
|
|
8790
|
+
i_phase = it.get('phase', '')
|
|
8791
|
+
i_model = it.get('model', '')
|
|
8792
|
+
i_inp = it.get('input_tokens', 0)
|
|
8793
|
+
i_out = it.get('output_tokens', 0)
|
|
8794
|
+
i_cost = it.get('cost_usd', 0)
|
|
8795
|
+
print(' {:>4} {:<24} {:<7} {:>7,} {:>7,} {}{:>6.2f}'.format(i_num, i_phase, i_model, i_inp, i_out, ds, i_cost))
|
|
8796
|
+
|
|
8797
|
+
print(f'\n{DIM}This is an estimate. Actual usage depends on PRD complexity,')
|
|
8798
|
+
print(f'code review cycles, and test failures.{NC}')
|
|
8799
|
+
print()
|
|
8800
|
+
" "$prd_path" "$show_json" "$show_verbose"
|
|
8801
|
+
}
|
|
8802
|
+
|
|
8013
8803
|
# Main command dispatcher
|
|
8014
8804
|
main() {
|
|
8015
8805
|
if [ $# -eq 0 ]; then
|
|
@@ -8053,6 +8843,9 @@ main() {
|
|
|
8053
8843
|
status)
|
|
8054
8844
|
cmd_status "$@"
|
|
8055
8845
|
;;
|
|
8846
|
+
stats)
|
|
8847
|
+
cmd_stats "$@"
|
|
8848
|
+
;;
|
|
8056
8849
|
dashboard)
|
|
8057
8850
|
cmd_dashboard "$@"
|
|
8058
8851
|
;;
|
|
@@ -8170,6 +8963,9 @@ main() {
|
|
|
8170
8963
|
trigger)
|
|
8171
8964
|
cmd_trigger "$@"
|
|
8172
8965
|
;;
|
|
8966
|
+
plan)
|
|
8967
|
+
cmd_plan "$@"
|
|
8968
|
+
;;
|
|
8173
8969
|
version|--version|-v)
|
|
8174
8970
|
cmd_version
|
|
8175
8971
|
;;
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED