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,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
MemoryPublisher
|
|
4
|
+
} from "./chunk-IYV6KNVF.js";
|
|
5
|
+
|
|
6
|
+
// src/core/journal.ts
|
|
7
|
+
import { appendFile, mkdir, rename, stat, unlink } from "fs/promises";
|
|
8
|
+
import { dirname } from "path";
|
|
9
|
+
var DEFAULT_JOURNAL_MAX_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
var JournalPublisher = class extends MemoryPublisher {
|
|
11
|
+
filePath;
|
|
12
|
+
rotatedPath;
|
|
13
|
+
maxFileBytes;
|
|
14
|
+
seq = 0;
|
|
15
|
+
writeChain = Promise.resolve();
|
|
16
|
+
closed = false;
|
|
17
|
+
/** Bytes in the current file. `null` until the first write measures it. */
|
|
18
|
+
bytes = null;
|
|
19
|
+
constructor(filePath, rotatedPath, options = {}) {
|
|
20
|
+
super(options);
|
|
21
|
+
this.filePath = filePath;
|
|
22
|
+
this.rotatedPath = rotatedPath;
|
|
23
|
+
this.maxFileBytes = options.maxFileBytes ?? DEFAULT_JOURNAL_MAX_BYTES;
|
|
24
|
+
}
|
|
25
|
+
afterPublish(event) {
|
|
26
|
+
if (this.closed) return;
|
|
27
|
+
this.seq++;
|
|
28
|
+
const line = `${JSON.stringify({ seq: this.seq, event })}
|
|
29
|
+
`;
|
|
30
|
+
this.enqueue(line);
|
|
31
|
+
}
|
|
32
|
+
enqueue(line) {
|
|
33
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
34
|
+
try {
|
|
35
|
+
await this.appendLine(line);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
this.warnOnce(err);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async appendLine(line) {
|
|
42
|
+
const size = Buffer.byteLength(line);
|
|
43
|
+
if (this.bytes === null) {
|
|
44
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
45
|
+
this.bytes = await currentSize(this.filePath);
|
|
46
|
+
}
|
|
47
|
+
if (this.maxFileBytes > 0 && this.bytes > 0 && this.bytes + size > this.maxFileBytes) {
|
|
48
|
+
await this.rotate();
|
|
49
|
+
}
|
|
50
|
+
await appendFile(this.filePath, line, "utf-8");
|
|
51
|
+
this.bytes += size;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Keep exactly one previous generation.
|
|
55
|
+
*
|
|
56
|
+
* Deliberately not a numbered cascade: two files bound the disk cost at
|
|
57
|
+
* `2 * maxFileBytes`, and a run long enough to fill 10 MB twice has already
|
|
58
|
+
* put everything a human would look for in the current file. A caller that
|
|
59
|
+
* needs full retention sets `maxFileBytes: 0` and rotates on its own terms.
|
|
60
|
+
*/
|
|
61
|
+
async rotate() {
|
|
62
|
+
try {
|
|
63
|
+
await unlink(this.rotatedPath);
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
await rename(this.filePath, this.rotatedPath);
|
|
67
|
+
this.bytes = 0;
|
|
68
|
+
}
|
|
69
|
+
async flush() {
|
|
70
|
+
await this.writeChain;
|
|
71
|
+
}
|
|
72
|
+
async close() {
|
|
73
|
+
await this.flush();
|
|
74
|
+
this.closed = true;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
async function currentSize(path) {
|
|
78
|
+
try {
|
|
79
|
+
return (await stat(path)).size;
|
|
80
|
+
} catch {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function parseJournal(content) {
|
|
85
|
+
const entries = [];
|
|
86
|
+
for (const line of content.split("\n")) {
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (trimmed === "") continue;
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(trimmed);
|
|
92
|
+
} catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!isEntry(parsed)) continue;
|
|
96
|
+
entries.push(parsed);
|
|
97
|
+
}
|
|
98
|
+
return entries;
|
|
99
|
+
}
|
|
100
|
+
function isEntry(value) {
|
|
101
|
+
if (typeof value !== "object" || value === null) return false;
|
|
102
|
+
const candidate = value;
|
|
103
|
+
if (typeof candidate.seq !== "number") return false;
|
|
104
|
+
const event = candidate.event;
|
|
105
|
+
return typeof event === "object" && event !== null && typeof event.type === "string";
|
|
106
|
+
}
|
|
107
|
+
var MultiPublisher = class {
|
|
108
|
+
members;
|
|
109
|
+
primary;
|
|
110
|
+
constructor(members) {
|
|
111
|
+
if (members.length === 0) {
|
|
112
|
+
throw new Error("MultiPublisher needs at least one member");
|
|
113
|
+
}
|
|
114
|
+
this.members = members;
|
|
115
|
+
this.primary = members[0];
|
|
116
|
+
}
|
|
117
|
+
publish(event) {
|
|
118
|
+
for (const member of this.members) {
|
|
119
|
+
member.publish(event);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
snapshot() {
|
|
123
|
+
return this.primary.snapshot();
|
|
124
|
+
}
|
|
125
|
+
version() {
|
|
126
|
+
return this.primary.version();
|
|
127
|
+
}
|
|
128
|
+
async flush() {
|
|
129
|
+
await Promise.all(this.members.map((member) => member.flush()));
|
|
130
|
+
}
|
|
131
|
+
async close() {
|
|
132
|
+
await Promise.all(this.members.map((member) => member.close()));
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export {
|
|
137
|
+
JournalPublisher,
|
|
138
|
+
parseJournal,
|
|
139
|
+
MultiPublisher
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=chunk-QCXPNSEL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/journal.ts"],"sourcesContent":["import { appendFile, mkdir, rename, stat, unlink } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport {\n createInitialSnapshot,\n MemoryPublisher,\n type MemoryPublisherOptions,\n reduceSessionEvent,\n type SessionEvent,\n type SessionPublisher,\n type SessionReducerOptions,\n type SessionSnapshot,\n} from './session-state.js';\n\n/**\n * Append-only journal of `SessionEvent`s.\n *\n * `session.json` is a *projection*: the reducer folds every event into one\n * snapshot and the events themselves are discarded. That is the right shape for\n * a dashboard and the wrong one for an audit — after a six-hour run, \"what\n * happened at 3am\" has no answer, because the retries, the failures and their\n * order were consumed to produce a single final state.\n *\n * This publisher writes the events instead of the state: one JSON line each, in\n * order, with a monotonic `seq`. It sits **beside** `FilePublisher`, never in\n * its place. The snapshot stays the projection; the journal is the history, and\n * replaying it through `reduceSessionEvent` reproduces the snapshot — which is\n * the property `replayJournal()` and its test exist to keep true.\n *\n * The invariant of `session-state.ts` holds here unchanged: `publish()` is\n * synchronous, never throws, and every I/O failure is swallowed after a single\n * warning. Monitoring never affects the pipeline.\n */\n\n/** One line of `events.jsonl`. */\nexport interface JournalEntry {\n /** 1-based, monotonic within a file *and* across a rotation. */\n seq: number;\n event: SessionEvent;\n}\n\n/** Default ceiling before the journal rotates. */\nexport const DEFAULT_JOURNAL_MAX_BYTES = 10 * 1024 * 1024;\n\nexport interface JournalPublisherOptions extends MemoryPublisherOptions {\n /**\n * Rotate once the file would grow past this. Default 10 MB. `0` disables\n * rotation, which is what a caller that manages its own retention wants.\n */\n maxFileBytes?: number;\n}\n\n/**\n * Publisher that appends every event to `events.jsonl`.\n *\n * Writes are serialized on a promise chain — the same discipline as\n * `FilePublisher` — so lines never interleave and the order on disk is the\n * order of `publish()`. Nothing is throttled: dropping or coalescing events is\n * exactly what the snapshot already does, and the point of the journal is that\n * it does not.\n */\nexport class JournalPublisher extends MemoryPublisher {\n private readonly filePath: string;\n private readonly rotatedPath: string;\n private readonly maxFileBytes: number;\n private seq = 0;\n private writeChain: Promise<void> = Promise.resolve();\n private closed = false;\n /** Bytes in the current file. `null` until the first write measures it. */\n private bytes: number | null = null;\n\n constructor(filePath: string, rotatedPath: string, options: JournalPublisherOptions = {}) {\n super(options);\n this.filePath = filePath;\n this.rotatedPath = rotatedPath;\n this.maxFileBytes = options.maxFileBytes ?? DEFAULT_JOURNAL_MAX_BYTES;\n }\n\n protected override afterPublish(event: SessionEvent): void {\n if (this.closed) return;\n this.seq++;\n const line = `${JSON.stringify({ seq: this.seq, event } satisfies JournalEntry)}\\n`;\n this.enqueue(line);\n }\n\n private enqueue(line: string): void {\n this.writeChain = this.writeChain.then(async () => {\n try {\n await this.appendLine(line);\n } catch (err) {\n this.warnOnce(err);\n }\n });\n }\n\n private async appendLine(line: string): Promise<void> {\n const size = Buffer.byteLength(line);\n\n if (this.bytes === null) {\n await mkdir(dirname(this.filePath), { recursive: true });\n this.bytes = await currentSize(this.filePath);\n }\n\n // Rotate *before* the write that would cross the ceiling, so a line is\n // never split across two files: an entry that cannot be parsed whole is\n // worse than a file slightly under its limit.\n if (this.maxFileBytes > 0 && this.bytes > 0 && this.bytes + size > this.maxFileBytes) {\n await this.rotate();\n }\n\n await appendFile(this.filePath, line, 'utf-8');\n this.bytes += size;\n }\n\n /**\n * Keep exactly one previous generation.\n *\n * Deliberately not a numbered cascade: two files bound the disk cost at\n * `2 * maxFileBytes`, and a run long enough to fill 10 MB twice has already\n * put everything a human would look for in the current file. A caller that\n * needs full retention sets `maxFileBytes: 0` and rotates on its own terms.\n */\n private async rotate(): Promise<void> {\n try {\n await unlink(this.rotatedPath);\n } catch {\n // No previous generation. Nothing to remove.\n }\n await rename(this.filePath, this.rotatedPath);\n this.bytes = 0;\n }\n\n override async flush(): Promise<void> {\n await this.writeChain;\n }\n\n override async close(): Promise<void> {\n await this.flush();\n this.closed = true;\n }\n}\n\n/** Size of `path` in bytes, or `0` when it does not exist yet. */\nasync function currentSize(path: string): Promise<number> {\n try {\n return (await stat(path)).size;\n } catch {\n return 0;\n }\n}\n\n/**\n * Parse the lines of a journal, skipping what cannot be read.\n *\n * A journal is appended to by a live process and read by another, so the last\n * line may well be a partial write. Tolerating it — and any line a newer\n * release wrote in a shape this one does not know — is what makes the file\n * usable at all; the alternative is a reader that throws on a healthy run.\n */\nexport function parseJournal(content: string): JournalEntry[] {\n const entries: JournalEntry[] = [];\n\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '') continue;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n\n if (!isEntry(parsed)) continue;\n entries.push(parsed);\n }\n\n return entries;\n}\n\nfunction isEntry(value: unknown): value is JournalEntry {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as { seq?: unknown; event?: unknown };\n if (typeof candidate.seq !== 'number') return false;\n const event = candidate.event;\n return (\n typeof event === 'object' && event !== null && typeof (event as SessionEvent).type === 'string'\n );\n}\n\n/**\n * Rebuild the snapshot from a journal.\n *\n * This is the reproducibility contract of the file: the same reducer that\n * produced `session.json` live, run over the events in the order they were\n * written, must land on the same state. A journal that cannot do that is a log,\n * not a journal.\n */\nexport function replayJournal(\n content: string,\n options: SessionReducerOptions = {},\n): SessionSnapshot {\n let snapshot = createInitialSnapshot();\n for (const entry of parseJournal(content)) {\n snapshot = reduceSessionEvent(snapshot, entry.event, options);\n }\n return snapshot;\n}\n\n/**\n * Fan one event stream out to several publishers.\n *\n * The journal sits **beside** the snapshot writer, so something has to hold\n * both. It is the composite rather than the journal that does it, because the\n * relationship is symmetric: neither surface owns the other, and dropping\n * either one has to leave the remaining one untouched.\n *\n * `snapshot()` and `version()` answer from the first member, which is the\n * primary surface. A member that fails has already swallowed its own error —\n * that is the contract of every publisher here — so the loop needs no guard of\n * its own beyond `publish()` never throwing.\n */\nexport class MultiPublisher implements SessionPublisher {\n private readonly members: readonly SessionPublisher[];\n private readonly primary: SessionPublisher;\n\n constructor(members: readonly SessionPublisher[]) {\n if (members.length === 0) {\n throw new Error('MultiPublisher needs at least one member');\n }\n this.members = members;\n this.primary = members[0] as SessionPublisher;\n }\n\n publish(event: SessionEvent): void {\n for (const member of this.members) {\n member.publish(event);\n }\n }\n\n snapshot(): SessionSnapshot {\n return this.primary.snapshot();\n }\n\n version(): number {\n return this.primary.version();\n }\n\n async flush(): Promise<void> {\n await Promise.all(this.members.map((member) => member.flush()));\n }\n\n async close(): Promise<void> {\n await Promise.all(this.members.map((member) => member.close()));\n }\n}\n"],"mappings":";;;;;;AAAA,SAAS,YAAY,OAAO,QAAQ,MAAM,cAAc;AACxD,SAAS,eAAe;AAwCjB,IAAM,4BAA4B,KAAK,OAAO;AAmB9C,IAAM,mBAAN,cAA+B,gBAAgB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACT,MAAM;AAAA,EACN,aAA4B,QAAQ,QAAQ;AAAA,EAC5C,SAAS;AAAA;AAAA,EAET,QAAuB;AAAA,EAE/B,YAAY,UAAkB,aAAqB,UAAmC,CAAC,GAAG;AACxF,UAAM,OAAO;AACb,SAAK,WAAW;AAChB,SAAK,cAAc;AACnB,SAAK,eAAe,QAAQ,gBAAgB;AAAA,EAC9C;AAAA,EAEmB,aAAa,OAA2B;AACzD,QAAI,KAAK,OAAQ;AACjB,SAAK;AACL,UAAM,OAAO,GAAG,KAAK,UAAU,EAAE,KAAK,KAAK,KAAK,MAAM,CAAwB,CAAC;AAAA;AAC/E,SAAK,QAAQ,IAAI;AAAA,EACnB;AAAA,EAEQ,QAAQ,MAAoB;AAClC,SAAK,aAAa,KAAK,WAAW,KAAK,YAAY;AACjD,UAAI;AACF,cAAM,KAAK,WAAW,IAAI;AAAA,MAC5B,SAAS,KAAK;AACZ,aAAK,SAAS,GAAG;AAAA,MACnB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,WAAW,MAA6B;AACpD,UAAM,OAAO,OAAO,WAAW,IAAI;AAEnC,QAAI,KAAK,UAAU,MAAM;AACvB,YAAM,MAAM,QAAQ,KAAK,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AACvD,WAAK,QAAQ,MAAM,YAAY,KAAK,QAAQ;AAAA,IAC9C;AAKA,QAAI,KAAK,eAAe,KAAK,KAAK,QAAQ,KAAK,KAAK,QAAQ,OAAO,KAAK,cAAc;AACpF,YAAM,KAAK,OAAO;AAAA,IACpB;AAEA,UAAM,WAAW,KAAK,UAAU,MAAM,OAAO;AAC7C,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,SAAwB;AACpC,QAAI;AACF,YAAM,OAAO,KAAK,WAAW;AAAA,IAC/B,QAAQ;AAAA,IAER;AACA,UAAM,OAAO,KAAK,UAAU,KAAK,WAAW;AAC5C,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,MAAe,QAAuB;AACpC,UAAM,KAAK;AAAA,EACb;AAAA,EAEA,MAAe,QAAuB;AACpC,UAAM,KAAK,MAAM;AACjB,SAAK,SAAS;AAAA,EAChB;AACF;AAGA,eAAe,YAAY,MAA+B;AACxD,MAAI;AACF,YAAQ,MAAM,KAAK,IAAI,GAAG;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUO,SAAS,aAAa,SAAiC;AAC5D,QAAM,UAA0B,CAAC;AAEjC,aAAW,QAAQ,QAAQ,MAAM,IAAI,GAAG;AACtC,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,YAAY,GAAI;AAEpB,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,OAAO;AAAA,IAC7B,QAAQ;AACN;AAAA,IACF;AAEA,QAAI,CAAC,QAAQ,MAAM,EAAG;AACtB,YAAQ,KAAK,MAAM;AAAA,EACrB;AAEA,SAAO;AACT;AAEA,SAAS,QAAQ,OAAuC;AACtD,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,YAAY;AAClB,MAAI,OAAO,UAAU,QAAQ,SAAU,QAAO;AAC9C,QAAM,QAAQ,UAAU;AACxB,SACE,OAAO,UAAU,YAAY,UAAU,QAAQ,OAAQ,MAAuB,SAAS;AAE3F;AAkCO,IAAM,iBAAN,MAAiD;AAAA,EACrC;AAAA,EACA;AAAA,EAEjB,YAAY,SAAsC;AAChD,QAAI,QAAQ,WAAW,GAAG;AACxB,YAAM,IAAI,MAAM,0CAA0C;AAAA,IAC5D;AACA,SAAK,UAAU;AACf,SAAK,UAAU,QAAQ,CAAC;AAAA,EAC1B;AAAA,EAEA,QAAQ,OAA2B;AACjC,eAAW,UAAU,KAAK,SAAS;AACjC,aAAO,QAAQ,KAAK;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,WAA4B;AAC1B,WAAO,KAAK,QAAQ,SAAS;AAAA,EAC/B;AAAA,EAEA,UAAkB;AAChB,WAAO,KAAK,QAAQ,QAAQ;AAAA,EAC9B;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,QAAQ,IAAI,KAAK,QAAQ,IAAI,CAAC,WAAW,OAAO,MAAM,CAAC,CAAC;AAAA,EAChE;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,QAAQ,IAAI,KAAK,QAAQ,IAAI,CAAC,WAAW,OAAO,MAAM,CAAC,CAAC;AAAA,EAChE;AACF;","names":[]}
|
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
applyAcceptanceToPlan,
|
|
4
|
+
formatVerificationLine,
|
|
5
|
+
runAcceptanceGate
|
|
6
|
+
} from "./chunk-BQDQTIM7.js";
|
|
2
7
|
import {
|
|
3
8
|
issuePlaceholders,
|
|
4
9
|
resolveCommandIssue
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_HEADLESS_TIMEOUT_MS,
|
|
8
|
-
runHeadless
|
|
9
|
-
} from "./chunk-4JSOYE4G.js";
|
|
10
|
+
} from "./chunk-FZDR7CGH.js";
|
|
10
11
|
import {
|
|
11
12
|
publishPhaseMetrics,
|
|
12
13
|
resolvePolicyPlaceholders
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-664Y3NDX.js";
|
|
14
15
|
import {
|
|
15
16
|
applyPlaceholders,
|
|
16
17
|
loadPrompt
|
|
17
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-MIVHK5YI.js";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_HEADLESS_TIMEOUT_MS,
|
|
21
|
+
runHeadless
|
|
22
|
+
} from "./chunk-ARFVHC2F.js";
|
|
18
23
|
import {
|
|
19
24
|
resolveIssuePaths
|
|
20
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-HQAHNIAR.js";
|
|
21
26
|
import {
|
|
22
27
|
getGlobalTimeout,
|
|
23
28
|
printError,
|
|
24
|
-
printSuccess
|
|
25
|
-
|
|
29
|
+
printSuccess,
|
|
30
|
+
printWarning
|
|
31
|
+
} from "./chunk-IYV6KNVF.js";
|
|
26
32
|
import {
|
|
27
33
|
loadTaskPlan,
|
|
28
34
|
saveTaskPlan
|
|
29
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-DIG5XKQQ.js";
|
|
30
36
|
|
|
31
37
|
// src/commands/review.ts
|
|
32
38
|
function parseReviewResult(output) {
|
|
@@ -59,6 +65,36 @@ async function runReview(issue, resolvedIssue) {
|
|
|
59
65
|
if (!resolution.ok) {
|
|
60
66
|
return resolution.code;
|
|
61
67
|
}
|
|
68
|
+
const acceptance = await runAcceptanceGate({
|
|
69
|
+
issueDir: paths.issueDir,
|
|
70
|
+
addDirs: [paths.issueDir]
|
|
71
|
+
});
|
|
72
|
+
if (acceptance.verdict === "failed") {
|
|
73
|
+
try {
|
|
74
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
75
|
+
const applied = applyAcceptanceToPlan(plan, acceptance);
|
|
76
|
+
await saveTaskPlan(tasksPath, applied.plan);
|
|
77
|
+
} catch {
|
|
78
|
+
printError(formatVerificationLine(acceptance.verdict, acceptance.level));
|
|
79
|
+
}
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
if (acceptance.verdict === "unverified") {
|
|
83
|
+
printWarning(formatVerificationLine(acceptance.verdict, acceptance.level));
|
|
84
|
+
} else {
|
|
85
|
+
printSuccess(formatVerificationLine(acceptance.verdict, acceptance.level));
|
|
86
|
+
}
|
|
87
|
+
if (acceptance.review?.status === "failed") {
|
|
88
|
+
printError("Independent review failed");
|
|
89
|
+
try {
|
|
90
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
91
|
+
plan.pipeline.reviewCompleted = false;
|
|
92
|
+
plan.lastReviewFindings = acceptance.review.findings.map((finding) => finding.claim).join("\n");
|
|
93
|
+
await saveTaskPlan(tasksPath, plan);
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
62
98
|
const template = await loadPrompt("review");
|
|
63
99
|
const prompt = applyPlaceholders(template, {
|
|
64
100
|
// The repository's own conventions. Empty when it declares none, which is
|
|
@@ -78,9 +114,11 @@ async function runReview(issue, resolvedIssue) {
|
|
|
78
114
|
outputFormat: "json",
|
|
79
115
|
allowedTools: ["Bash", "Read", "Glob", "Grep"],
|
|
80
116
|
addDirs: [paths.issueDir],
|
|
81
|
-
statusMessage: `Reviewing issue #${issueNumber} resolution
|
|
117
|
+
statusMessage: `Reviewing issue #${issueNumber} resolution...`,
|
|
118
|
+
phase: "review",
|
|
119
|
+
permission: "read-only"
|
|
82
120
|
});
|
|
83
|
-
publishPhaseMetrics("review", result.cost, startedAtMs);
|
|
121
|
+
publishPhaseMetrics("review", result.cost, startedAtMs, result.agent?.provider);
|
|
84
122
|
if (!result.success) {
|
|
85
123
|
printError(`Review failed: ${result.error}`);
|
|
86
124
|
return 1;
|
|
@@ -114,4 +152,4 @@ async function runReview(issue, resolvedIssue) {
|
|
|
114
152
|
export {
|
|
115
153
|
runReview
|
|
116
154
|
};
|
|
117
|
-
//# sourceMappingURL=chunk-
|
|
155
|
+
//# sourceMappingURL=chunk-QXDNBYQF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/review.ts"],"sourcesContent":["import { DEFAULT_HEADLESS_TIMEOUT_MS, runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printSuccess, printWarning } from '../ui/logger.js';\nimport { applyAcceptanceToPlan, runAcceptanceGate } from '../verify/gate.js';\nimport { formatVerificationLine } from '../verify/present.js';\n\nexport interface ReviewResult {\n status: 'PASS' | 'FAIL';\n findings?: string;\n}\n\n/**\n * Parse the <review-result> block from headless output.\n */\nfunction parseReviewResult(output: string): ReviewResult {\n const match = output.match(/<review-result>([\\s\\S]*?)<\\/review-result>/);\n if (!match) {\n // Try to detect PASS/FAIL from the raw output\n if (/STATUS:\\s*PASS/i.test(output)) {\n return { status: 'PASS' };\n }\n if (/STATUS:\\s*FAIL/i.test(output)) {\n const findingsMatch = output.match(/FINDINGS:([\\s\\S]*?)(?:$|<\\/)/);\n return { status: 'FAIL', findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings' };\n }\n // Default to PASS if no explicit status found and no errors\n return { status: 'PASS' };\n }\n\n const block = match[1];\n if (/STATUS:\\s*PASS/i.test(block)) {\n return { status: 'PASS' };\n }\n\n const findingsMatch = block.match(/FINDINGS:([\\s\\S]*)/);\n return {\n status: 'FAIL',\n findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings',\n };\n}\n\nexport async function runReview(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const tasksPath = paths.tasksFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n const acceptance = await runAcceptanceGate({\n issueDir: paths.issueDir,\n addDirs: [paths.issueDir],\n });\n if (acceptance.verdict === 'failed') {\n try {\n const plan = await loadTaskPlan(tasksPath);\n const applied = applyAcceptanceToPlan(plan, acceptance);\n await saveTaskPlan(tasksPath, applied.plan);\n } catch {\n printError(formatVerificationLine(acceptance.verdict, acceptance.level));\n }\n return 1;\n }\n if (acceptance.verdict === 'unverified') {\n printWarning(formatVerificationLine(acceptance.verdict, acceptance.level));\n } else {\n printSuccess(formatVerificationLine(acceptance.verdict, acceptance.level));\n }\n if (acceptance.review?.status === 'failed') {\n printError('Independent review failed');\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = false;\n plan.lastReviewFindings = acceptance.review.findings\n .map((finding) => finding.claim)\n .join('\\n');\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n return 1;\n }\n\n const template = await loadPrompt('review');\n const prompt = applyPlaceholders(template, {\n // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\n __ISSUE_NUMBER__: issueNumber,\n __TASKS_PATH__: tasksPath,\n ...issuePlaceholders(resolution.resolved),\n });\n\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? DEFAULT_HEADLESS_TIMEOUT_MS,\n // json (not text) so the CLI reports usage: the envelope's `result` field\n // carries the same assistant text parseReviewResult() already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n addDirs: [paths.issueDir],\n statusMessage: `Reviewing issue #${issueNumber} resolution...`,\n phase: 'review',\n permission: 'read-only',\n });\n // Before the success check: the tokens were spent either way. A correction\n // cycle calls runReview() again and the reducer sums both invocations.\n publishPhaseMetrics('review', result.cost, startedAtMs, result.agent?.provider);\n\n if (!result.success) {\n printError(`Review failed: ${result.error}`);\n return 1;\n }\n\n const review = parseReviewResult(result.result);\n\n if (review.status === 'PASS') {\n // Update pipeline state\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = true;\n plan.lastReviewFindings = null;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n printSuccess('Review PASSED — implementation meets acceptance criteria');\n return 0;\n }\n\n printError('Review FAILED — issues found:');\n if (review.findings) {\n console.log(review.findings);\n }\n\n // Persist the findings so a correction cycle can act on them: without this,\n // a re-run of execute sees every userStories[].passes already true and\n // exits immediately without attempting any fix (see engine.ts's\n // \"already complete\" guards).\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = false;\n plan.lastReviewFindings = review.findings ?? 'Unknown findings';\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n return 1;\n}\n"],"mappings":"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","names":["findingsMatch"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
sessionSnapshotSchema
|
|
4
|
+
} from "./chunk-NEZZ6KDR.js";
|
|
5
|
+
|
|
6
|
+
// src/storage/session-file.ts
|
|
7
|
+
import { readFile, stat as statFile } from "fs/promises";
|
|
8
|
+
async function readSessionFile(filePath) {
|
|
9
|
+
let raw;
|
|
10
|
+
let mtimeMs;
|
|
11
|
+
try {
|
|
12
|
+
const [content, stat] = await Promise.all([readFile(filePath, "utf-8"), statFile(filePath)]);
|
|
13
|
+
raw = content;
|
|
14
|
+
mtimeMs = stat.mtimeMs;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const parsed = sessionSnapshotSchema.safeParse(JSON.parse(raw));
|
|
20
|
+
if (!parsed.success || parsed.data.sessionId === null) return null;
|
|
21
|
+
return { snapshot: parsed.data, updatedAtMs: mtimeMs };
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
readSessionFile
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=chunk-SNTTOQAD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/storage/session-file.ts"],"sourcesContent":["import { readFile, stat as statFile } from 'node:fs/promises';\nimport { sessionSnapshotSchema, type ValidatedSessionSnapshot } from '../schemas.js';\n\nexport interface ReadSessionFileResult {\n snapshot: ValidatedSessionSnapshot;\n updatedAtMs: number;\n}\n\n/**\n * Read and validate one `session.json`. Anything short of a fully valid file\n * (missing, mid-write, corrupted, written by an incompatible future version)\n * reads as \"not currently a session\" — a directory scan must never fail\n * because one file is momentarily inconsistent.\n */\nexport async function readSessionFile(filePath: string): Promise<ReadSessionFileResult | null> {\n let raw: string;\n let mtimeMs: number;\n try {\n const [content, stat] = await Promise.all([readFile(filePath, 'utf-8'), statFile(filePath)]);\n raw = content;\n mtimeMs = stat.mtimeMs;\n } catch {\n return null;\n }\n\n try {\n const parsed = sessionSnapshotSchema.safeParse(JSON.parse(raw));\n if (!parsed.success || parsed.data.sessionId === null) return null;\n return { snapshot: parsed.data, updatedAtMs: mtimeMs };\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;AAAA,SAAS,UAAU,QAAQ,gBAAgB;AAc3C,eAAsB,gBAAgB,UAAyD;AAC7F,MAAI;AACJ,MAAI;AACJ,MAAI;AACF,UAAM,CAAC,SAAS,IAAI,IAAI,MAAM,QAAQ,IAAI,CAAC,SAAS,UAAU,OAAO,GAAG,SAAS,QAAQ,CAAC,CAAC;AAC3F,UAAM;AACN,cAAU,KAAK;AAAA,EACjB,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,SAAS,sBAAsB,UAAU,KAAK,MAAM,GAAG,CAAC;AAC9D,QAAI,CAAC,OAAO,WAAW,OAAO,KAAK,cAAc,KAAM,QAAO;AAC9D,WAAO,EAAE,UAAU,OAAO,MAAM,aAAa,QAAQ;AAAA,EACvD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -1,47 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
isTransientFailure,
|
|
3
4
|
readFileWithGrace,
|
|
4
5
|
runPhaseWithRetry
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-IV5SS4JN.js";
|
|
6
7
|
import {
|
|
7
8
|
issuePlaceholders,
|
|
8
9
|
resolveCommandIssue
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import {
|
|
11
|
-
DEFAULT_HEADLESS_TIMEOUT_MS,
|
|
12
|
-
runHeadless
|
|
13
|
-
} from "./chunk-4JSOYE4G.js";
|
|
14
|
-
import {
|
|
15
|
-
isTransientFailure
|
|
16
|
-
} from "./chunk-LWRSMTTZ.js";
|
|
10
|
+
} from "./chunk-FZDR7CGH.js";
|
|
17
11
|
import {
|
|
18
12
|
publishPhaseMetrics,
|
|
19
13
|
resolvePolicyPlaceholders
|
|
20
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-664Y3NDX.js";
|
|
21
15
|
import {
|
|
22
16
|
applyPlaceholders,
|
|
23
17
|
loadPrompt
|
|
24
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-MIVHK5YI.js";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_HEADLESS_TIMEOUT_MS,
|
|
21
|
+
runHeadless
|
|
22
|
+
} from "./chunk-ARFVHC2F.js";
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_BRANCH_CONVENTION,
|
|
25
|
+
branchName,
|
|
26
|
+
loadRepositoryPolicy,
|
|
27
|
+
resolveChangeType
|
|
28
|
+
} from "./chunk-UOCPC2Y2.js";
|
|
25
29
|
import {
|
|
26
|
-
getIssuePaths,
|
|
27
|
-
getProjectRoot,
|
|
28
30
|
recordUserStoryNumbering,
|
|
29
31
|
resolveIssuePaths,
|
|
30
32
|
resolveProjectPaths
|
|
31
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-HQAHNIAR.js";
|
|
32
34
|
import {
|
|
33
35
|
getGlobalTimeout,
|
|
34
36
|
printError,
|
|
35
37
|
printInfo,
|
|
36
38
|
printSuccess,
|
|
37
39
|
printWarning
|
|
38
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-IYV6KNVF.js";
|
|
39
41
|
import {
|
|
40
42
|
isoNow,
|
|
41
43
|
loadTaskPlan,
|
|
42
|
-
saveTaskPlan
|
|
44
|
+
saveTaskPlan
|
|
45
|
+
} from "./chunk-DIG5XKQQ.js";
|
|
46
|
+
import {
|
|
43
47
|
taskPlanSchema
|
|
44
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-NEZZ6KDR.js";
|
|
49
|
+
import {
|
|
50
|
+
getIssuePaths
|
|
51
|
+
} from "./chunk-TX5WIVYP.js";
|
|
52
|
+
import {
|
|
53
|
+
getProjectRoot
|
|
54
|
+
} from "./chunk-KQHB7ZT3.js";
|
|
45
55
|
|
|
46
56
|
// src/commands/plan.ts
|
|
47
57
|
import { mkdir, readFile as readFile2 } from "fs/promises";
|
|
@@ -186,6 +196,30 @@ async function runPlan(issue, resolvedIssue, numbering) {
|
|
|
186
196
|
startUs: numbering?.startUs
|
|
187
197
|
});
|
|
188
198
|
printInfo(numberingMessage);
|
|
199
|
+
let persistedBranch = null;
|
|
200
|
+
try {
|
|
201
|
+
persistedBranch = (await loadTaskPlan(tasksPath)).branchName ?? null;
|
|
202
|
+
} catch {
|
|
203
|
+
persistedBranch = null;
|
|
204
|
+
}
|
|
205
|
+
const policy = await loadRepositoryPolicy();
|
|
206
|
+
const change = resolveChangeType({
|
|
207
|
+
labels: resolution.resolved.issue.labels,
|
|
208
|
+
title: resolution.resolved.issue.title,
|
|
209
|
+
titleConvention: policy.issues.titleConvention,
|
|
210
|
+
typeMap: policy.git.typeMap
|
|
211
|
+
});
|
|
212
|
+
const numericIssue = /^\d+$/.test(issueNumber) ? Number(issueNumber) : null;
|
|
213
|
+
const computedBranch = branchName({
|
|
214
|
+
type: change.type,
|
|
215
|
+
issueNumber: numericIssue,
|
|
216
|
+
title: resolution.resolved.issue.title,
|
|
217
|
+
convention: policy.git.branchConvention ?? DEFAULT_BRANCH_CONVENTION
|
|
218
|
+
});
|
|
219
|
+
const resolvedBranch = persistedBranch !== null && persistedBranch !== "" ? persistedBranch : computedBranch;
|
|
220
|
+
printInfo(
|
|
221
|
+
`Branch: ${resolvedBranch} (type ${change.type} from ${change.source}${persistedBranch !== null && persistedBranch !== "" ? ", persisted" : ""})`
|
|
222
|
+
);
|
|
189
223
|
const template = await loadPrompt("plan");
|
|
190
224
|
const prompt = applyPlaceholders(template, {
|
|
191
225
|
// The repository's own conventions. Empty when it declares none, which is
|
|
@@ -195,6 +229,7 @@ async function runPlan(issue, resolvedIssue, numbering) {
|
|
|
195
229
|
__PRD_CONTENT__: prdContent,
|
|
196
230
|
__TASKS_PATH__: tasksPath,
|
|
197
231
|
__NEXT_US_NUMBER__: nextUserStoryId,
|
|
232
|
+
__BRANCH_NAME__: resolvedBranch,
|
|
198
233
|
...issuePlaceholders(resolution.resolved)
|
|
199
234
|
});
|
|
200
235
|
await mkdir(paths.issueDir, { recursive: true });
|
|
@@ -206,18 +241,24 @@ async function runPlan(issue, resolvedIssue, numbering) {
|
|
|
206
241
|
prompt,
|
|
207
242
|
maxTurns: 25,
|
|
208
243
|
timeout: getGlobalTimeout() ?? DEFAULT_HEADLESS_TIMEOUT_MS,
|
|
244
|
+
timeoutHistory: {
|
|
245
|
+
phase: "plan",
|
|
246
|
+
journalFiles: [paths.rotatedEventsFile, paths.eventsFile]
|
|
247
|
+
},
|
|
209
248
|
// json (not text) so the CLI reports usage: the envelope's `result`
|
|
210
249
|
// field carries the same assistant text this phase already consumed.
|
|
211
250
|
outputFormat: "json",
|
|
212
251
|
allowedTools: ["Bash", "Read", "Glob", "Grep", "Write"],
|
|
213
252
|
addDirs: [paths.issueDir],
|
|
214
|
-
statusMessage: `Converting PRD to task plan for issue #${issueNumber}
|
|
253
|
+
statusMessage: `Converting PRD to task plan for issue #${issueNumber}...`,
|
|
254
|
+
phase: "plan",
|
|
255
|
+
permission: "workspace"
|
|
215
256
|
});
|
|
216
|
-
publishPhaseMetrics("plan", result.cost, startedAtMs);
|
|
257
|
+
publishPhaseMetrics("plan", result.cost, startedAtMs, result.agent?.provider);
|
|
217
258
|
if (!result.success) {
|
|
218
259
|
return {
|
|
219
260
|
ok: false,
|
|
220
|
-
transient: isTransientFailure(1, result.error ?? ""),
|
|
261
|
+
transient: result.retryExhausted !== true && isTransientFailure(1, result.error ?? ""),
|
|
221
262
|
error: `Task plan generation failed: ${result.error}`
|
|
222
263
|
};
|
|
223
264
|
}
|
|
@@ -252,6 +293,7 @@ ${issues}`
|
|
|
252
293
|
}
|
|
253
294
|
const plan = await loadTaskPlan(tasksPath);
|
|
254
295
|
plan.pipeline.jsonCompleted = true;
|
|
296
|
+
plan.branchName = persistedBranch !== null && persistedBranch !== "" ? persistedBranch : computedBranch;
|
|
255
297
|
await saveTaskPlan(tasksPath, plan);
|
|
256
298
|
const generatedLowest = plan.userStories.map((story) => parseUserStoryNumber(story.id)).filter((value) => value !== null).reduce(
|
|
257
299
|
(lowest, value) => lowest === null ? value : Math.min(lowest, value),
|
|
@@ -269,4 +311,4 @@ ${issues}`
|
|
|
269
311
|
export {
|
|
270
312
|
runPlan
|
|
271
313
|
};
|
|
272
|
-
//# sourceMappingURL=chunk-
|
|
314
|
+
//# sourceMappingURL=chunk-TM67EHSV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/plan.ts","../src/storage/user-story-numbering.ts"],"sourcesContent":["import { mkdir, readFile } from 'node:fs/promises';\nimport {\n branchName,\n DEFAULT_BRANCH_CONVENTION,\n resolveChangeType,\n} from '../conventions/git/index.js';\nimport { DEFAULT_HEADLESS_TIMEOUT_MS, runHeadless } from '../core/headless.js';\nimport { readFileWithGrace, runPhaseWithRetry } from '../core/phase-runner.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { loadRepositoryPolicy } from '../policy/index.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.js';\nimport { taskPlanSchema } from '../schemas.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport {\n determineUserStoryNumbering,\n formatUserStoryId,\n parseUserStoryNumber,\n} from '../storage/user-story-numbering.js';\nimport { printError, printInfo, printSuccess, printWarning } from '../ui/logger.js';\nimport { isTransientFailure } from '../utils/retry.js';\n\n/** `--continue` / `--start-us <n>` override of the numbering cascade (issue #36). */\nexport interface PlanUserStoryNumberingOptions {\n continueFlag?: boolean;\n startUs?: number;\n}\n\nexport async function runPlan(\n issue: string,\n resolvedIssue?: ResolvedIssue,\n numbering?: PlanUserStoryNumberingOptions,\n): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n // Read the PRD\n const prdPath = paths.prdFile;\n let prdContent: string;\n try {\n prdContent = await readFile(prdPath, 'utf-8');\n } catch {\n printError(`PRD not found at ${prdPath}. Run 'issue-flow prd ${issueNumber}' first.`);\n return 1;\n }\n\n const tasksPath = paths.tasksFile;\n\n // Resolve the User Story numbering continuity (issue #36), log where the\n // decision came from — never silently — and persist it into the project's\n // metadata.json for audit before the prompt is even built, so the record on\n // disk always matches what the prompt was told.\n const {\n message: numberingMessage,\n nextUserStoryId,\n decision: { nextNumber },\n } = await determineUserStoryNumbering({\n issueNumber,\n continueFlag: numbering?.continueFlag,\n startUs: numbering?.startUs,\n });\n printInfo(numberingMessage);\n\n let persistedBranch: string | null = null;\n try {\n persistedBranch = (await loadTaskPlan(tasksPath)).branchName ?? null;\n } catch {\n persistedBranch = null;\n }\n\n const policy = await loadRepositoryPolicy();\n const change = resolveChangeType({\n labels: resolution.resolved.issue.labels,\n title: resolution.resolved.issue.title,\n titleConvention: policy.issues.titleConvention,\n typeMap: policy.git.typeMap,\n });\n const numericIssue = /^\\d+$/.test(issueNumber) ? Number(issueNumber) : null;\n const computedBranch = branchName({\n type: change.type,\n issueNumber: numericIssue,\n title: resolution.resolved.issue.title,\n convention: policy.git.branchConvention ?? DEFAULT_BRANCH_CONVENTION,\n });\n const resolvedBranch =\n persistedBranch !== null && persistedBranch !== '' ? persistedBranch : computedBranch;\n printInfo(\n `Branch: ${resolvedBranch} (type ${change.type} from ${change.source}${\n persistedBranch !== null && persistedBranch !== '' ? ', persisted' : ''\n })`,\n );\n\n const template = await loadPrompt('plan');\n const prompt = applyPlaceholders(template, {\n // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\n __ISSUE_NUMBER__: issueNumber,\n __PRD_CONTENT__: prdContent,\n __TASKS_PATH__: tasksPath,\n __NEXT_US_NUMBER__: nextUserStoryId,\n __BRANCH_NAME__: resolvedBranch,\n ...issuePlaceholders(resolution.resolved),\n });\n\n await mkdir(paths.issueDir, { recursive: true });\n\n const outcome = await runPhaseWithRetry({\n phase: 'plan',\n attempt: async () => {\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? DEFAULT_HEADLESS_TIMEOUT_MS,\n timeoutHistory: {\n phase: 'plan',\n journalFiles: [paths.rotatedEventsFile, paths.eventsFile],\n },\n // json (not text) so the CLI reports usage: the envelope's `result`\n // field carries the same assistant text this phase already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep', 'Write'],\n addDirs: [paths.issueDir],\n statusMessage: `Converting PRD to task plan for issue #${issueNumber}...`,\n phase: 'plan',\n permission: 'workspace',\n });\n // One event per attempt; the reducer sums them into the phase total.\n publishPhaseMetrics('plan', result.cost, startedAtMs, result.agent?.provider);\n\n if (!result.success) {\n return {\n ok: false,\n transient: result.retryExhausted !== true && isTransientFailure(1, result.error ?? ''),\n error: `Task plan generation failed: ${result.error}`,\n };\n }\n\n // Verify the file was created, tolerating a brief FS-visibility lag.\n let rawContent: string;\n try {\n rawContent = await readFileWithGrace(tasksPath);\n } catch {\n return { ok: false, transient: true, error: `tasks.json was not created at ${tasksPath}` };\n }\n\n // Validate JSON structure — a content defect, not a timing issue, but\n // still worth a bounded retry since it's a fresh Claude invocation.\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawContent);\n } catch {\n return { ok: false, transient: true, error: 'tasks.json contains invalid JSON' };\n }\n\n // Validate with zod schema\n const validation = taskPlanSchema.safeParse(parsed);\n if (!validation.success) {\n const issues = validation.error.issues\n .map((i) => ` - ${i.path.join('.')}: ${i.message}`)\n .join('\\n');\n return {\n ok: false,\n transient: true,\n error: `tasks.json does not match expected schema:\\n${issues}`,\n };\n }\n\n return { ok: true };\n },\n });\n\n if (!outcome.ok) {\n printError(outcome.error ?? `Task plan generation failed for issue #${issueNumber}`);\n return 1;\n }\n\n // Ensure pipeline state reflects completion of this phase.\n // The branch is a CLI calculation: a persisted name is never recalculated,\n // and a fresh plan is overwritten if the agent drifted from __BRANCH_NAME__.\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.jsonCompleted = true;\n plan.branchName =\n persistedBranch !== null && persistedBranch !== '' ? persistedBranch : computedBranch;\n await saveTaskPlan(tasksPath, plan);\n\n // The numbering is a prompt instruction, not a programmatic rewrite, so the\n // generated plan can still ignore it. Say so out loud instead of leaving the\n // log and metadata.json claiming a numbering the file on disk contradicts.\n const generatedLowest = plan.userStories\n .map((story) => parseUserStoryNumber(story.id))\n .filter((value): value is number => value !== null)\n .reduce<number | null>(\n (lowest, value) => (lowest === null ? value : Math.min(lowest, value)),\n null,\n );\n if (generatedLowest !== null && generatedLowest < nextNumber) {\n printWarning(\n `The generated plan starts at ${formatUserStoryId(generatedLowest)}, not the requested ` +\n `${nextUserStoryId}. User Story ids may collide with earlier issues of this project — ` +\n `re-run 'issue-flow plan ${issueNumber} --start-us ${nextNumber}' if that matters.`,\n );\n }\n\n printSuccess(`Task plan saved to ${tasksPath} (${plan.userStories.length} stories)`);\n return 0;\n}\n","import { readdir, readFile } from 'node:fs/promises';\nimport { isoNow } from '../core/state-manager.js';\nimport { getProjectRoot } from '../utils/git.js';\nimport { recordUserStoryNumbering } from './compat.js';\nimport { getIssuePaths } from './paths.js';\nimport { type ResolveIssuePathsOptions, resolveProjectPaths } from './resolve.js';\nimport type { UserStoryNumberingDecision } from './schemas.js';\n\n/**\n * Continuity of `US-NNN` numbering across `plan` executions of the same\n * project (issue #36).\n *\n * The User Story `id` field is free-form — `userStorySchema` in\n * `src/schemas.ts` only requires a non-empty string, no `US-NNN` regex is\n * enforced — so every reader here tolerates an id that does not carry a\n * trailing number instead of throwing on it.\n */\n\nconst USER_STORY_ID_PAD = 3;\n\n/** `16` -> `'US-016'`. Numbers past 999 are not padded further. */\nexport function formatUserStoryId(n: number): string {\n return `US-${String(n).padStart(USER_STORY_ID_PAD, '0')}`;\n}\n\n/**\n * Extract the last run of digits from a User Story id, or `null` when none is\n * found. Tolerant by design: an id like `US-001` yields `1`, but so does a\n * free-form one such as `story-1` or `v2-US-007` (the last digit run, `007`,\n * wins over the `2` version prefix) — only an id with no digits at all (e.g.\n * `add-auth`) is ignored.\n */\nexport function parseUserStoryNumber(id: string): number | null {\n const matches = id.match(/\\d+/g);\n if (!matches || matches.length === 0) return null;\n\n const value = Number.parseInt(matches[matches.length - 1], 10);\n return Number.isNaN(value) ? null : value;\n}\n\nexport interface HighestUserStoryNumberResult {\n number: number;\n /** Issue directory name the winning story came from. */\n issueId: string;\n /** Verbatim id of the winning story. */\n storyId: string;\n}\n\n/**\n * Loose read of one `tasks.json`: only cares about `userStories[].id` and\n * tolerates every failure (missing file, invalid JSON, unexpected shape) by\n * returning `null` — a sibling issue mid-migration, or with a corrupt plan,\n * must not abort the whole project scan.\n */\nasync function readUserStoryIds(tasksPath: string): Promise<string[] | null> {\n let raw: string;\n try {\n raw = await readFile(tasksPath, 'utf-8');\n } catch {\n return null;\n }\n\n try {\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n !Array.isArray((parsed as { userStories?: unknown }).userStories)\n ) {\n return null;\n }\n\n return (parsed as { userStories: unknown[] }).userStories\n .map((story) =>\n typeof story === 'object' &&\n story !== null &&\n typeof (story as { id?: unknown }).id === 'string'\n ? (story as { id: string }).id\n : null,\n )\n .filter((id): id is string => id !== null);\n } catch {\n return null;\n }\n}\n\nexport interface FindHighestUserStoryNumberOptions extends ResolveIssuePathsOptions {\n /**\n * Issue directory to leave out of the scan — the issue the numbering is\n * being resolved for. Re-planning an issue overwrites its own `tasks.json`,\n * so counting the plan about to be replaced would push the numbering forward\n * on every `plan` re-run of the same issue.\n */\n excludeIssueId?: string;\n}\n\n/**\n * Highest User Story number already used anywhere in the project, scanning\n * every `<issuesDir>/<id>/tasks.json` regardless of which issue produced it —\n * that project-wide scope is what makes `US-NNN` unambiguous across issues.\n * The issue named by `excludeIssueId` is skipped, so re-planning is idempotent.\n *\n * `null` means no readable `tasks.json` was found (the project's first `plan`\n * run ever, or a brand-new project directory). A storage directory that exists\n * but cannot be read throws instead: silently restarting at `US-001` because\n * `~/.issue-flow` was momentarily unreadable is the worst outcome this feature\n * can produce.\n */\nexport async function findHighestUserStoryNumber(\n options: FindHighestUserStoryNumberOptions = {},\n): Promise<HighestUserStoryNumberResult | null> {\n const { excludeIssueId, ...rootOptions } = options;\n const { projectId, issuesDir } = await resolveProjectPaths(rootOptions);\n\n let entries: string[];\n try {\n entries = (await readdir(issuesDir, { withFileTypes: true }))\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch (error) {\n // ENOENT is the legitimate \"no history yet\" case. Any other failure\n // (EACCES, EIO, an unavailable mount) must not be reported as an empty\n // history, or the numbering would restart silently.\n if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return null;\n throw new Error(\n `Could not scan the project's User Story history at ${issuesDir}: ` +\n `${(error as Error).message}. Fix the storage directory, or pass --start-us <n> ` +\n 'to set the numbering explicitly.',\n { cause: error },\n );\n }\n\n let best: HighestUserStoryNumberResult | null = null;\n\n for (const issueId of entries) {\n if (excludeIssueId !== undefined && issueId === excludeIssueId) continue;\n // Never hand-build an artifact path (see src/storage/AGENTS.md): asking\n // getIssuePaths() keeps a rename of `tasks.json` a one-file change instead\n // of silently making this scan find nothing.\n const { tasksFile } = getIssuePaths(projectId, issueId, { env: options.env });\n const ids = await readUserStoryIds(tasksFile);\n if (!ids) continue;\n\n for (const id of ids) {\n const value = parseUserStoryNumber(id);\n if (value === null) continue;\n if (best === null || value > best.number) {\n best = { number: value, issueId, storyId: id };\n }\n }\n }\n\n return best;\n}\n\nexport interface ResolveUserStoryNumberingOptions extends ResolveIssuePathsOptions {\n /** Issue the decision is being made for — carried into the audit record. */\n issueNumber: string;\n /** `--continue` was passed explicitly. Purely cosmetic on the log message:\n * the resolved number is identical to the automatic default. */\n continueFlag?: boolean;\n /** `--start-us <n>` was passed. Wins outright over any history. */\n startUs?: number;\n /** Timestamp source, injectable for tests. */\n now?: () => string;\n}\n\nexport interface UserStoryNumberingOutcome {\n decision: UserStoryNumberingDecision;\n /** Ready-to-print explanation of where the decision came from — the CLI\n * must never resolve this silently (issue #36). */\n message: string;\n}\n\n/**\n * Resolve the numbering cascade of #36:\n *\n * 1. `--start-us <n>` wins outright — history is not even scanned, since the\n * whole point is to ignore it.\n * 2. Otherwise the project's history is scanned for the highest number used\n * so far. This is what running with no flag at all does automatically,\n * and what `--continue` names explicitly (the two produce the same\n * number; the flag only changes the log message).\n * 3. No history found (the project's first `plan` ever) starts at `US-001`.\n *\n * Pure with respect to `metadata.json` — this only reads `tasks.json` files,\n * never writes. Persisting the decision is {@link determineUserStoryNumbering}\n * below, via `recordUserStoryNumbering()` in `compat.ts`.\n */\nexport async function resolveUserStoryNumbering(\n options: ResolveUserStoryNumberingOptions,\n): Promise<UserStoryNumberingOutcome> {\n const { issueNumber, continueFlag, startUs, now, ...rootOptions } = options;\n const decidedAt = (now ?? isoNow)();\n\n if (startUs !== undefined) {\n const decision: UserStoryNumberingDecision = {\n nextNumber: startUs,\n source: 'start-us',\n issueNumber,\n decidedAt,\n };\n return {\n decision,\n message: `User Story numbering forced to ${formatUserStoryId(startUs)} via --start-us.`,\n };\n }\n\n // The issue being planned is excluded from the scan: `plan` overwrites its\n // own `tasks.json`, so counting the plan it is about to replace would push\n // the numbering forward on every re-run of `plan` for the same issue.\n const highest = await findHighestUserStoryNumber({ ...rootOptions, excludeIssueId: issueNumber });\n\n if (highest === null) {\n const decision: UserStoryNumberingDecision = {\n nextNumber: 1,\n source: 'none',\n issueNumber,\n decidedAt,\n };\n return {\n decision,\n message: `Starting User Story numbering at ${formatUserStoryId(1)} (no previous history found for this project).`,\n };\n }\n\n const nextNumber = highest.number + 1;\n const decision: UserStoryNumberingDecision = {\n nextNumber,\n source: 'history',\n issueNumber,\n decidedAt,\n detail: `${highest.storyId} (issue #${highest.issueId})`,\n };\n const via = continueFlag ? ' via --continue' : '';\n return {\n decision,\n message:\n `Continuing User Story numbering from ${formatUserStoryId(nextNumber)}${via} ` +\n `— last used was ${highest.storyId} (issue #${highest.issueId}).`,\n };\n}\n\nexport interface DetermineUserStoryNumberingOptions extends ResolveUserStoryNumberingOptions {\n /** Repository root. Defaults to the git toplevel of the current directory. */\n projectRoot?: string;\n}\n\nexport interface UserStoryNumberingResult extends UserStoryNumberingOutcome {\n /** `decision.nextNumber` formatted as `US-NNN`, ready for the prompt. */\n nextUserStoryId: string;\n}\n\n/**\n * The entry point commands use: resolve the cascade and persist the decision\n * into `metadata.json` in one call, so no call site can log/prompt with one\n * decision and audit a different one.\n */\nexport async function determineUserStoryNumbering(\n options: DetermineUserStoryNumberingOptions,\n): Promise<UserStoryNumberingResult> {\n const { projectRoot: projectRootOption, ...rest } = options;\n const projectRoot = projectRootOption ?? (await getProjectRoot());\n\n const outcome = await resolveUserStoryNumbering({ ...rest, projectRoot });\n await recordUserStoryNumbering(projectRoot, outcome.decision, {\n env: options.env,\n now: options.now,\n });\n\n return { ...outcome, nextUserStoryId: formatUserStoryId(outcome.decision.nextNumber) };\n}\n"],"mappings":"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","names":["readFile","decision","readFile"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getGlobalRoot
|
|
4
|
+
} from "./chunk-TX5WIVYP.js";
|
|
5
|
+
import {
|
|
6
|
+
getProjectRoot
|
|
7
|
+
} from "./chunk-KQHB7ZT3.js";
|
|
8
|
+
import {
|
|
9
|
+
writeFileAtomic
|
|
10
|
+
} from "./chunk-7Z4Y3VKM.js";
|
|
11
|
+
|
|
12
|
+
// src/agents/permissions.ts
|
|
13
|
+
import { mkdir, readFile } from "fs/promises";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { dirname, join } from "path";
|
|
16
|
+
var MANAGED_MARKER = "issue-flow:managed";
|
|
17
|
+
function managedEntries(root) {
|
|
18
|
+
const glob = `${root.replace(/\\/g, "/")}/**`;
|
|
19
|
+
return [`Read(${glob})`, `Write(${glob})`];
|
|
20
|
+
}
|
|
21
|
+
function cursorPermissionsPath(mode, projectRoot) {
|
|
22
|
+
if (mode === "none") return null;
|
|
23
|
+
if (mode === "project") {
|
|
24
|
+
return join(projectRoot ?? process.cwd(), ".cursor", "cli.json");
|
|
25
|
+
}
|
|
26
|
+
return join(homedir(), ".cursor", "cli-config.json");
|
|
27
|
+
}
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
async function ensureCursorStorageGrant(options) {
|
|
32
|
+
const mode = options.mode ?? "global";
|
|
33
|
+
if (mode === "none") return { skipped: true, reason: "none" };
|
|
34
|
+
const path = options.filePath ?? cursorPermissionsPath(
|
|
35
|
+
mode,
|
|
36
|
+
options.projectRoot ?? await getProjectRoot().catch(() => process.cwd())
|
|
37
|
+
);
|
|
38
|
+
if (path === null) return { skipped: true, reason: "none" };
|
|
39
|
+
const root = options.globalRoot ?? getGlobalRoot();
|
|
40
|
+
const wanted = managedEntries(root);
|
|
41
|
+
let current = {};
|
|
42
|
+
try {
|
|
43
|
+
const raw = JSON.parse(await readFile(path, "utf-8"));
|
|
44
|
+
if (isRecord(raw)) current = raw;
|
|
45
|
+
} catch {
|
|
46
|
+
current = {};
|
|
47
|
+
}
|
|
48
|
+
const permissions = isRecord(current.permissions) ? { ...current.permissions } : {};
|
|
49
|
+
const allow = Array.isArray(permissions.allow) ? permissions.allow.filter((entry) => typeof entry === "string") : [];
|
|
50
|
+
const missing = wanted.filter((entry) => !allow.includes(entry));
|
|
51
|
+
if (missing.length === 0) {
|
|
52
|
+
return { path, wrote: false };
|
|
53
|
+
}
|
|
54
|
+
permissions.allow = [...allow, ...missing];
|
|
55
|
+
const next = {
|
|
56
|
+
...current,
|
|
57
|
+
permissions,
|
|
58
|
+
[MANAGED_MARKER]: true
|
|
59
|
+
};
|
|
60
|
+
await mkdir(dirname(path), { recursive: true });
|
|
61
|
+
await writeFileAtomic(path, `${JSON.stringify(next, null, 2)}
|
|
62
|
+
`);
|
|
63
|
+
return { path, wrote: true };
|
|
64
|
+
}
|
|
65
|
+
function cursorStorageGranted(allow, globalRoot) {
|
|
66
|
+
if (!Array.isArray(allow)) return false;
|
|
67
|
+
const wanted = managedEntries(globalRoot ?? getGlobalRoot());
|
|
68
|
+
return wanted.every((entry) => allow.includes(entry));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
MANAGED_MARKER,
|
|
73
|
+
cursorPermissionsPath,
|
|
74
|
+
ensureCursorStorageGrant,
|
|
75
|
+
cursorStorageGranted
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=chunk-TVN2JVOF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/agents/permissions.ts"],"sourcesContent":["import { mkdir, readFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\nimport { getGlobalRoot } from '../storage/paths.js';\nimport { writeFileAtomic } from '../utils/fs.js';\nimport { getProjectRoot } from '../utils/git.js';\nimport type { CursorPermissionsFile } from './types.js';\n\nexport const MANAGED_MARKER = 'issue-flow:managed';\n\nfunction managedEntries(root: string): string[] {\n const glob = `${root.replace(/\\\\/g, '/')}/**`;\n return [`Read(${glob})`, `Write(${glob})`];\n}\n\nexport function cursorPermissionsPath(\n mode: CursorPermissionsFile,\n projectRoot?: string,\n): string | null {\n if (mode === 'none') return null;\n if (mode === 'project') {\n return join(projectRoot ?? process.cwd(), '.cursor', 'cli.json');\n }\n return join(homedir(), '.cursor', 'cli-config.json');\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Additive merge of `~/.cursor/cli-config.json` (or the project file).\n * Never removes foreign entries. Idempotent.\n */\nexport async function ensureCursorStorageGrant(options: {\n mode?: CursorPermissionsFile;\n projectRoot?: string;\n globalRoot?: string;\n /** Test seam: write this file instead of the Cursor config path. */\n filePath?: string;\n}): Promise<{ path: string; wrote: boolean } | { skipped: true; reason: 'none' }> {\n const mode = options.mode ?? 'global';\n if (mode === 'none') return { skipped: true, reason: 'none' };\n\n const path =\n options.filePath ??\n cursorPermissionsPath(\n mode,\n options.projectRoot ?? (await getProjectRoot().catch(() => process.cwd())),\n );\n if (path === null) return { skipped: true, reason: 'none' };\n\n const root = options.globalRoot ?? getGlobalRoot();\n const wanted = managedEntries(root);\n\n let current: Record<string, unknown> = {};\n try {\n const raw = JSON.parse(await readFile(path, 'utf-8')) as unknown;\n if (isRecord(raw)) current = raw;\n } catch {\n current = {};\n }\n\n const permissions = isRecord(current.permissions) ? { ...current.permissions } : {};\n const allow = Array.isArray(permissions.allow)\n ? permissions.allow.filter((entry): entry is string => typeof entry === 'string')\n : [];\n const missing = wanted.filter((entry) => !allow.includes(entry));\n if (missing.length === 0) {\n return { path, wrote: false };\n }\n\n permissions.allow = [...allow, ...missing];\n const next = {\n ...current,\n permissions,\n [MANAGED_MARKER]: true,\n };\n await mkdir(dirname(path), { recursive: true });\n await writeFileAtomic(path, `${JSON.stringify(next, null, 2)}\\n`);\n return { path, wrote: true };\n}\n\nexport function cursorStorageGranted(allow: unknown, globalRoot?: string): boolean {\n if (!Array.isArray(allow)) return false;\n const wanted = managedEntries(globalRoot ?? getGlobalRoot());\n return wanted.every((entry) => allow.includes(entry));\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,OAAO,gBAAgB;AAChC,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAMvB,IAAM,iBAAiB;AAE9B,SAAS,eAAe,MAAwB;AAC9C,QAAM,OAAO,GAAG,KAAK,QAAQ,OAAO,GAAG,CAAC;AACxC,SAAO,CAAC,QAAQ,IAAI,KAAK,SAAS,IAAI,GAAG;AAC3C;AAEO,SAAS,sBACd,MACA,aACe;AACf,MAAI,SAAS,OAAQ,QAAO;AAC5B,MAAI,SAAS,WAAW;AACtB,WAAO,KAAK,eAAe,QAAQ,IAAI,GAAG,WAAW,UAAU;AAAA,EACjE;AACA,SAAO,KAAK,QAAQ,GAAG,WAAW,iBAAiB;AACrD;AAEA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAMA,eAAsB,yBAAyB,SAMmC;AAChF,QAAM,OAAO,QAAQ,QAAQ;AAC7B,MAAI,SAAS,OAAQ,QAAO,EAAE,SAAS,MAAM,QAAQ,OAAO;AAE5D,QAAM,OACJ,QAAQ,YACR;AAAA,IACE;AAAA,IACA,QAAQ,eAAgB,MAAM,eAAe,EAAE,MAAM,MAAM,QAAQ,IAAI,CAAC;AAAA,EAC1E;AACF,MAAI,SAAS,KAAM,QAAO,EAAE,SAAS,MAAM,QAAQ,OAAO;AAE1D,QAAM,OAAO,QAAQ,cAAc,cAAc;AACjD,QAAM,SAAS,eAAe,IAAI;AAElC,MAAI,UAAmC,CAAC;AACxC,MAAI;AACF,UAAM,MAAM,KAAK,MAAM,MAAM,SAAS,MAAM,OAAO,CAAC;AACpD,QAAI,SAAS,GAAG,EAAG,WAAU;AAAA,EAC/B,QAAQ;AACN,cAAU,CAAC;AAAA,EACb;AAEA,QAAM,cAAc,SAAS,QAAQ,WAAW,IAAI,EAAE,GAAG,QAAQ,YAAY,IAAI,CAAC;AAClF,QAAM,QAAQ,MAAM,QAAQ,YAAY,KAAK,IACzC,YAAY,MAAM,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ,IAC9E,CAAC;AACL,QAAM,UAAU,OAAO,OAAO,CAAC,UAAU,CAAC,MAAM,SAAS,KAAK,CAAC;AAC/D,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,EAAE,MAAM,OAAO,MAAM;AAAA,EAC9B;AAEA,cAAY,QAAQ,CAAC,GAAG,OAAO,GAAG,OAAO;AACzC,QAAM,OAAO;AAAA,IACX,GAAG;AAAA,IACH;AAAA,IACA,CAAC,cAAc,GAAG;AAAA,EACpB;AACA,QAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9C,QAAM,gBAAgB,MAAM,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAAA,CAAI;AAChE,SAAO,EAAE,MAAM,OAAO,KAAK;AAC7B;AAEO,SAAS,qBAAqB,OAAgB,YAA8B;AACjF,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAClC,QAAM,SAAS,eAAe,cAAc,cAAc,CAAC;AAC3D,SAAO,OAAO,MAAM,CAAC,UAAU,MAAM,SAAS,KAAK,CAAC;AACtD;","names":[]}
|