issue-flow 0.12.0 → 0.13.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/README.md +79 -274
- package/dist/agent-BXRIEKVM.js +238 -0
- package/dist/agent-BXRIEKVM.js.map +1 -0
- package/dist/{analyze-OEET7KGE.js → analyze-WS47UYGN.js} +28 -16
- package/dist/analyze-WS47UYGN.js.map +1 -0
- package/dist/{apply-ZIA3C52S.js → apply-OS62UWRN.js} +15 -7
- package/dist/{apply-ZIA3C52S.js.map → apply-OS62UWRN.js.map} +1 -1
- package/dist/bench-SA3BOME5.js +906 -0
- package/dist/bench-SA3BOME5.js.map +1 -0
- package/dist/chunk-37N2RJW2.js +120 -0
- package/dist/chunk-37N2RJW2.js.map +1 -0
- package/dist/chunk-3XLCNVMD.js +180 -0
- package/dist/chunk-3XLCNVMD.js.map +1 -0
- package/dist/chunk-5TB6KLC2.js +72 -0
- package/dist/chunk-5TB6KLC2.js.map +1 -0
- package/dist/{chunk-MJULIKQU.js → chunk-664Y3NDX.js} +23 -47
- package/dist/chunk-664Y3NDX.js.map +1 -0
- package/dist/chunk-6FDFRNXZ.js +357 -0
- package/dist/chunk-6FDFRNXZ.js.map +1 -0
- package/dist/{chunk-56KS3TFQ.js → chunk-7KD66LIM.js} +115 -160
- package/dist/chunk-7KD66LIM.js.map +1 -0
- package/dist/chunk-7Z4Y3VKM.js +26 -0
- package/dist/chunk-7Z4Y3VKM.js.map +1 -0
- package/dist/chunk-ARFVHC2F.js +1111 -0
- package/dist/chunk-ARFVHC2F.js.map +1 -0
- package/dist/{chunk-NS6R37II.js → chunk-BHJCGWQX.js} +141 -12
- package/dist/chunk-BHJCGWQX.js.map +1 -0
- package/dist/chunk-BQDQTIM7.js +74 -0
- package/dist/chunk-BQDQTIM7.js.map +1 -0
- package/dist/chunk-CNBYCMSU.js +2709 -0
- package/dist/chunk-CNBYCMSU.js.map +1 -0
- package/dist/chunk-CRGIQFUQ.js +83 -0
- package/dist/chunk-CRGIQFUQ.js.map +1 -0
- package/dist/chunk-DIG5XKQQ.js +298 -0
- package/dist/chunk-DIG5XKQQ.js.map +1 -0
- package/dist/chunk-FZDR7CGH.js +71 -0
- package/dist/chunk-FZDR7CGH.js.map +1 -0
- package/dist/chunk-HEF2RMKN.js +413 -0
- package/dist/chunk-HEF2RMKN.js.map +1 -0
- package/dist/{chunk-M5UCXTIT.js → chunk-HJGGNLWI.js} +22 -18
- package/dist/chunk-HJGGNLWI.js.map +1 -0
- package/dist/chunk-HQAHNIAR.js +283 -0
- package/dist/chunk-HQAHNIAR.js.map +1 -0
- package/dist/{chunk-FJP3UQGD.js → chunk-IHIEXUTC.js} +39 -19
- package/dist/chunk-IHIEXUTC.js.map +1 -0
- package/dist/{chunk-Y5ZDJ4PJ.js → chunk-IV5SS4JN.js} +18 -10
- package/dist/chunk-IV5SS4JN.js.map +1 -0
- package/dist/{chunk-BYZDPR7N.js → chunk-IYV6KNVF.js} +146 -13
- package/dist/chunk-IYV6KNVF.js.map +1 -0
- package/dist/{chunk-TSOLWK7Y.js → chunk-J62QSVP6.js} +8 -8
- package/dist/chunk-KQHB7ZT3.js +559 -0
- package/dist/chunk-KQHB7ZT3.js.map +1 -0
- package/dist/chunk-L6HSKTQE.js +77 -0
- package/dist/chunk-L6HSKTQE.js.map +1 -0
- package/dist/{chunk-3QHK4YQA.js → chunk-LZ6GFQAG.js} +334 -117
- package/dist/chunk-LZ6GFQAG.js.map +1 -0
- package/dist/{chunk-LAFNLLQP.js → chunk-MIVHK5YI.js} +5 -5
- package/dist/chunk-NEZZ6KDR.js +1217 -0
- package/dist/chunk-NEZZ6KDR.js.map +1 -0
- package/dist/{chunk-NTMMF5UD.js → chunk-OPNGPUKJ.js} +436 -254
- package/dist/chunk-OPNGPUKJ.js.map +1 -0
- package/dist/chunk-P3LRUNC6.js +16 -0
- package/dist/chunk-P3LRUNC6.js.map +1 -0
- package/dist/{chunk-FADTCQGM.js → chunk-PIVGJM6K.js} +94 -32
- package/dist/chunk-PIVGJM6K.js.map +1 -0
- package/dist/{chunk-73ATPIYX.js → chunk-PV4OX5TC.js} +36 -71
- package/dist/chunk-PV4OX5TC.js.map +1 -0
- package/dist/chunk-QCXPNSEL.js +141 -0
- package/dist/chunk-QCXPNSEL.js.map +1 -0
- package/dist/{chunk-XQUSIEKA.js → chunk-QXDNBYQF.js} +52 -14
- package/dist/chunk-QXDNBYQF.js.map +1 -0
- package/dist/chunk-SNTTOQAD.js +30 -0
- package/dist/chunk-SNTTOQAD.js.map +1 -0
- package/dist/{chunk-QCPGHBSZ.js → chunk-TM67EHSV.js} +63 -21
- package/dist/chunk-TM67EHSV.js.map +1 -0
- package/dist/chunk-TVN2JVOF.js +77 -0
- package/dist/chunk-TVN2JVOF.js.map +1 -0
- package/dist/chunk-TX5WIVYP.js +129 -0
- package/dist/chunk-TX5WIVYP.js.map +1 -0
- package/dist/{chunk-A4SLHOW7.js → chunk-UJ7XWK7J.js} +24 -18
- package/dist/chunk-UJ7XWK7J.js.map +1 -0
- package/dist/{chunk-EZATO3DW.js → chunk-UOCPC2Y2.js} +594 -17
- package/dist/chunk-UOCPC2Y2.js.map +1 -0
- package/dist/chunk-XEYH6ZZI.js +289 -0
- package/dist/chunk-XEYH6ZZI.js.map +1 -0
- package/dist/cli.js +264 -44
- package/dist/cli.js.map +1 -1
- package/dist/{config-PRJGWW7J.js → config-MEM2IWWN.js} +28 -5
- package/dist/contract-JH2WBIO6.js +8 -0
- package/dist/conventions-WV4NDXGH.js +143 -0
- package/dist/conventions-WV4NDXGH.js.map +1 -0
- package/dist/execute-NITIHHCK.js +31 -0
- package/dist/{generate-EGB4KV2B.js → generate-MVQI7A45.js} +34 -20
- package/dist/{generate-EGB4KV2B.js.map → generate-MVQI7A45.js.map} +1 -1
- package/dist/git-YRVRUN3C.js +31 -0
- package/dist/init-OKWEC3EV.js +21 -0
- package/dist/{operations-BYT626KQ.js → operations-LU6JA3BX.js} +24 -15
- package/dist/{operations-BYT626KQ.js.map → operations-LU6JA3BX.js.map} +1 -1
- package/dist/permissions-7NMDFU27.js +18 -0
- package/dist/plan-LEJJF3DE.js +31 -0
- package/dist/{policy-6S4GFC76.js → policy-JSYEK5B2.js} +17 -6
- package/dist/policy-JSYEK5B2.js.map +1 -0
- package/dist/pr-HPVUZ7S3.js +36 -0
- package/dist/pr-review-R2QZBT3F.js +29 -0
- package/dist/prd-5O6GYCC6.js +30 -0
- package/dist/ps-TNE44R5V.js +90 -0
- package/dist/ps-TNE44R5V.js.map +1 -0
- package/dist/recorder-XGF55WXI.js +31 -0
- package/dist/recorder-XGF55WXI.js.map +1 -0
- package/dist/registry-LGPE53QD.js +15 -0
- package/dist/registry-LGPE53QD.js.map +1 -0
- package/dist/{resume-PPCNQVVV.js → resume-QMEPMSZW.js} +51 -34
- package/dist/{resume-PPCNQVVV.js.map → resume-QMEPMSZW.js.map} +1 -1
- package/dist/review-YKWD6SNN.js +32 -0
- package/dist/review-YKWD6SNN.js.map +1 -0
- package/dist/routing-2ZUMZPJB.js +91 -0
- package/dist/routing-2ZUMZPJB.js.map +1 -0
- package/dist/run-YU4TR62R.js +50 -0
- package/dist/run-YU4TR62R.js.map +1 -0
- package/dist/{state-manager-2EDVDBAR.js → state-manager-X3PT3FGT.js} +4 -2
- package/dist/state-manager-X3PT3FGT.js.map +1 -0
- package/dist/usage-VCFLDA5Y.js +174 -0
- package/dist/usage-VCFLDA5Y.js.map +1 -0
- package/dist/{web-EF5WIIGG.js → web-XVHLX62B.js} +41 -30
- package/dist/web-XVHLX62B.js.map +1 -0
- package/package.json +6 -2
- package/prompts/execute.md +7 -8
- package/prompts/plan.md +4 -5
- package/prompts/pr.md +2 -2
- package/web/AGENTS.md +3 -3
- package/web/public/app.css +38 -0
- package/web/public/app.js +121 -11
- package/web/public/index.html +24 -0
- package/dist/analyze-OEET7KGE.js.map +0 -1
- package/dist/chunk-3QHK4YQA.js.map +0 -1
- package/dist/chunk-42PJFMAR.js +0 -605
- package/dist/chunk-42PJFMAR.js.map +0 -1
- package/dist/chunk-4JSOYE4G.js +0 -368
- package/dist/chunk-4JSOYE4G.js.map +0 -1
- package/dist/chunk-56KS3TFQ.js.map +0 -1
- package/dist/chunk-73ATPIYX.js.map +0 -1
- package/dist/chunk-A4SLHOW7.js.map +0 -1
- package/dist/chunk-BAUPQXPE.js +0 -173
- package/dist/chunk-BAUPQXPE.js.map +0 -1
- package/dist/chunk-BYZDPR7N.js.map +0 -1
- package/dist/chunk-CD5ULRVC.js +0 -615
- package/dist/chunk-CD5ULRVC.js.map +0 -1
- package/dist/chunk-DIDUVL6O.js +0 -1365
- package/dist/chunk-DIDUVL6O.js.map +0 -1
- package/dist/chunk-EZATO3DW.js.map +0 -1
- package/dist/chunk-FADTCQGM.js.map +0 -1
- package/dist/chunk-FJP3UQGD.js.map +0 -1
- package/dist/chunk-LWRSMTTZ.js +0 -18
- package/dist/chunk-LWRSMTTZ.js.map +0 -1
- package/dist/chunk-M5UCXTIT.js.map +0 -1
- package/dist/chunk-M66SJHQQ.js +0 -206
- package/dist/chunk-M66SJHQQ.js.map +0 -1
- package/dist/chunk-MJULIKQU.js.map +0 -1
- package/dist/chunk-NS6R37II.js.map +0 -1
- package/dist/chunk-NTMMF5UD.js.map +0 -1
- package/dist/chunk-QCPGHBSZ.js.map +0 -1
- package/dist/chunk-XQUSIEKA.js.map +0 -1
- package/dist/chunk-Y5ZDJ4PJ.js.map +0 -1
- package/dist/execute-XBNVL753.js +0 -18
- package/dist/init-AJHCXJBP.js +0 -10
- package/dist/plan-QOZ3RIO6.js +0 -21
- package/dist/policy-6S4GFC76.js.map +0 -1
- package/dist/pr-EBBHD2ZX.js +0 -26
- package/dist/pr-review-VVMW5PO4.js +0 -20
- package/dist/prd-JQSZF7PR.js +0 -21
- package/dist/review-3F6MARWH.js +0 -19
- package/dist/run-RXE5GDQO.js +0 -36
- package/dist/web-EF5WIIGG.js.map +0 -1
- /package/dist/{chunk-TSOLWK7Y.js.map → chunk-J62QSVP6.js.map} +0 -0
- /package/dist/{chunk-LAFNLLQP.js.map → chunk-MIVHK5YI.js.map} +0 -0
- /package/dist/{config-PRJGWW7J.js.map → config-MEM2IWWN.js.map} +0 -0
- /package/dist/{execute-XBNVL753.js.map → contract-JH2WBIO6.js.map} +0 -0
- /package/dist/{init-AJHCXJBP.js.map → execute-NITIHHCK.js.map} +0 -0
- /package/dist/{plan-QOZ3RIO6.js.map → git-YRVRUN3C.js.map} +0 -0
- /package/dist/{pr-EBBHD2ZX.js.map → init-OKWEC3EV.js.map} +0 -0
- /package/dist/{pr-review-VVMW5PO4.js.map → permissions-7NMDFU27.js.map} +0 -0
- /package/dist/{prd-JQSZF7PR.js.map → plan-LEJJF3DE.js.map} +0 -0
- /package/dist/{review-3F6MARWH.js.map → pr-HPVUZ7S3.js.map} +0 -0
- /package/dist/{run-RXE5GDQO.js.map → pr-review-R2QZBT3F.js.map} +0 -0
- /package/dist/{state-manager-2EDVDBAR.js.map → prd-5O6GYCC6.js.map} +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
groupBy,
|
|
4
|
+
summarize
|
|
5
|
+
} from "./chunk-CRGIQFUQ.js";
|
|
6
|
+
import {
|
|
7
|
+
resolveProjectPaths
|
|
8
|
+
} from "./chunk-HQAHNIAR.js";
|
|
9
|
+
import {
|
|
10
|
+
printError,
|
|
11
|
+
printInfo
|
|
12
|
+
} from "./chunk-IYV6KNVF.js";
|
|
13
|
+
import {
|
|
14
|
+
discardedExecutionCount
|
|
15
|
+
} from "./chunk-6FDFRNXZ.js";
|
|
16
|
+
import "./chunk-P3LRUNC6.js";
|
|
17
|
+
import {
|
|
18
|
+
loadTaskPlan
|
|
19
|
+
} from "./chunk-DIG5XKQQ.js";
|
|
20
|
+
import "./chunk-NEZZ6KDR.js";
|
|
21
|
+
import {
|
|
22
|
+
getIssuePaths
|
|
23
|
+
} from "./chunk-TX5WIVYP.js";
|
|
24
|
+
import "./chunk-KQHB7ZT3.js";
|
|
25
|
+
import "./chunk-7Z4Y3VKM.js";
|
|
26
|
+
import "./chunk-XEYH6ZZI.js";
|
|
27
|
+
|
|
28
|
+
// src/commands/usage.ts
|
|
29
|
+
import { readdir } from "fs/promises";
|
|
30
|
+
var USAGE_GROUP_KEYS = [
|
|
31
|
+
"harness",
|
|
32
|
+
"provider",
|
|
33
|
+
"model",
|
|
34
|
+
"purpose",
|
|
35
|
+
"status"
|
|
36
|
+
];
|
|
37
|
+
function sinceCutoff(since) {
|
|
38
|
+
if (since === void 0 || since === "") return null;
|
|
39
|
+
const parsed = Date.parse(since);
|
|
40
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
41
|
+
}
|
|
42
|
+
function afterCutoff(records, cutoff) {
|
|
43
|
+
if (cutoff === null) return [...records];
|
|
44
|
+
return records.filter((record) => Date.parse(record.startedAt) >= cutoff);
|
|
45
|
+
}
|
|
46
|
+
function formatUsd(amount) {
|
|
47
|
+
return `$${amount.toFixed(2)}`;
|
|
48
|
+
}
|
|
49
|
+
function formatCost(summary) {
|
|
50
|
+
const parts = [];
|
|
51
|
+
if (summary.totalCost.reported > 0 || summary.totalCost.estimated === 0) {
|
|
52
|
+
if (summary.count > 0 && summary.totalCost.unknownExecutions < summary.count) {
|
|
53
|
+
parts.push(`${formatUsd(summary.totalCost.reported)} reportado`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (summary.totalCost.estimated > 0) {
|
|
57
|
+
parts.push(`${formatUsd(summary.totalCost.estimated)} estimado`);
|
|
58
|
+
}
|
|
59
|
+
if (summary.totalCost.unknownExecutions > 0) {
|
|
60
|
+
parts.push(`\u2014 desconhecido (${summary.totalCost.unknownExecutions})`);
|
|
61
|
+
}
|
|
62
|
+
return parts.length === 0 ? "\u2014" : parts.join(" \xB7 ");
|
|
63
|
+
}
|
|
64
|
+
function formatTokens(summary) {
|
|
65
|
+
const total = summary.usage.inputTokens + summary.usage.outputTokens;
|
|
66
|
+
if (total >= 1e6) return `${(total / 1e6).toFixed(1)}M`;
|
|
67
|
+
if (total >= 1e3) return `${(total / 1e3).toFixed(1)}k`;
|
|
68
|
+
return String(total);
|
|
69
|
+
}
|
|
70
|
+
function successRate(summary) {
|
|
71
|
+
if (summary.count === 0) return "\u2014";
|
|
72
|
+
const ok = summary.byStatus.completed ?? 0;
|
|
73
|
+
return `${(ok / summary.count * 100).toFixed(1)}%`;
|
|
74
|
+
}
|
|
75
|
+
async function loadPlans(issue) {
|
|
76
|
+
let project;
|
|
77
|
+
try {
|
|
78
|
+
project = await resolveProjectPaths();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
printError(`Not inside a usable project: ${err instanceof Error ? err.message : String(err)}`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
if (issue !== void 0) {
|
|
84
|
+
try {
|
|
85
|
+
const paths = getIssuePaths(project.projectId, issue);
|
|
86
|
+
return [{ id: issue, plan: await loadTaskPlan(paths.tasksFile) }];
|
|
87
|
+
} catch (err) {
|
|
88
|
+
printError(
|
|
89
|
+
`No telemetry for issue ${issue}: ${err instanceof Error ? err.message : String(err)}`
|
|
90
|
+
);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
let ids;
|
|
95
|
+
try {
|
|
96
|
+
ids = await readdir(project.issuesDir);
|
|
97
|
+
} catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const loaded = [];
|
|
101
|
+
for (const id of ids) {
|
|
102
|
+
try {
|
|
103
|
+
loaded.push({
|
|
104
|
+
id,
|
|
105
|
+
plan: await loadTaskPlan(getIssuePaths(project.projectId, id).tasksFile)
|
|
106
|
+
});
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return loaded;
|
|
111
|
+
}
|
|
112
|
+
function buildUsageReport(plans, options, discarded = 0) {
|
|
113
|
+
const cutoff = sinceCutoff(options.since);
|
|
114
|
+
const records = plans.flatMap(({ plan }) => afterCutoff(plan.executions ?? [], cutoff));
|
|
115
|
+
const total = summarize(records, discarded);
|
|
116
|
+
const groups = options.by === void 0 ? [] : [...groupBy(records, options.by)].map(([key, summary]) => ({ key, summary }));
|
|
117
|
+
return {
|
|
118
|
+
issue: options.issue ?? (plans.length === 1 ? plans[0]?.id ?? null : null),
|
|
119
|
+
since: options.since ?? null,
|
|
120
|
+
by: options.by ?? null,
|
|
121
|
+
discarded,
|
|
122
|
+
total,
|
|
123
|
+
groups
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function formatUsageReport(report) {
|
|
127
|
+
if (report.total.count === 0) {
|
|
128
|
+
return "No execution telemetry recorded yet.";
|
|
129
|
+
}
|
|
130
|
+
const header = [
|
|
131
|
+
report.issue === null ? "project" : `issue #${report.issue}`,
|
|
132
|
+
`${report.total.count} execu\xE7\xE3o${report.total.count === 1 ? "" : "\xF5es"}`,
|
|
133
|
+
formatCost(report.total)
|
|
134
|
+
].join(" \xB7 ");
|
|
135
|
+
if (report.by === null || report.groups.length === 0) {
|
|
136
|
+
return header;
|
|
137
|
+
}
|
|
138
|
+
const rows = report.groups.map((group) => {
|
|
139
|
+
const name = group.key.padEnd(14);
|
|
140
|
+
const count = String(group.summary.count).padStart(10);
|
|
141
|
+
const success = successRate(group.summary).padStart(8);
|
|
142
|
+
const tokens = formatTokens(group.summary).padStart(8);
|
|
143
|
+
return `${name}${count}${success}${tokens.padStart(10)} ${formatCost(group.summary)}`;
|
|
144
|
+
});
|
|
145
|
+
const columns = `${report.by.padEnd(14)}${"execu\xE7\xF5es".padStart(10)}${"sucesso".padStart(8)}${"tokens".padStart(10)} custo`;
|
|
146
|
+
return [`${header}`, columns, ...rows].join("\n");
|
|
147
|
+
}
|
|
148
|
+
async function runUsage(issue, options = {}) {
|
|
149
|
+
const plans = await loadPlans(issue ?? options.issue);
|
|
150
|
+
if (plans === null) return 1;
|
|
151
|
+
if (options.since !== void 0 && sinceCutoff(options.since) === null) {
|
|
152
|
+
printError(`Invalid --since date: ${options.since}`);
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
const report = buildUsageReport(plans, { ...options, issue: issue ?? options.issue });
|
|
156
|
+
if (plans.length === 0 || report.total.count === 0 && (issue ?? options.issue) !== void 0) {
|
|
157
|
+
printInfo("No execution telemetry recorded yet.");
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
if (options.json === true) {
|
|
161
|
+
printInfo(JSON.stringify(report, null, 2));
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
printInfo(formatUsageReport(report));
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
USAGE_GROUP_KEYS,
|
|
169
|
+
buildUsageReport,
|
|
170
|
+
discardedExecutionCount,
|
|
171
|
+
formatUsageReport,
|
|
172
|
+
runUsage
|
|
173
|
+
};
|
|
174
|
+
//# sourceMappingURL=usage-VCFLDA5Y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/usage.ts"],"sourcesContent":["import { readdir } from 'node:fs/promises';\nimport { loadTaskPlan } from '../core/state-manager.js';\nimport { getIssuePaths } from '../storage/paths.js';\nimport { resolveProjectPaths } from '../storage/resolve.js';\nimport { type GroupKey, groupBy, summarize } from '../telemetry/aggregate.js';\nimport { discardedExecutionCount } from '../telemetry/recorder.js';\nimport type { ExecutionRecord, ExecutionSummary } from '../telemetry/types.js';\nimport type { TaskPlan } from '../types.js';\nimport { printError, printInfo } from '../ui/logger.js';\n\nexport const USAGE_GROUP_KEYS = [\n 'harness',\n 'provider',\n 'model',\n 'purpose',\n 'status',\n] as const satisfies readonly GroupKey[];\n\nexport type UsageGroupKey = (typeof USAGE_GROUP_KEYS)[number];\n\nexport interface UsageOptions {\n issue?: string;\n since?: string;\n by?: UsageGroupKey;\n json?: boolean;\n}\n\nexport interface UsageReport {\n issue: string | null;\n since: string | null;\n by: UsageGroupKey | null;\n discarded: number;\n total: ExecutionSummary;\n groups: Array<{ key: string; summary: ExecutionSummary }>;\n}\n\nfunction sinceCutoff(since: string | undefined): number | null {\n if (since === undefined || since === '') return null;\n const parsed = Date.parse(since);\n return Number.isNaN(parsed) ? null : parsed;\n}\n\nfunction afterCutoff(\n records: readonly ExecutionRecord[],\n cutoff: number | null,\n): ExecutionRecord[] {\n if (cutoff === null) return [...records];\n return records.filter((record) => Date.parse(record.startedAt) >= cutoff);\n}\n\nfunction formatUsd(amount: number): string {\n return `$${amount.toFixed(2)}`;\n}\n\nfunction formatCost(summary: ExecutionSummary): string {\n const parts: string[] = [];\n if (summary.totalCost.reported > 0 || summary.totalCost.estimated === 0) {\n if (summary.count > 0 && summary.totalCost.unknownExecutions < summary.count) {\n parts.push(`${formatUsd(summary.totalCost.reported)} reportado`);\n }\n }\n if (summary.totalCost.estimated > 0) {\n parts.push(`${formatUsd(summary.totalCost.estimated)} estimado`);\n }\n if (summary.totalCost.unknownExecutions > 0) {\n parts.push(`— desconhecido (${summary.totalCost.unknownExecutions})`);\n }\n return parts.length === 0 ? '—' : parts.join(' · ');\n}\n\nfunction formatTokens(summary: ExecutionSummary): string {\n const total = summary.usage.inputTokens + summary.usage.outputTokens;\n if (total >= 1_000_000) return `${(total / 1_000_000).toFixed(1)}M`;\n if (total >= 1_000) return `${(total / 1_000).toFixed(1)}k`;\n return String(total);\n}\n\nfunction successRate(summary: ExecutionSummary): string {\n if (summary.count === 0) return '—';\n const ok = summary.byStatus.completed ?? 0;\n return `${((ok / summary.count) * 100).toFixed(1)}%`;\n}\n\nasync function loadPlans(\n issue: string | undefined,\n): Promise<{ id: string; plan: TaskPlan }[] | null> {\n let project: Awaited<ReturnType<typeof resolveProjectPaths>>;\n try {\n project = await resolveProjectPaths();\n } catch (err) {\n printError(`Not inside a usable project: ${err instanceof Error ? err.message : String(err)}`);\n return null;\n }\n\n if (issue !== undefined) {\n try {\n const paths = getIssuePaths(project.projectId, issue);\n return [{ id: issue, plan: await loadTaskPlan(paths.tasksFile) }];\n } catch (err) {\n printError(\n `No telemetry for issue ${issue}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\n }\n }\n\n let ids: string[];\n try {\n ids = await readdir(project.issuesDir);\n } catch {\n return [];\n }\n\n const loaded: { id: string; plan: TaskPlan }[] = [];\n for (const id of ids) {\n try {\n loaded.push({\n id,\n plan: await loadTaskPlan(getIssuePaths(project.projectId, id).tasksFile),\n });\n } catch {\n // An issue without a readable plan is not telemetry.\n }\n }\n return loaded;\n}\n\nexport function buildUsageReport(\n plans: { id: string; plan: TaskPlan }[],\n options: UsageOptions,\n discarded = 0,\n): UsageReport {\n const cutoff = sinceCutoff(options.since);\n const records = plans.flatMap(({ plan }) => afterCutoff(plan.executions ?? [], cutoff));\n const total = summarize(records, discarded);\n const groups =\n options.by === undefined\n ? []\n : [...groupBy(records, options.by)].map(([key, summary]) => ({ key, summary }));\n return {\n issue: options.issue ?? (plans.length === 1 ? (plans[0]?.id ?? null) : null),\n since: options.since ?? null,\n by: options.by ?? null,\n discarded,\n total,\n groups,\n };\n}\n\nexport function formatUsageReport(report: UsageReport): string {\n if (report.total.count === 0) {\n return 'No execution telemetry recorded yet.';\n }\n\n const header = [\n report.issue === null ? 'project' : `issue #${report.issue}`,\n `${report.total.count} execução${report.total.count === 1 ? '' : 'ões'}`,\n formatCost(report.total),\n ].join(' · ');\n\n if (report.by === null || report.groups.length === 0) {\n return header;\n }\n\n const rows = report.groups.map((group) => {\n const name = group.key.padEnd(14);\n const count = String(group.summary.count).padStart(10);\n const success = successRate(group.summary).padStart(8);\n const tokens = formatTokens(group.summary).padStart(8);\n return `${name}${count}${success}${tokens.padStart(10)} ${formatCost(group.summary)}`;\n });\n const columns = `${report.by.padEnd(14)}${'execuções'.padStart(10)}${'sucesso'.padStart(8)}${'tokens'.padStart(10)} custo`;\n return [`${header}`, columns, ...rows].join('\\n');\n}\n\nexport async function runUsage(\n issue: string | undefined,\n options: UsageOptions = {},\n): Promise<number> {\n const plans = await loadPlans(issue ?? options.issue);\n if (plans === null) return 1;\n\n if (options.since !== undefined && sinceCutoff(options.since) === null) {\n printError(`Invalid --since date: ${options.since}`);\n return 1;\n }\n\n const report = buildUsageReport(plans, { ...options, issue: issue ?? options.issue });\n if (plans.length === 0 || (report.total.count === 0 && (issue ?? options.issue) !== undefined)) {\n printInfo('No execution telemetry recorded yet.');\n return 0;\n }\n\n if (options.json === true) {\n printInfo(JSON.stringify(report, null, 2));\n return 0;\n }\n\n printInfo(formatUsageReport(report));\n return 0;\n}\n\nexport { discardedExecutionCount };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,eAAe;AAUjB,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAoBA,SAAS,YAAY,OAA0C;AAC7D,MAAI,UAAU,UAAa,UAAU,GAAI,QAAO;AAChD,QAAM,SAAS,KAAK,MAAM,KAAK;AAC/B,SAAO,OAAO,MAAM,MAAM,IAAI,OAAO;AACvC;AAEA,SAAS,YACP,SACA,QACmB;AACnB,MAAI,WAAW,KAAM,QAAO,CAAC,GAAG,OAAO;AACvC,SAAO,QAAQ,OAAO,CAAC,WAAW,KAAK,MAAM,OAAO,SAAS,KAAK,MAAM;AAC1E;AAEA,SAAS,UAAU,QAAwB;AACzC,SAAO,IAAI,OAAO,QAAQ,CAAC,CAAC;AAC9B;AAEA,SAAS,WAAW,SAAmC;AACrD,QAAM,QAAkB,CAAC;AACzB,MAAI,QAAQ,UAAU,WAAW,KAAK,QAAQ,UAAU,cAAc,GAAG;AACvE,QAAI,QAAQ,QAAQ,KAAK,QAAQ,UAAU,oBAAoB,QAAQ,OAAO;AAC5E,YAAM,KAAK,GAAG,UAAU,QAAQ,UAAU,QAAQ,CAAC,YAAY;AAAA,IACjE;AAAA,EACF;AACA,MAAI,QAAQ,UAAU,YAAY,GAAG;AACnC,UAAM,KAAK,GAAG,UAAU,QAAQ,UAAU,SAAS,CAAC,WAAW;AAAA,EACjE;AACA,MAAI,QAAQ,UAAU,oBAAoB,GAAG;AAC3C,UAAM,KAAK,wBAAmB,QAAQ,UAAU,iBAAiB,GAAG;AAAA,EACtE;AACA,SAAO,MAAM,WAAW,IAAI,WAAM,MAAM,KAAK,QAAK;AACpD;AAEA,SAAS,aAAa,SAAmC;AACvD,QAAM,QAAQ,QAAQ,MAAM,cAAc,QAAQ,MAAM;AACxD,MAAI,SAAS,IAAW,QAAO,IAAI,QAAQ,KAAW,QAAQ,CAAC,CAAC;AAChE,MAAI,SAAS,IAAO,QAAO,IAAI,QAAQ,KAAO,QAAQ,CAAC,CAAC;AACxD,SAAO,OAAO,KAAK;AACrB;AAEA,SAAS,YAAY,SAAmC;AACtD,MAAI,QAAQ,UAAU,EAAG,QAAO;AAChC,QAAM,KAAK,QAAQ,SAAS,aAAa;AACzC,SAAO,IAAK,KAAK,QAAQ,QAAS,KAAK,QAAQ,CAAC,CAAC;AACnD;AAEA,eAAe,UACb,OACkD;AAClD,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,oBAAoB;AAAA,EACtC,SAAS,KAAK;AACZ,eAAW,gCAAgC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAC7F,WAAO;AAAA,EACT;AAEA,MAAI,UAAU,QAAW;AACvB,QAAI;AACF,YAAM,QAAQ,cAAc,QAAQ,WAAW,KAAK;AACpD,aAAO,CAAC,EAAE,IAAI,OAAO,MAAM,MAAM,aAAa,MAAM,SAAS,EAAE,CAAC;AAAA,IAClE,SAAS,KAAK;AACZ;AAAA,QACE,0BAA0B,KAAK,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACtF;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,QAAQ,QAAQ,SAAS;AAAA,EACvC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,SAA2C,CAAC;AAClD,aAAW,MAAM,KAAK;AACpB,QAAI;AACF,aAAO,KAAK;AAAA,QACV;AAAA,QACA,MAAM,MAAM,aAAa,cAAc,QAAQ,WAAW,EAAE,EAAE,SAAS;AAAA,MACzE,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,iBACd,OACA,SACA,YAAY,GACC;AACb,QAAM,SAAS,YAAY,QAAQ,KAAK;AACxC,QAAM,UAAU,MAAM,QAAQ,CAAC,EAAE,KAAK,MAAM,YAAY,KAAK,cAAc,CAAC,GAAG,MAAM,CAAC;AACtF,QAAM,QAAQ,UAAU,SAAS,SAAS;AAC1C,QAAM,SACJ,QAAQ,OAAO,SACX,CAAC,IACD,CAAC,GAAG,QAAQ,SAAS,QAAQ,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,KAAK,OAAO,OAAO,EAAE,KAAK,QAAQ,EAAE;AAClF,SAAO;AAAA,IACL,OAAO,QAAQ,UAAU,MAAM,WAAW,IAAK,MAAM,CAAC,GAAG,MAAM,OAAQ;AAAA,IACvE,OAAO,QAAQ,SAAS;AAAA,IACxB,IAAI,QAAQ,MAAM;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEO,SAAS,kBAAkB,QAA6B;AAC7D,MAAI,OAAO,MAAM,UAAU,GAAG;AAC5B,WAAO;AAAA,EACT;AAEA,QAAM,SAAS;AAAA,IACb,OAAO,UAAU,OAAO,YAAY,UAAU,OAAO,KAAK;AAAA,IAC1D,GAAG,OAAO,MAAM,KAAK,kBAAY,OAAO,MAAM,UAAU,IAAI,KAAK,QAAK;AAAA,IACtE,WAAW,OAAO,KAAK;AAAA,EACzB,EAAE,KAAK,QAAK;AAEZ,MAAI,OAAO,OAAO,QAAQ,OAAO,OAAO,WAAW,GAAG;AACpD,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,OAAO,OAAO,IAAI,CAAC,UAAU;AACxC,UAAM,OAAO,MAAM,IAAI,OAAO,EAAE;AAChC,UAAM,QAAQ,OAAO,MAAM,QAAQ,KAAK,EAAE,SAAS,EAAE;AACrD,UAAM,UAAU,YAAY,MAAM,OAAO,EAAE,SAAS,CAAC;AACrD,UAAM,SAAS,aAAa,MAAM,OAAO,EAAE,SAAS,CAAC;AACrD,WAAO,GAAG,IAAI,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,SAAS,EAAE,CAAC,KAAK,WAAW,MAAM,OAAO,CAAC;AAAA,EACtF,CAAC;AACD,QAAM,UAAU,GAAG,OAAO,GAAG,OAAO,EAAE,CAAC,GAAG,kBAAY,SAAS,EAAE,CAAC,GAAG,UAAU,SAAS,CAAC,CAAC,GAAG,SAAS,SAAS,EAAE,CAAC;AAClH,SAAO,CAAC,GAAG,MAAM,IAAI,SAAS,GAAG,IAAI,EAAE,KAAK,IAAI;AAClD;AAEA,eAAsB,SACpB,OACA,UAAwB,CAAC,GACR;AACjB,QAAM,QAAQ,MAAM,UAAU,SAAS,QAAQ,KAAK;AACpD,MAAI,UAAU,KAAM,QAAO;AAE3B,MAAI,QAAQ,UAAU,UAAa,YAAY,QAAQ,KAAK,MAAM,MAAM;AACtE,eAAW,yBAAyB,QAAQ,KAAK,EAAE;AACnD,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,iBAAiB,OAAO,EAAE,GAAG,SAAS,OAAO,SAAS,QAAQ,MAAM,CAAC;AACpF,MAAI,MAAM,WAAW,KAAM,OAAO,MAAM,UAAU,MAAM,SAAS,QAAQ,WAAW,QAAY;AAC9F,cAAU,sCAAsC;AAChD,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,SAAS,MAAM;AACzB,cAAU,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AACzC,WAAO;AAAA,EACT;AAEA,YAAU,kBAAkB,MAAM,CAAC;AACnC,SAAO;AACT;","names":[]}
|
|
@@ -1,34 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
readSessionFile
|
|
4
|
+
} from "./chunk-SNTTOQAD.js";
|
|
2
5
|
import {
|
|
3
6
|
ensureSingleWebServer,
|
|
4
7
|
getWebLockFile,
|
|
5
8
|
readWebLock,
|
|
6
9
|
removeWebLock
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-LAFNLLQP.js";
|
|
10
|
+
} from "./chunk-IHIEXUTC.js";
|
|
11
|
+
import "./chunk-MIVHK5YI.js";
|
|
10
12
|
import {
|
|
11
|
-
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
parseJournal
|
|
14
|
+
} from "./chunk-QCXPNSEL.js";
|
|
13
15
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from "./chunk-DIDUVL6O.js";
|
|
16
|
+
loadWebConfig
|
|
17
|
+
} from "./chunk-CNBYCMSU.js";
|
|
18
|
+
import "./chunk-HQAHNIAR.js";
|
|
18
19
|
import {
|
|
19
20
|
printError,
|
|
20
21
|
printInfo,
|
|
21
22
|
printWarning
|
|
22
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-IYV6KNVF.js";
|
|
24
|
+
import "./chunk-6FDFRNXZ.js";
|
|
25
|
+
import "./chunk-P3LRUNC6.js";
|
|
26
|
+
import "./chunk-DIG5XKQQ.js";
|
|
27
|
+
import "./chunk-NEZZ6KDR.js";
|
|
23
28
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
EVENTS_FILENAME,
|
|
30
|
+
ISSUES_DIR_NAME,
|
|
31
|
+
PROJECTS_DIR_NAME,
|
|
32
|
+
ROTATED_EVENTS_FILENAME,
|
|
33
|
+
SESSION_FILENAME,
|
|
34
|
+
getGlobalRoot
|
|
35
|
+
} from "./chunk-TX5WIVYP.js";
|
|
36
|
+
import "./chunk-KQHB7ZT3.js";
|
|
37
|
+
import "./chunk-7Z4Y3VKM.js";
|
|
38
|
+
import "./chunk-XEYH6ZZI.js";
|
|
26
39
|
|
|
27
40
|
// src/web/session-directory.ts
|
|
28
|
-
import { readdir, readFile
|
|
41
|
+
import { readdir, readFile } from "fs/promises";
|
|
29
42
|
import { join } from "path";
|
|
30
43
|
var DEFAULT_POLL_INTERVAL_MS = 3e3;
|
|
31
|
-
var DEFAULT_STALE_AFTER_MS =
|
|
44
|
+
var DEFAULT_STALE_AFTER_MS = 9e4;
|
|
32
45
|
async function listSubdirectories(dir) {
|
|
33
46
|
try {
|
|
34
47
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -44,27 +57,16 @@ async function discoverSessionFiles(root) {
|
|
|
44
57
|
projectIds.map(async (projectId) => {
|
|
45
58
|
const issuesDir = join(projectsDir, projectId, ISSUES_DIR_NAME);
|
|
46
59
|
const issueIds = await listSubdirectories(issuesDir);
|
|
47
|
-
return issueIds.map((issueId) => join(issuesDir, issueId,
|
|
60
|
+
return issueIds.map((issueId) => join(issuesDir, issueId, SESSION_FILENAME));
|
|
48
61
|
})
|
|
49
62
|
);
|
|
50
63
|
return perProject.flat();
|
|
51
64
|
}
|
|
52
|
-
async function
|
|
53
|
-
let raw;
|
|
54
|
-
let mtimeMs;
|
|
55
|
-
try {
|
|
56
|
-
const [content, stat] = await Promise.all([readFile(filePath, "utf-8"), statFile(filePath)]);
|
|
57
|
-
raw = content;
|
|
58
|
-
mtimeMs = stat.mtimeMs;
|
|
59
|
-
} catch {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
65
|
+
async function readJournalFile(filePath) {
|
|
62
66
|
try {
|
|
63
|
-
|
|
64
|
-
if (!parsed.success || parsed.data.sessionId === null) return null;
|
|
65
|
-
return { snapshot: parsed.data, updatedAtMs: mtimeMs };
|
|
67
|
+
return parseJournal(await readFile(filePath, "utf-8"));
|
|
66
68
|
} catch {
|
|
67
|
-
return
|
|
69
|
+
return [];
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
function watchSessionDirectory(options = {}) {
|
|
@@ -109,6 +111,15 @@ function watchSessionDirectory(options = {}) {
|
|
|
109
111
|
return {
|
|
110
112
|
sessions: () => [...sessions.values()],
|
|
111
113
|
getSession: (sessionId) => sessions.get(sessionId),
|
|
114
|
+
events: async (sessionId) => {
|
|
115
|
+
const session = sessions.get(sessionId);
|
|
116
|
+
if (session === void 0) return void 0;
|
|
117
|
+
const [rotated, current] = await Promise.all([
|
|
118
|
+
readJournalFile(join(session.issueDir, ROTATED_EVENTS_FILENAME)),
|
|
119
|
+
readJournalFile(join(session.issueDir, EVENTS_FILENAME))
|
|
120
|
+
]);
|
|
121
|
+
return [...rotated, ...current];
|
|
122
|
+
},
|
|
112
123
|
refresh: scan,
|
|
113
124
|
close: () => clearInterval(timer)
|
|
114
125
|
};
|
|
@@ -190,4 +201,4 @@ export {
|
|
|
190
201
|
runWebServe,
|
|
191
202
|
runWebStop
|
|
192
203
|
};
|
|
193
|
-
//# sourceMappingURL=web-
|
|
204
|
+
//# sourceMappingURL=web-XVHLX62B.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/web/session-directory.ts","../src/commands/web.ts"],"sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { type JournalEntry, parseJournal } from '../core/journal.js';\nimport type { ValidatedSessionSnapshot } from '../schemas.js';\nimport {\n EVENTS_FILENAME,\n type GetGlobalRootOptions,\n getGlobalRoot,\n ISSUES_DIR_NAME,\n PROJECTS_DIR_NAME,\n ROTATED_EVENTS_FILENAME,\n SESSION_FILENAME,\n} from '../storage/paths.js';\nimport { readSessionFile } from '../storage/session-file.js';\n\n/**\n * Multi-session discovery for the web monitoring server (US-003).\n *\n * The single-instance server (`web/lock.ts`) runs decoupled from any one\n * pipeline invocation, so it cannot hold a `SessionPublisher` in memory for\n * \"the\" run being monitored — there may be zero, one or several running at\n * once, each in its own process. Instead this module polls every\n * `~/.issue-flow/projects/<project>/issues/<n>/session.json` on disk (the\n * same file `FilePublisher` already writes) and keeps an in-memory map of the\n * ones that are both well-formed and recently updated.\n *\n * Polling rather than `fs.watch` is a deliberate choice, not a placeholder:\n * `fs.watch`'s `recursive` option is only reliable on macOS and Windows (Linux\n * needs a manual per-directory watch tree, and the whole thing still degrades\n * to polling on network filesystems), while a `~/.issue-flow` tree is small\n * and local — a cheap poll of it is simpler and behaves identically on every\n * platform, which is what the US-003 acceptance criteria ask to validate.\n */\n\n/** How often the directory tree is rescanned, unless overridden. */\nexport const DEFAULT_POLL_INTERVAL_MS = 3000;\n\n/**\n * A session stops being reported once its file has gone this long without an\n * update. `FilePublisher` touches a live session every 10s without rewriting\n * its content. Three missed heartbeats plus ample scheduling/filesystem slack\n * avoid flapping while still removing an abruptly killed run promptly.\n */\nexport const DEFAULT_STALE_AFTER_MS = 90_000;\n\nexport interface ActiveSession {\n /** Directory the session.json was read from, one level up from the file. */\n issueDir: string;\n filePath: string;\n snapshot: ValidatedSessionSnapshot;\n /** mtime of the file at the last successful read, in epoch ms. */\n updatedAtMs: number;\n}\n\nexport interface SessionDirectoryOptions extends GetGlobalRootOptions {\n /** Poll interval, in ms. Default {@link DEFAULT_POLL_INTERVAL_MS}. */\n pollIntervalMs?: number;\n /** Staleness threshold, in ms. Default {@link DEFAULT_STALE_AFTER_MS}. */\n staleAfterMs?: number;\n /** Invoked (at most once) when a poll fails unexpectedly. Best-effort logging only. */\n onWarn?: (message: string) => void;\n}\n\nexport interface SessionDirectoryHandle {\n /** Snapshot of every session currently considered active. */\n sessions(): ActiveSession[];\n /** A single active session by id, or undefined. */\n getSession(sessionId: string): ActiveSession | undefined;\n /** Journal entries for one active session, oldest generation first. */\n events(sessionId: string): Promise<JournalEntry[] | undefined>;\n /** Force an immediate rescan instead of waiting for the next tick. Never throws. */\n refresh(): Promise<void>;\n /** Stop polling. Idempotent. */\n close(): void;\n}\n\nasync function listSubdirectories(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n // Symlinks are skipped on purpose, same discipline as the storage\n // migration walk: following one could pull session data from outside the\n // global tree into the listing.\n return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);\n } catch {\n // Missing/unreadable projects (or issues) directory reads as \"none yet\" —\n // this is routinely the case before the first --web run ever happens.\n return [];\n }\n}\n\n/** Every `session.json` path currently on disk, one per project/issue pair. */\nasync function discoverSessionFiles(root: string): Promise<string[]> {\n const projectsDir = join(root, PROJECTS_DIR_NAME);\n const projectIds = await listSubdirectories(projectsDir);\n\n const perProject = await Promise.all(\n projectIds.map(async (projectId) => {\n const issuesDir = join(projectsDir, projectId, ISSUES_DIR_NAME);\n const issueIds = await listSubdirectories(issuesDir);\n return issueIds.map((issueId) => join(issuesDir, issueId, SESSION_FILENAME));\n }),\n );\n\n return perProject.flat();\n}\n\nasync function readJournalFile(filePath: string): Promise<JournalEntry[]> {\n try {\n return parseJournal(await readFile(filePath, 'utf-8'));\n } catch {\n return [];\n }\n}\n\n/**\n * Start polling the global storage tree for active sessions.\n *\n * Returns a handle immediately; the first scan happens synchronously in the\n * background and `sessions()` answers `[]` until it completes, same as \"no\n * sessions found yet\" — there is nothing to await here that would not also\n * delay the server's very first bind.\n */\nexport function watchSessionDirectory(\n options: SessionDirectoryOptions = {},\n): SessionDirectoryHandle {\n const root = getGlobalRoot(options);\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;\n const warn = options.onWarn;\n\n // Keyed by sessionId, not by file path: a session's identity is the id\n // inside its content, and that is what /api/status?session=<id> looks up.\n let sessions = new Map<string, ActiveSession>();\n let warned = false;\n\n async function scan(): Promise<void> {\n let files: string[];\n try {\n files = await discoverSessionFiles(root);\n } catch (err) {\n if (!warned) {\n warned = true;\n warn?.(\n `issue-flow: web monitor could not scan the session directory (will keep retrying silently): ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n return;\n }\n\n const now = Date.now();\n const next = new Map<string, ActiveSession>();\n for (const filePath of files) {\n const result = await readSessionFile(filePath);\n if (result === null) continue;\n if (now - result.updatedAtMs > staleAfterMs) continue; // stopped updating: not active\n const sessionId = result.snapshot.sessionId as string;\n next.set(sessionId, {\n issueDir: join(filePath, '..'),\n filePath,\n snapshot: result.snapshot,\n updatedAtMs: result.updatedAtMs,\n });\n }\n sessions = next;\n }\n\n // The first scan runs immediately (not just after the first interval tick)\n // so a server started right after a session begins does not report \"no\n // sessions\" for a whole poll interval.\n void scan();\n const timer = setInterval(() => void scan(), pollIntervalMs);\n timer.unref();\n\n return {\n sessions: () => [...sessions.values()],\n getSession: (sessionId: string) => sessions.get(sessionId),\n events: async (sessionId: string) => {\n const session = sessions.get(sessionId);\n if (session === undefined) return undefined;\n const [rotated, current] = await Promise.all([\n readJournalFile(join(session.issueDir, ROTATED_EVENTS_FILENAME)),\n readJournalFile(join(session.issueDir, EVENTS_FILENAME)),\n ]);\n return [...rotated, ...current];\n },\n refresh: scan,\n close: () => clearInterval(timer),\n };\n}\n","import { loadWebConfig } from '../config.js';\nimport type { WebConfig } from '../schemas.js';\nimport { printError, printInfo, printWarning } from '../ui/logger.js';\nimport { ensureSingleWebServer, getWebLockFile, readWebLock, removeWebLock } from '../web/lock.js';\nimport { watchSessionDirectory } from '../web/session-directory.js';\n\n/**\n * `issue-flow web serve` / `issue-flow web stop` (US-002).\n *\n * `serve` is the process `ensureWebMonitor()` (`web/lock.ts`) spawns detached\n * — it is not meant to be run interactively, though nothing stops a user from\n * doing so to watch every session on the machine without going through a\n * pipeline command first. It never exits on its own: the server stays bound\n * (see `unref: false` in `web/server.ts`) until `stop` sends it `SIGTERM`, at\n * which point `startWebServer`'s own signal handler closes it (removing the\n * lock) and re-raises the signal for the default termination behavior.\n */\n\nconst STOP_POLL_INTERVAL_MS = 100;\nconst STOP_TIMEOUT_MS = 5000;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport interface RunWebServeOptions {\n port?: number;\n host?: string;\n refresh?: number;\n}\n\n/**\n * Bind (or defer to) the single web monitor instance and keep the process\n * alive for as long as it stays bound. Logging is intentionally silent on the\n * happy path: this process is spawned with `stdio: 'ignore'`, so anything\n * printed here goes nowhere — the caller (`ensureWebMonitor`) is the one that\n * tells the user the server is up.\n */\nexport async function runWebServe(options: RunWebServeOptions): Promise<number> {\n // Built conditionally, not `{ port: options.port, ... }`: loadWebConfig()\n // spreads this object directly over the lower-precedence layers, and an\n // explicit `undefined` value would overwrite an env/.issue-flow.json\n // setting instead of falling through to it (only an *absent* key does).\n const cli: Partial<WebConfig> = {};\n if (options.port !== undefined) cli.port = options.port;\n if (options.host !== undefined) cli.host = options.host;\n if (options.refresh !== undefined) cli.refreshSeconds = options.refresh;\n const webConfig = await loadWebConfig({ cli });\n\n const sessions = watchSessionDirectory();\n\n const noop = (): void => {};\n const handle = await ensureSingleWebServer({\n sessions,\n port: webConfig.port,\n host: webConfig.host,\n refreshSeconds: webConfig.refreshSeconds,\n unref: false,\n info: noop,\n warn: noop,\n });\n\n if (handle === null) {\n sessions.close();\n return 1;\n }\n\n if (handle.server === undefined) {\n // Another instance already won the race (or was already running by the\n // time this process got here): nothing to serve, so it exits right away\n // instead of idling as a redundant detached process.\n sessions.close();\n return 0;\n }\n\n const originalClose = handle.close;\n handle.close = async () => {\n await originalClose();\n sessions.close();\n };\n\n return 0;\n}\n\n/** Stop the single running web monitor instance, if any. */\nexport async function runWebStop(): Promise<number> {\n const lockFile = getWebLockFile();\n const lock = await readWebLock(lockFile);\n if (lock === null) {\n printInfo('No web monitor is currently running.');\n return 0;\n }\n\n try {\n process.kill(lock.pid, 'SIGTERM');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ESRCH') {\n printWarning(\n 'The web monitor lock referenced a process that is no longer running; removing it.',\n );\n await removeWebLock(lockFile);\n return 0;\n }\n printError(\n `Failed to stop the web monitor: ${err instanceof Error ? err.message : String(err)}`,\n );\n return 1;\n }\n\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n if ((await readWebLock(lockFile)) === null) {\n printInfo(`Web monitor (pid ${lock.pid}) stopped.`);\n return 0;\n }\n await sleep(STOP_POLL_INTERVAL_MS);\n }\n\n printWarning('Sent the stop signal, but the web monitor did not confirm shutdown in time.');\n return 0;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "issue-flow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Unified CLI for orchestrating the full issue-flow pipeline via Claude Code
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Unified CLI for orchestrating the full issue-flow pipeline via Claude Code, Codex CLI, Cursor CLI or Antigravity CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"issue-flow": "./dist/cli.js"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit",
|
|
13
13
|
"test": "vitest run",
|
|
14
14
|
"test:watch": "vitest",
|
|
15
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
15
16
|
"smoke": "bash scripts/smoke-issue-providers.sh",
|
|
16
17
|
"lint": "biome check src/",
|
|
17
18
|
"lint:fix": "biome check --write src/",
|
|
@@ -34,6 +35,9 @@
|
|
|
34
35
|
"ai",
|
|
35
36
|
"agent",
|
|
36
37
|
"claude",
|
|
38
|
+
"codex",
|
|
39
|
+
"cursor",
|
|
40
|
+
"antigravity",
|
|
37
41
|
"automation",
|
|
38
42
|
"cli",
|
|
39
43
|
"pipeline",
|
package/prompts/execute.md
CHANGED
|
@@ -10,7 +10,7 @@ You are an autonomous coding agent working on a software project.
|
|
|
10
10
|
4. Treat the issue as unresolved if **any** `userStories[].passes` is `false`, **or** if the PRD's top-level `lastReviewFindings` is non-null — that second condition holds even when every story already passes (see "Correction Findings from a Failed Review" below).
|
|
11
11
|
5. If `lastReviewFindings` is non-null, address it first (see below); otherwise pick the **highest priority** user story where `passes: false`
|
|
12
12
|
6. Implement that single user story
|
|
13
|
-
7. Run quality checks (
|
|
13
|
+
7. Run the quality checks that cover the files you changed (typecheck/lint/test for those paths). The orchestrator runs the full suite at the end of execute and again in review.
|
|
14
14
|
8. Update CLAUDE.md files if you discover reusable patterns (see below)
|
|
15
15
|
9. If checks pass, commit ALL changes with message: `__COMMIT_MESSAGE__`
|
|
16
16
|
10. Update the PRD to set `passes: true` for the completed story
|
|
@@ -35,14 +35,14 @@ APPEND to `__PROGRESS_FILE__` (never replace, always append):
|
|
|
35
35
|
## [Date/Time] - [Story ID]
|
|
36
36
|
- What was implemented
|
|
37
37
|
- Files changed
|
|
38
|
-
- **Learnings for future iterations:**
|
|
38
|
+
- **Learnings for future iterations:** (omit this block when the iteration discovered nothing reusable)
|
|
39
39
|
- Patterns discovered (e.g., "this codebase uses X for Y")
|
|
40
40
|
- Gotchas encountered (e.g., "don't forget to update Z when changing W")
|
|
41
41
|
- Useful context (e.g., "the evaluation panel is in component X")
|
|
42
42
|
---
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
The learnings section is
|
|
45
|
+
The learnings section is required only when the iteration discovered something reusable. Do not invent patterns.
|
|
46
46
|
|
|
47
47
|
## Consolidate Patterns
|
|
48
48
|
|
|
@@ -130,7 +130,7 @@ If you need to stop for user guidance or another non-transient blocker, record i
|
|
|
130
130
|
|
|
131
131
|
## Important
|
|
132
132
|
|
|
133
|
-
- Work on ONE story per iteration
|
|
133
|
+
- Work on ONE story per iteration unless `__STORIES_PER_ITERATION__` is greater than 1, in which case you may close that many related stories that touch the same files, in one session. The default is 1.
|
|
134
134
|
- Commit frequently
|
|
135
135
|
- Keep CI green
|
|
136
136
|
- Read the Codebase Patterns section in `__PROGRESS_FILE__` before starting
|
|
@@ -157,8 +157,7 @@ a decision depends on what they say.
|
|
|
157
157
|
|
|
158
158
|
This repository declares: __COMMIT_CONVENTION__
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
corrupts the changelog and any version bump computed from the history.
|
|
160
|
+
The commit message above is already in the resolved format. If you must pick a
|
|
161
|
+
`<type>` inside that format, choose from the repository's vocabulary and match
|
|
162
|
+
the **nature of this commit** — never invent a type outside it.
|
|
164
163
|
<!-- /if -->
|
package/prompts/plan.md
CHANGED
|
@@ -30,7 +30,7 @@ Create a tasks.json file at __TASKS_PATH__ with this exact structure:
|
|
|
30
30
|
"project": "<repo-name>",
|
|
31
31
|
"issueNumber": __ISSUE_NUMBER__,
|
|
32
32
|
"issueUrl": "__ISSUE_URL__",
|
|
33
|
-
"branchName": "
|
|
33
|
+
"branchName": "__BRANCH_NAME__",
|
|
34
34
|
"description": "<brief description>",
|
|
35
35
|
"issueStatus": "pending",
|
|
36
36
|
"completedAt": null,
|
|
@@ -71,7 +71,7 @@ Rules:
|
|
|
71
71
|
and never derive it yourself
|
|
72
72
|
- Get the repo name from the repository itself (package name, directory name)
|
|
73
73
|
- If __ISSUE_NUMBER__ is not a number, keep it as a JSON string in issueNumber
|
|
74
|
-
-
|
|
74
|
+
- branchName is already resolved: use __BRANCH_NAME__ verbatim. Do not invent a branch.
|
|
75
75
|
|
|
76
76
|
IMPORTANT: You MUST write the tasks.json to the file path above. Do not just output it.
|
|
77
77
|
|
|
@@ -94,6 +94,5 @@ a decision depends on what they say.
|
|
|
94
94
|
|
|
95
95
|
## Branch naming
|
|
96
96
|
|
|
97
|
-
`branchName`
|
|
98
|
-
|
|
99
|
-
declares its own convention above overrides this one.
|
|
97
|
+
`branchName` is `__BRANCH_NAME__`. Copy it exactly. The CLI computed it from the
|
|
98
|
+
repository convention; do not slugify the title yourself.
|
package/prompts/pr.md
CHANGED
|
@@ -27,7 +27,7 @@ Steps:
|
|
|
27
27
|
5. Create a well-structured PR using gh pr create
|
|
28
28
|
|
|
29
29
|
The PR should:
|
|
30
|
-
-
|
|
30
|
+
- Use this title verbatim: `__PR_TITLE_CONVENTION__`
|
|
31
31
|
- Include a summary of changes
|
|
32
32
|
- Include a test plan
|
|
33
33
|
- Cite where the demand is described: the reference above (`__ISSUE_URL__`)
|
|
@@ -36,7 +36,7 @@ Issue reference line(s):
|
|
|
36
36
|
- Include the lines below verbatim in the PR body, each entry on its own line,
|
|
37
37
|
with no surrounding code fence or quoting — GitHub only auto-closes an issue
|
|
38
38
|
when the `Closes #N` line is plain body text:
|
|
39
|
-
|
|
39
|
+
__ISSUE_REFERENCE__
|
|
40
40
|
- When nothing follows the line above, the issue has no GitHub counterpart: do
|
|
41
41
|
NOT invent a "Closes #" reference, and cite `__ISSUE_URL__` in the body instead
|
|
42
42
|
|
package/web/AGENTS.md
CHANGED
|
@@ -10,7 +10,7 @@ vitest. Toda verificação de mudança aqui é manual, no navegador.
|
|
|
10
10
|
|
|
11
11
|
O painel consome `GET api/sessions` (lista enriquecida para o dashboard),
|
|
12
12
|
`GET api/status` (o `SessionSnapshot` serializado, opcionalmente com
|
|
13
|
-
`?session=<id>`) e `GET api/health`. Ele precisa renderizar **session.json de
|
|
13
|
+
`?session=<id>`), `GET api/events?session=<id>` (journal) e `GET api/health`. Ele precisa renderizar **session.json de
|
|
14
14
|
execuções antigas**, então todo campo pode chegar como `undefined` (não
|
|
15
15
|
existia na versão que gravou o arquivo) além do `null` (existe, não
|
|
16
16
|
informado). Os dois significam "não informado" e nunca podem virar `0`,
|
|
@@ -36,7 +36,7 @@ do snapshot.
|
|
|
36
36
|
|
|
37
37
|
## Abas, Kanban e drawer
|
|
38
38
|
|
|
39
|
-
O painel tem
|
|
39
|
+
O painel tem três abas ("Execução", "Kanban" e "Histórico") e um drawer de detalhes por
|
|
40
40
|
story. Três regras seguram esse conjunto:
|
|
41
41
|
|
|
42
42
|
- **Acesso a story sempre por `getStoryById()` / `getStories()`.** Elas são a
|
|
@@ -51,7 +51,7 @@ story. Três regras seguram esse conjunto:
|
|
|
51
51
|
então uma referência guardada na abertura apontaria para um nó fora do
|
|
52
52
|
documento. Pelo mesmo motivo o foco volta ao card via
|
|
53
53
|
`[data-story-id="…"]` no momento de fechar.
|
|
54
|
-
- **`renderKanban()` e `renderDrawer()` são chamadas incondicionalmente** dentro
|
|
54
|
+
- **`renderKanban()`, `renderHistory()` e `renderDrawer()` são chamadas incondicionalmente** dentro
|
|
55
55
|
de `render()`, não ao trocar de aba. Uma aba inativa não pode ficar defasada,
|
|
56
56
|
e é o `renderDrawer()` de cada poll que mantém o drawer aberto em dia (e o
|
|
57
57
|
fecha quando a story some do plano).
|
package/web/public/app.css
CHANGED
|
@@ -47,6 +47,10 @@
|
|
|
47
47
|
box-sizing: border-box;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
[hidden] {
|
|
51
|
+
display: none !important;
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
body {
|
|
51
55
|
margin: 0;
|
|
52
56
|
background: var(--bg);
|
|
@@ -454,6 +458,34 @@ progress {
|
|
|
454
458
|
border-bottom: 1px solid var(--border);
|
|
455
459
|
}
|
|
456
460
|
|
|
461
|
+
.plain-list .history-entry {
|
|
462
|
+
align-items: flex-start;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.history-type {
|
|
466
|
+
flex: 0 0 9rem;
|
|
467
|
+
color: var(--muted);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.history-message {
|
|
471
|
+
min-width: 0;
|
|
472
|
+
overflow-wrap: anywhere;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@media (max-width: 560px) {
|
|
476
|
+
.plain-list .history-entry {
|
|
477
|
+
flex-wrap: wrap;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.history-type {
|
|
481
|
+
flex-basis: auto;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.history-message {
|
|
485
|
+
flex-basis: 100%;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
457
489
|
.plain-list li:last-child {
|
|
458
490
|
border-bottom: none;
|
|
459
491
|
}
|
|
@@ -904,6 +936,12 @@ progress {
|
|
|
904
936
|
font: inherit;
|
|
905
937
|
cursor: pointer;
|
|
906
938
|
display: grid;
|
|
939
|
+
/* O grid externo estica os cards de uma linha para a mesma altura. Sem isto,
|
|
940
|
+
`align-content: stretch` distribui a sobra entre as faixas — inclusive a do
|
|
941
|
+
resumo — e a faixa fica mais alta que as 3 linhas do clamp, deixando vazar
|
|
942
|
+
uma 4ª linha cortada por baixo do reticente. Empacotar no topo manda a
|
|
943
|
+
sobra para o fim do card, que é onde ela não engana ninguém. */
|
|
944
|
+
align-content: start;
|
|
907
945
|
gap: 10px;
|
|
908
946
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
909
947
|
}
|