issue-flow 0.18.0 → 0.20.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 +43 -97
- package/dist/agent-4DHRKZ2Z.js +32 -0
- package/dist/{analyze-Q4ONIWRZ.js → analyze-UEJ2R3H5.js} +42 -41
- package/dist/analyze-UEJ2R3H5.js.map +1 -0
- package/dist/{apply-EEFPY2W4.js → apply-QDYNQMUX.js} +22 -24
- package/dist/apply-QDYNQMUX.js.map +1 -0
- package/dist/artifacts-AMJQJFFV.js +82 -0
- package/dist/artifacts-AMJQJFFV.js.map +1 -0
- package/dist/{bench-WWWXLTFH.js → bench-RNRXVEFK.js} +42 -42
- package/dist/{chunk-RYOUZ6EK.js → chunk-23VHVVKW.js} +3 -3
- package/dist/chunk-2EYLOS5I.js +99 -0
- package/dist/chunk-2EYLOS5I.js.map +1 -0
- package/dist/{chunk-C5TL7T4V.js → chunk-2YM2VH6R.js} +2 -2
- package/dist/{chunk-XUEMATVY.js → chunk-37O2FMAN.js} +28 -25
- package/dist/chunk-37O2FMAN.js.map +1 -0
- package/dist/chunk-44RF3VFG.js +717 -0
- package/dist/chunk-44RF3VFG.js.map +1 -0
- package/dist/chunk-6AOZCWN2.js +21 -0
- package/dist/chunk-6AOZCWN2.js.map +1 -0
- package/dist/chunk-7MJGYPHF.js +1571 -0
- package/dist/chunk-7MJGYPHF.js.map +1 -0
- package/dist/{chunk-SA6HXL4Y.js → chunk-7WBUEBD5.js} +9 -9
- package/dist/chunk-7WBUEBD5.js.map +1 -0
- package/dist/chunk-7ZZUSMBJ.js +35 -0
- package/dist/chunk-7ZZUSMBJ.js.map +1 -0
- package/dist/{chunk-QJ44VG5G.js → chunk-ADH6RJNX.js} +3 -3
- package/dist/{chunk-34OCSKOB.js → chunk-AKL5UPR6.js} +159 -114
- package/dist/chunk-AKL5UPR6.js.map +1 -0
- package/dist/{chunk-7F7LGMVO.js → chunk-BTBJ5UOO.js} +12 -12
- package/dist/{chunk-7F7LGMVO.js.map → chunk-BTBJ5UOO.js.map} +1 -1
- package/dist/{chunk-HOAHMERN.js → chunk-CLZ6RRZL.js} +21 -21
- package/dist/chunk-DBLXLKGU.js +1201 -0
- package/dist/chunk-DBLXLKGU.js.map +1 -0
- package/dist/{chunk-DUS5ZNKL.js → chunk-DIMDAYZS.js} +7 -6
- package/dist/chunk-DZ3DZ3EN.js +278 -0
- package/dist/chunk-DZ3DZ3EN.js.map +1 -0
- package/dist/{chunk-CRGIQFUQ.js → chunk-E2H3JV2E.js} +3 -1
- package/dist/chunk-E2H3JV2E.js.map +1 -0
- package/dist/{chunk-45WZ6GMI.js → chunk-EORDSKWZ.js} +17 -17
- package/dist/chunk-EORDSKWZ.js.map +1 -0
- package/dist/{chunk-LTH6XYZ3.js → chunk-EYKPWHT7.js} +40 -30
- package/dist/chunk-EYKPWHT7.js.map +1 -0
- package/dist/{chunk-OODXFCQV.js → chunk-FU2FQ4JE.js} +14 -16
- package/dist/{chunk-OODXFCQV.js.map → chunk-FU2FQ4JE.js.map} +1 -1
- package/dist/{chunk-V7QOIQYC.js → chunk-G442RDFX.js} +9 -23
- package/dist/{chunk-V7QOIQYC.js.map → chunk-G442RDFX.js.map} +1 -1
- package/dist/{chunk-QPGEFLMY.js → chunk-I64N6RL6.js} +7 -7
- package/dist/{chunk-KRAGAUKD.js → chunk-JBYXJOJJ.js} +201 -64
- package/dist/chunk-JBYXJOJJ.js.map +1 -0
- package/dist/{chunk-SOM4QA6C.js → chunk-JCR5C5A7.js} +93 -18
- package/dist/chunk-JCR5C5A7.js.map +1 -0
- package/dist/{chunk-RNFUVKC3.js → chunk-K5NM26NV.js} +30 -13
- package/dist/chunk-K5NM26NV.js.map +1 -0
- package/dist/{chunk-YVPZUN6X.js → chunk-KUA6KBBD.js} +335 -38
- package/dist/chunk-KUA6KBBD.js.map +1 -0
- package/dist/{chunk-KMCS76ZW.js → chunk-KUL64XUF.js} +44 -12
- package/dist/chunk-KUL64XUF.js.map +1 -0
- package/dist/{chunk-HAKDLR6N.js → chunk-MAJ6QDRL.js} +65 -78
- package/dist/chunk-MAJ6QDRL.js.map +1 -0
- package/dist/{chunk-DGRTJD5R.js → chunk-MML2G7IC.js} +7 -7
- package/dist/chunk-NP47HRX2.js +136 -0
- package/dist/chunk-NP47HRX2.js.map +1 -0
- package/dist/{chunk-3R27MJ5R.js → chunk-PFWHHYEO.js} +3 -3
- package/dist/{chunk-ECU7YO7S.js → chunk-PM5YWJYO.js} +22 -6
- package/dist/chunk-PM5YWJYO.js.map +1 -0
- package/dist/{chunk-E4I6MMHD.js → chunk-PXPTIXJF.js} +154 -101
- package/dist/chunk-PXPTIXJF.js.map +1 -0
- package/dist/{chunk-CWSQVHVV.js → chunk-QJHZ3HYJ.js} +248 -102
- package/dist/chunk-QJHZ3HYJ.js.map +1 -0
- package/dist/{chunk-LI35PIIS.js → chunk-RDPK7LYK.js} +20 -117
- package/dist/chunk-RDPK7LYK.js.map +1 -0
- package/dist/{chunk-PMLE7KAQ.js → chunk-RMIBLFG3.js} +30 -14
- package/dist/chunk-RMIBLFG3.js.map +1 -0
- package/dist/{chunk-62GVVZQO.js → chunk-T736DSZI.js} +32 -316
- package/dist/chunk-T736DSZI.js.map +1 -0
- package/dist/{chunk-TZYINKCC.js → chunk-UDUUUFQF.js} +57 -29
- package/dist/chunk-UDUUUFQF.js.map +1 -0
- package/dist/{chunk-NEWNXZ65.js → chunk-V7GETODQ.js} +36 -28
- package/dist/{chunk-NEWNXZ65.js.map → chunk-V7GETODQ.js.map} +1 -1
- package/dist/{chunk-PPZ3K4YM.js → chunk-VHQMMY2D.js} +2 -2
- package/dist/{chunk-ZVZMWNCG.js → chunk-VJ3PF6B7.js} +50 -16
- package/dist/chunk-VJ3PF6B7.js.map +1 -0
- package/dist/{chunk-L6HSKTQE.js → chunk-W7LZLJAF.js} +4 -4
- package/dist/{chunk-6K2LCFG6.js → chunk-WB4FA2EH.js} +70 -81
- package/dist/chunk-WB4FA2EH.js.map +1 -0
- package/dist/{chunk-LGHND47K.js → chunk-WQSGL5LX.js} +22 -16
- package/dist/chunk-WQSGL5LX.js.map +1 -0
- package/dist/{chunk-ICM463EO.js → chunk-XNK5UN2D.js} +5 -649
- package/dist/chunk-XNK5UN2D.js.map +1 -0
- package/dist/{chunk-7UMMFOL2.js → chunk-Y2KOWABB.js} +39 -24
- package/dist/chunk-Y2KOWABB.js.map +1 -0
- package/dist/{chunk-QF7VEL3M.js → chunk-YC24E66P.js} +568 -158
- package/dist/chunk-YC24E66P.js.map +1 -0
- package/dist/{chunk-EG2MGDP5.js → chunk-Z6RCKCVB.js} +52 -46
- package/dist/chunk-Z6RCKCVB.js.map +1 -0
- package/dist/{chunk-YUNBOUQH.js → chunk-ZPGNVBWY.js} +8 -8
- package/dist/cli.js +132 -62
- package/dist/cli.js.map +1 -1
- package/dist/compat-OJG76J4I.js +24 -0
- package/dist/{config-LEBZZCJP.js → config-6YTWPDBK.js} +20 -17
- package/dist/{contract-JH2WBIO6.js → contract-HWLFFL3F.js} +2 -2
- package/dist/{conventions-XOT6YW5L.js → conventions-5UPOTU42.js} +20 -16
- package/dist/{conventions-XOT6YW5L.js.map → conventions-5UPOTU42.js.map} +1 -1
- package/dist/db-QAZG4LQD.js +264 -0
- package/dist/db-QAZG4LQD.js.map +1 -0
- package/dist/diagnostics-YEFB42E5.js +25 -0
- package/dist/execute-QCT64D37.js +38 -0
- package/dist/{generate-JGW5JSKB.js → generate-QATMDKOK.js} +46 -43
- package/dist/generate-QATMDKOK.js.map +1 -0
- package/dist/{git-A2IP32PQ.js → git-XGOECXWG.js} +2 -3
- package/dist/history-NBPUZVGQ.js +101 -0
- package/dist/history-NBPUZVGQ.js.map +1 -0
- package/dist/init-4LB2YKWP.js +28 -0
- package/dist/{operations-O4FORFIH.js → operations-RUQP2Y2F.js} +78 -52
- package/dist/operations-RUQP2Y2F.js.map +1 -0
- package/dist/{permissions-G4XCAESF.js → permissions-QZPUUBSO.js} +5 -6
- package/dist/plan-2NMSHTMD.js +38 -0
- package/dist/{policy-U5NBFAO5.js → policy-RKKFF5A2.js} +17 -15
- package/dist/{policy-U5NBFAO5.js.map → policy-RKKFF5A2.js.map} +1 -1
- package/dist/pr-QKFO5HI3.js +42 -0
- package/dist/pr-review-H2BFD6F2.js +33 -0
- package/dist/prd-ET6XKYRS.js +37 -0
- package/dist/{ps-76L6ZAZL.js → ps-UIKSOUAQ.js} +26 -15
- package/dist/ps-UIKSOUAQ.js.map +1 -0
- package/dist/{recorder-2BAKPBRA.js → recorder-LOU3PTIE.js} +11 -7
- package/dist/registry-HV7M6MAH.js +17 -0
- package/dist/repository-RCRR6RDE.js +80 -0
- package/dist/resume-BWJZGBLZ.js +278 -0
- package/dist/resume-BWJZGBLZ.js.map +1 -0
- package/dist/review-2MTZLNSS.js +38 -0
- package/dist/routing-RM5ABHHD.js +35 -0
- package/dist/run-XMACQBRJ.js +61 -0
- package/dist/{session-publisher-KAJQRHNZ.js → session-publisher-Q5TNEZPX.js} +3 -3
- package/dist/session-publisher-Q5TNEZPX.js.map +1 -0
- package/dist/{state-manager-UPHO2EA2.js → state-manager-SRN4VJSX.js} +9 -4
- package/dist/state-manager-SRN4VJSX.js.map +1 -0
- package/dist/{usage-KTMYXAPF.js → usage-JOODVE5O.js} +69 -63
- package/dist/usage-JOODVE5O.js.map +1 -0
- package/dist/web-TLMWJ2BM.js +205 -0
- package/dist/web-TLMWJ2BM.js.map +1 -0
- package/package.json +22 -8
- package/prompts/analyze.md +16 -19
- package/prompts/execute.md +73 -144
- package/prompts/generate.md +23 -10
- package/prompts/plan.md +32 -60
- package/prompts/pr-review.md +58 -40
- package/prompts/pr.md +50 -16
- package/prompts/prd.md +26 -19
- package/prompts/review.md +41 -18
- package/web/public/app.js +111 -112
- package/dist/agent-HMZTXCRT.js +0 -30
- package/dist/analyze-Q4ONIWRZ.js.map +0 -1
- package/dist/apply-EEFPY2W4.js.map +0 -1
- package/dist/chunk-34OCSKOB.js.map +0 -1
- package/dist/chunk-45WZ6GMI.js.map +0 -1
- package/dist/chunk-62GVVZQO.js.map +0 -1
- package/dist/chunk-6K2LCFG6.js.map +0 -1
- package/dist/chunk-7UMMFOL2.js.map +0 -1
- package/dist/chunk-CRGIQFUQ.js.map +0 -1
- package/dist/chunk-CWSQVHVV.js.map +0 -1
- package/dist/chunk-E4I6MMHD.js.map +0 -1
- package/dist/chunk-ECU7YO7S.js.map +0 -1
- package/dist/chunk-EG2MGDP5.js.map +0 -1
- package/dist/chunk-HAKDLR6N.js.map +0 -1
- package/dist/chunk-ICM463EO.js.map +0 -1
- package/dist/chunk-KMCS76ZW.js.map +0 -1
- package/dist/chunk-KRAGAUKD.js.map +0 -1
- package/dist/chunk-LGHND47K.js.map +0 -1
- package/dist/chunk-LI35PIIS.js.map +0 -1
- package/dist/chunk-LTH6XYZ3.js.map +0 -1
- package/dist/chunk-PMLE7KAQ.js.map +0 -1
- package/dist/chunk-QF7VEL3M.js.map +0 -1
- package/dist/chunk-RNFUVKC3.js.map +0 -1
- package/dist/chunk-SA6HXL4Y.js.map +0 -1
- package/dist/chunk-SOM4QA6C.js.map +0 -1
- package/dist/chunk-TZYINKCC.js.map +0 -1
- package/dist/chunk-WCCZJL7W.js +0 -141
- package/dist/chunk-WCCZJL7W.js.map +0 -1
- package/dist/chunk-XEYH6ZZI.js +0 -289
- package/dist/chunk-XEYH6ZZI.js.map +0 -1
- package/dist/chunk-XUEMATVY.js.map +0 -1
- package/dist/chunk-XZUEFTUP.js +0 -243
- package/dist/chunk-XZUEFTUP.js.map +0 -1
- package/dist/chunk-YVPZUN6X.js.map +0 -1
- package/dist/chunk-ZVZMWNCG.js.map +0 -1
- package/dist/diagnostics-HTCWF7F4.js +0 -23
- package/dist/execute-LELOSR5T.js +0 -37
- package/dist/generate-JGW5JSKB.js.map +0 -1
- package/dist/init-TO6MUXO4.js +0 -26
- package/dist/operations-O4FORFIH.js.map +0 -1
- package/dist/plan-OXAT344Q.js +0 -34
- package/dist/pr-UZHNKKZC.js +0 -39
- package/dist/pr-review-Q4ACAWDM.js +0 -32
- package/dist/prd-6CYEMLDY.js +0 -33
- package/dist/ps-76L6ZAZL.js.map +0 -1
- package/dist/registry-XLHHYLTR.js +0 -15
- package/dist/resume-URCU6CIJ.js +0 -244
- package/dist/resume-URCU6CIJ.js.map +0 -1
- package/dist/review-LVZT2TMW.js +0 -35
- package/dist/routing-EK6RBZM4.js +0 -33
- package/dist/run-2RXJCJKT.js +0 -56
- package/dist/usage-KTMYXAPF.js.map +0 -1
- package/dist/web-B5UPHHR5.js +0 -172
- package/dist/web-B5UPHHR5.js.map +0 -1
- /package/dist/{agent-HMZTXCRT.js.map → agent-4DHRKZ2Z.js.map} +0 -0
- /package/dist/{bench-WWWXLTFH.js.map → bench-RNRXVEFK.js.map} +0 -0
- /package/dist/{chunk-RYOUZ6EK.js.map → chunk-23VHVVKW.js.map} +0 -0
- /package/dist/{chunk-C5TL7T4V.js.map → chunk-2YM2VH6R.js.map} +0 -0
- /package/dist/{chunk-QJ44VG5G.js.map → chunk-ADH6RJNX.js.map} +0 -0
- /package/dist/{chunk-HOAHMERN.js.map → chunk-CLZ6RRZL.js.map} +0 -0
- /package/dist/{chunk-DUS5ZNKL.js.map → chunk-DIMDAYZS.js.map} +0 -0
- /package/dist/{chunk-QPGEFLMY.js.map → chunk-I64N6RL6.js.map} +0 -0
- /package/dist/{chunk-DGRTJD5R.js.map → chunk-MML2G7IC.js.map} +0 -0
- /package/dist/{chunk-3R27MJ5R.js.map → chunk-PFWHHYEO.js.map} +0 -0
- /package/dist/{chunk-PPZ3K4YM.js.map → chunk-VHQMMY2D.js.map} +0 -0
- /package/dist/{chunk-L6HSKTQE.js.map → chunk-W7LZLJAF.js.map} +0 -0
- /package/dist/{chunk-YUNBOUQH.js.map → chunk-ZPGNVBWY.js.map} +0 -0
- /package/dist/{config-LEBZZCJP.js.map → compat-OJG76J4I.js.map} +0 -0
- /package/dist/{contract-JH2WBIO6.js.map → config-6YTWPDBK.js.map} +0 -0
- /package/dist/{diagnostics-HTCWF7F4.js.map → contract-HWLFFL3F.js.map} +0 -0
- /package/dist/{execute-LELOSR5T.js.map → diagnostics-YEFB42E5.js.map} +0 -0
- /package/dist/{git-A2IP32PQ.js.map → execute-QCT64D37.js.map} +0 -0
- /package/dist/{init-TO6MUXO4.js.map → git-XGOECXWG.js.map} +0 -0
- /package/dist/{permissions-G4XCAESF.js.map → init-4LB2YKWP.js.map} +0 -0
- /package/dist/{plan-OXAT344Q.js.map → permissions-QZPUUBSO.js.map} +0 -0
- /package/dist/{pr-UZHNKKZC.js.map → plan-2NMSHTMD.js.map} +0 -0
- /package/dist/{pr-review-Q4ACAWDM.js.map → pr-QKFO5HI3.js.map} +0 -0
- /package/dist/{prd-6CYEMLDY.js.map → pr-review-H2BFD6F2.js.map} +0 -0
- /package/dist/{recorder-2BAKPBRA.js.map → prd-ET6XKYRS.js.map} +0 -0
- /package/dist/{registry-XLHHYLTR.js.map → recorder-LOU3PTIE.js.map} +0 -0
- /package/dist/{review-LVZT2TMW.js.map → registry-HV7M6MAH.js.map} +0 -0
- /package/dist/{routing-EK6RBZM4.js.map → repository-RCRR6RDE.js.map} +0 -0
- /package/dist/{run-2RXJCJKT.js.map → review-2MTZLNSS.js.map} +0 -0
- /package/dist/{session-publisher-KAJQRHNZ.js.map → routing-RM5ABHHD.js.map} +0 -0
- /package/dist/{state-manager-UPHO2EA2.js.map → run-XMACQBRJ.js.map} +0 -0
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_BRANCH_CONVENTION,
|
|
4
4
|
loadRepositoryPolicy
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-WQSGL5LX.js";
|
|
6
6
|
import {
|
|
7
7
|
loadPolicyConfig
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import {
|
|
10
|
-
getBaseBranch
|
|
11
|
-
} from "./chunk-YVPZUN6X.js";
|
|
8
|
+
} from "./chunk-YC24E66P.js";
|
|
12
9
|
import {
|
|
13
10
|
getSessionPublisher
|
|
14
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-DIMDAYZS.js";
|
|
15
12
|
import {
|
|
16
13
|
hasUsageData,
|
|
17
14
|
isoNow,
|
|
18
15
|
sumUsage
|
|
19
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-JCR5C5A7.js";
|
|
17
|
+
import {
|
|
18
|
+
getBaseBranch
|
|
19
|
+
} from "./chunk-KUA6KBBD.js";
|
|
20
20
|
|
|
21
21
|
// src/core/session-metrics.ts
|
|
22
22
|
function elapsedSecondsSince(startedAtMs) {
|
|
@@ -219,7 +219,7 @@ function renderPolicySummary(policy, budgetTokens) {
|
|
|
219
219
|
|
|
220
220
|
${section.body}`;
|
|
221
221
|
const cost = estimateTokens(block);
|
|
222
|
-
if (used + cost <= budgetTokens) {
|
|
222
|
+
if (used + cost <= budgetTokens || ["Base branch", "Conventions", "Policy documents"].includes(section.title)) {
|
|
223
223
|
rendered.push(block);
|
|
224
224
|
used += cost;
|
|
225
225
|
continue;
|
|
@@ -241,6 +241,7 @@ function policyPlaceholders(policy, options = {}) {
|
|
|
241
241
|
if (policy === null || !policy.enabled || isEmptyPolicy(policy)) {
|
|
242
242
|
return emptyPolicyPlaceholders();
|
|
243
243
|
}
|
|
244
|
+
policy = policyForPhase(policy, options.phase);
|
|
244
245
|
const budget = options.budgetTokens ?? DEFAULT_POLICY_CONTEXT_BUDGET;
|
|
245
246
|
return {
|
|
246
247
|
__REPO_POLICY__: renderPolicySummary(policy, budget),
|
|
@@ -255,6 +256,30 @@ function policyPlaceholders(policy, options = {}) {
|
|
|
255
256
|
__REPO_DOCS__: renderDocs(policy.docs)
|
|
256
257
|
};
|
|
257
258
|
}
|
|
259
|
+
function policyForPhase(policy, phase) {
|
|
260
|
+
if (!phase || phase === "generate" || phase === "analyze") return policy;
|
|
261
|
+
const publication = phase === "pr" || phase === "pr-review";
|
|
262
|
+
return {
|
|
263
|
+
...policy,
|
|
264
|
+
issues: {
|
|
265
|
+
...policy.issues,
|
|
266
|
+
templates: publication ? policy.issues.templates : [],
|
|
267
|
+
types: publication ? policy.issues.types : [],
|
|
268
|
+
labels: publication ? policy.issues.labels : [],
|
|
269
|
+
titleConvention: null
|
|
270
|
+
},
|
|
271
|
+
pullRequests: {
|
|
272
|
+
...policy.pullRequests,
|
|
273
|
+
template: publication ? policy.pullRequests.template : null,
|
|
274
|
+
templates: publication ? policy.pullRequests.templates : [],
|
|
275
|
+
titleConvention: publication ? policy.pullRequests.titleConvention : null
|
|
276
|
+
},
|
|
277
|
+
git: {
|
|
278
|
+
...policy.git,
|
|
279
|
+
pullRequestTitleConvention: publication ? policy.git.pullRequestTitleConvention : null
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
258
283
|
var DEFAULT_POLICY_CONTEXT_BUDGET = 1500;
|
|
259
284
|
function conventionPlaceholders(policy, baseBranchFallback) {
|
|
260
285
|
return {
|
|
@@ -269,15 +294,22 @@ async function resolvePolicyPlaceholders(options = {}) {
|
|
|
269
294
|
let policy = null;
|
|
270
295
|
let projection = emptyPolicyPlaceholders();
|
|
271
296
|
try {
|
|
272
|
-
policy = await loadRepositoryPolicy({
|
|
297
|
+
policy = await loadRepositoryPolicy({
|
|
298
|
+
root: options.root,
|
|
299
|
+
scope: options.scope ?? null,
|
|
300
|
+
remote: options.remote
|
|
301
|
+
});
|
|
273
302
|
const config = await loadPolicyConfig({ projectRoot: policy.root });
|
|
274
|
-
projection = policyPlaceholders(policy, {
|
|
303
|
+
projection = policyPlaceholders(policy, {
|
|
304
|
+
budgetTokens: config.contextBudget,
|
|
305
|
+
phase: options.phase
|
|
306
|
+
});
|
|
275
307
|
} catch {
|
|
276
308
|
policy = null;
|
|
277
309
|
}
|
|
278
310
|
let fallbackBase = "main";
|
|
279
311
|
try {
|
|
280
|
-
fallbackBase = await getBaseBranch();
|
|
312
|
+
if (options.remote !== false) fallbackBase = await getBaseBranch();
|
|
281
313
|
} catch {
|
|
282
314
|
}
|
|
283
315
|
return { ...projection, ...conventionPlaceholders(policy, fallbackBase) };
|
|
@@ -294,4 +326,4 @@ export {
|
|
|
294
326
|
publishStoryMetrics,
|
|
295
327
|
resolvePolicyPlaceholders
|
|
296
328
|
};
|
|
297
|
-
//# sourceMappingURL=chunk-
|
|
329
|
+
//# sourceMappingURL=chunk-KUL64XUF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/session-metrics.ts","../src/policy/placeholders.ts"],"sourcesContent":["import { type ClaudeUsage, hasUsageData, sumUsage } from './metrics.js';\nimport { getSessionPublisher } from './session-publisher.js';\nimport { isoNow } from './state-manager.js';\n\n/**\n * Publication helper for the single-invocation phases (analyze, generate, prd,\n * plan, review, pr, pr-review).\n *\n * Each of them owns its own `runHeadless` call, so the metrics are published\n * from inside the command rather than from the `instrumentedRunners` wrapper in\n * run.ts: the wrapper only sees `() => Promise<void>` and never touches the\n * HeadlessResult. Publishing here also covers standalone invocations\n * (`issue-flow prd 42`), which never go through the pipeline wrapper at all.\n */\n\n/** Whole seconds elapsed since a `Date.now()` mark, never negative. */\nexport function elapsedSecondsSince(startedAtMs: number): number {\n return Math.max(0, Math.round((Date.now() - startedAtMs) / 1000));\n}\n\n/**\n * Process-owned totals, accumulated alongside every published event.\n *\n * The session snapshot now stays populated without `--web` (`MemoryPublisher`),\n * but the end-of-run summary box still reads these process-owned counters so a\n * standalone phase command (no publisher installed) does not print zeros.\n *\n * Only phase- and iteration-scoped usage is recorded. Story metrics are a\n * rateio of an iteration already counted here — adding them would double the\n * totals, the same rule the reducer follows.\n */\ninterface UsageAccumulator {\n all: ClaudeUsage;\n byPhase: Map<string, ClaudeUsage>;\n byAgent: Map<string, ClaudeUsage>;\n}\n\nfunction createAccumulator(): UsageAccumulator {\n return { all: {}, byPhase: new Map(), byAgent: new Map() };\n}\n\n/**\n * Stack of active accumulators, innermost last.\n *\n * The bottom entry is the process total and always exists. A multi-issue run\n * pushes one accumulator per issue (and one for the whole queue) so the terminal\n * summary of issue B never inherits what issue A spent — the leak `core/CLAUDE.md`\n * warned about when it stated that the module-level counters were safe \"only\n * under the current one-issue-per-process model\".\n *\n * Every publication feeds **all** active accumulators, so a scope is a view of\n * the same events rather than a redirection of them.\n */\nconst scopes: UsageAccumulator[] = [createAccumulator()];\n\n/** Innermost active scope: what `run` and the engine report right now. */\nfunction current(): UsageAccumulator {\n return scopes[scopes.length - 1] as UsageAccumulator;\n}\n\nfunction recordRunUsage(phase: string, usage: ClaudeUsage, providerId?: string): void {\n for (const scope of scopes) {\n scope.all = sumUsage(scope.all, usage);\n scope.byPhase.set(phase, sumUsage(scope.byPhase.get(phase), usage));\n const agent = providerId ?? 'claude';\n scope.byAgent.set(agent, sumUsage(scope.byAgent.get(agent), usage));\n }\n}\n\n/** Everything the innermost active scope has spent, across all phases. */\nexport function getRunUsageTotals(): ClaudeUsage {\n return { ...current().all };\n}\n\n/** What a single phase has spent so far. Empty when the phase reported nothing. */\nexport function getPhaseUsageTotals(phase: string): ClaudeUsage {\n return { ...(current().byPhase.get(phase) ?? {}) };\n}\n\n/** A usage scope opened by {@link beginUsageScope}. */\nexport interface UsageScope {\n /** Everything published while this scope was open. */\n totals(): ClaudeUsage;\n /** What one phase published while this scope was open. */\n phaseTotals(phase: string): ClaudeUsage;\n /** Close the scope. Idempotent, and safe to call out of order. */\n end(): ClaudeUsage;\n}\n\n/**\n * Open a nested usage scope, for a run that processes several issues in one\n * process.\n *\n * The scope accumulates only what is published while it is open, which is what\n * lets a queue report a per-issue cost and a consolidated total from the same\n * stream of events. Closing it never discards anything: the outer scopes were\n * fed in parallel.\n */\nexport function beginUsageScope(): UsageScope {\n const accumulator = createAccumulator();\n scopes.push(accumulator);\n let open = true;\n\n return {\n totals: () => ({ ...accumulator.all }),\n phaseTotals: (phase: string) => ({ ...(accumulator.byPhase.get(phase) ?? {}) }),\n end: () => {\n if (open) {\n const index = scopes.indexOf(accumulator);\n // Never pop the process total, even if a caller double-ends a scope.\n if (index > 0) scopes.splice(index, 1);\n open = false;\n }\n return { ...accumulator.all };\n },\n };\n}\n\n/** Test seam: the counters are module state and must be cleared between runs. */\nexport function resetRunUsageTotals(): void {\n scopes.splice(0, scopes.length, createAccumulator());\n}\n\n/**\n * Publish one `metrics:update` event of scope `phase` for a headless\n * invocation.\n *\n * A no-op when the CLI reported no usage at all: an event with every field\n * undefined would bump the snapshot version without adding information, and the\n * reducer would leave every counter at null anyway.\n *\n * A command with more than one invocation (the retrying phases, or `review`\n * inside a correction cycle) calls this once per invocation — the reducer's\n * summing is what turns them into the phase total.\n *\n * Never throws and never returns a value: publishing metrics must not be able\n * to change a command's exit code.\n */\nexport function publishPhaseMetrics(\n phase: string,\n usage: ClaudeUsage | null | undefined,\n startedAtMs?: number,\n providerId?: string,\n): void {\n if (usage == null || !hasUsageData(usage)) return;\n\n recordRunUsage(phase, usage, providerId);\n\n getSessionPublisher().publish({\n type: 'metrics:update',\n at: isoNow(),\n scope: 'phase',\n phase,\n ...usage,\n // Informational only: a phase's durationSeconds keeps coming exclusively\n // from phase:start/phase:end, so the reducer ignores this for scope 'phase'.\n durationSeconds: startedAtMs === undefined ? undefined : elapsedSecondsSince(startedAtMs),\n });\n}\n\n/** The execute loop is the only producer of iteration- and story-scoped metrics. */\nexport const EXECUTE_PHASE = 'execute';\n\n/**\n * Publish one `metrics:update` event of scope `iteration` for a pass of the\n * execute loop.\n *\n * Lands on the `execute` phase and on the issue-wide aggregate, exactly like a\n * phase-scoped event: an iteration *is* a slice of the execute phase. Failed\n * iterations (transient or fatal) publish too — those tokens were spent.\n *\n * A no-op when the CLI reported no usage. Never throws.\n */\nexport function publishIterationMetrics(\n iteration: number,\n usage: ClaudeUsage | null | undefined,\n durationSeconds?: number,\n providerId?: string,\n): void {\n if (usage == null || !hasUsageData(usage)) return;\n\n recordRunUsage(EXECUTE_PHASE, usage, providerId);\n\n getSessionPublisher().publish({\n type: 'metrics:update',\n at: isoNow(),\n scope: 'iteration',\n phase: EXECUTE_PHASE,\n iteration,\n ...usage,\n // Informational only: the reducer keeps a phase's durationSeconds coming\n // exclusively from phase:start/phase:end.\n durationSeconds,\n });\n}\n\n/**\n * Publish one `metrics:update` event of scope `story`.\n *\n * The values are the rateio of the iteration that completed the story, so the\n * reducer deliberately keeps them off the phase and the aggregate — the\n * iteration event of the same cycle already counted them there.\n *\n * A no-op when there is neither usage nor a duration to report. Never throws.\n */\nexport function publishStoryMetrics(\n storyId: string,\n usage: ClaudeUsage | null | undefined,\n durationSeconds?: number,\n): void {\n if (!hasUsageData(usage) && durationSeconds === undefined) return;\n\n getSessionPublisher().publish({\n type: 'metrics:update',\n at: isoNow(),\n scope: 'story',\n storyId,\n ...(usage ?? {}),\n durationSeconds,\n });\n}\n","import type { AgentPhase } from '../agents/types.js';\nimport { loadPolicyConfig } from '../config.js';\nimport { DEFAULT_BRANCH_CONVENTION } from '../conventions/git/index.js';\nimport { getBaseBranch } from '../utils/git.js';\nimport { loadRepositoryPolicy } from './resolve.js';\nimport type { IssueTemplate, PolicyDocument, RepositoryPolicy } from './types.js';\n\n/**\n * Projection of a {@link RepositoryPolicy} onto the prompt placeholders.\n *\n * The prompts receive a **summary**, never the raw files. Dumping\n * `CONTRIBUTING.md` and three `AGENTS.md` into every prompt would cost tokens on\n * every single run and drown the actual instruction — and it would duplicate,\n * inside Issue Flow, a rule that already lives in the repository.\n *\n * The whole projection is empty when the repository declares nothing, which is\n * what lets a prompt render byte for byte as it did before this layer existed.\n */\n\n/** Every placeholder this module produces. Ordered as they appear in a prompt. */\nexport const POLICY_PLACEHOLDER_KEYS = [\n '__REPO_POLICY__',\n '__REPO_ISSUE_TEMPLATES__',\n '__REPO_LABELS__',\n '__REPO_ISSUE_TYPES__',\n '__REPO_PR_TEMPLATE__',\n '__REPO_BASE_BRANCH__',\n '__REPO_CONVENTIONS__',\n '__REPO_DOCS__',\n] as const;\n\nexport type PolicyPlaceholderKey = (typeof POLICY_PLACEHOLDER_KEYS)[number];\n\n/** Every placeholder empty — the projection of \"this repository declares nothing\". */\nexport function emptyPolicyPlaceholders(): Record<string, string> {\n return Object.fromEntries(POLICY_PLACEHOLDER_KEYS.map((key) => [key, ''])) as Record<\n string,\n string\n >;\n}\n\n/**\n * Rough token count of a rendered section.\n *\n * Four characters per token is the usual English/Portuguese approximation and is\n * deliberately crude: the budget exists to stop a policy from swallowing the\n * prompt, not to bill anyone. Erring high is the safe direction, so the estimate\n * rounds up.\n */\nexport function estimateTokens(text: string): number {\n return Math.ceil(text.length / 4);\n}\n\n// ── Section renderers ───────────────────────────────────────────────────────\n\nfunction renderIssueTemplates(templates: IssueTemplate[]): string {\n if (templates.length === 0) return '';\n\n const lines = templates.map((template) => {\n const parts = [`- **${template.name ?? template.path}** (\\`${template.path}\\`)`];\n if (template.type !== null) parts.push(`type: ${template.type}`);\n if (template.labels.length > 0) parts.push(`labels: ${template.labels.join(', ')}`);\n if (template.title !== null) parts.push(`title prefix: \\`${template.title}\\``);\n if (template.about !== null) parts.push(template.about);\n return parts.join(' — ');\n });\n\n return lines.join('\\n');\n}\n\nfunction renderLabels(policy: RepositoryPolicy): string {\n if (policy.issues.labels.length === 0) return '';\n\n return policy.issues.labels\n .map((label) =>\n label.description === null ? `- ${label.name}` : `- ${label.name} — ${label.description}`,\n )\n .join('\\n');\n}\n\nfunction renderConventions(policy: RepositoryPolicy): string {\n const rows: string[] = [];\n if (policy.issues.titleConvention !== null) {\n rows.push(`- Issue title: ${policy.issues.titleConvention}`);\n }\n if (policy.pullRequests.titleConvention !== null) {\n rows.push(`- Pull Request title: ${policy.pullRequests.titleConvention}`);\n }\n if (policy.git.branchConvention !== null) {\n rows.push(`- Branch: ${policy.git.branchConvention}`);\n }\n if (policy.git.commitConvention !== null) {\n rows.push(`- Commit: ${policy.git.commitConvention}`);\n }\n if (policy.git.pullRequestTitleConvention !== null) {\n rows.push(`- Pull Request title: ${policy.git.pullRequestTitleConvention}`);\n }\n if (policy.git.allowedTypes !== null && policy.git.allowedTypes.length > 0) {\n rows.push(`- Allowed types: ${policy.git.allowedTypes.join(', ')}`);\n }\n return rows.join('\\n');\n}\n\n/**\n * Paths of the applicable documents — **never their content**.\n *\n * The agent has `Read` and fetches what it needs. Embedding the documents would\n * multiply the cost of every run and, worse, freeze a copy of the repository's\n * own rule inside a prompt.\n */\nfunction renderDocs(docs: PolicyDocument[]): string {\n if (docs.length === 0) return '';\n\n return docs\n .map((doc) => {\n const scope = doc.scope === '' ? '' : ` (applies to \\`${doc.scope}/\\`)`;\n return `- \\`${doc.path}\\`${scope}`;\n })\n .join('\\n');\n}\n\n// ── The budgeted summary ────────────────────────────────────────────────────\n\ninterface Section {\n /** Heading inside the summary. */\n title: string;\n body: string;\n /**\n * Kept whole even when the budget is blown. The essentials are the taxonomy\n * an agent cannot guess: it can re-read a document, but it cannot invent the\n * label a repository actually uses.\n */\n essential: boolean;\n /** Shown in place of the body once the budget is exhausted. */\n pointer: string;\n}\n\n/**\n * Compose optional policy within `budgetTokens`; mandatory pointers and conventions survive.\n *\n * Sections are added whole, in priority order, while they fit. A section that\n * does not fit is replaced **whole** by a pointer to where the agent can read it\n * — nothing is ever cut mid-sentence, because a summary truncated mid-rule is\n * worse than one that says where the rule lives.\n */\nexport function renderPolicySummary(policy: RepositoryPolicy, budgetTokens: number): string {\n const sections: Section[] = [\n {\n title: 'Base branch',\n body: policy.pullRequests.baseBranch ?? '',\n essential: true,\n pointer: '',\n },\n {\n title: 'Conventions',\n body: renderConventions(policy),\n essential: true,\n pointer: '',\n },\n {\n title: 'Issue Types',\n body: policy.issues.types.join(', '),\n essential: true,\n pointer: '',\n },\n {\n title: 'Issue Templates',\n body: renderIssueTemplates(policy.issues.templates),\n essential: true,\n pointer: 'Run `issue-flow policy --json` to read the templates in full.',\n },\n {\n title: 'Labels',\n body: renderLabels(policy),\n essential: true,\n pointer: 'Run `gh label list` to read the labels of this repository.',\n },\n {\n title: 'Pull Request template',\n body: policy.pullRequests.templates[0]?.path ?? '',\n essential: false,\n pointer: '',\n },\n {\n title: 'Policy documents',\n body: renderDocs(policy.docs),\n essential: false,\n pointer: '',\n },\n ];\n\n const rendered: string[] = [];\n let used = 0;\n\n for (const section of sections) {\n if (section.body.trim() === '') continue;\n\n const block = `### ${section.title}\\n\\n${section.body}`;\n const cost = estimateTokens(block);\n\n if (\n used + cost <= budgetTokens ||\n ['Base branch', 'Conventions', 'Policy documents'].includes(section.title)\n ) {\n rendered.push(block);\n used += cost;\n continue;\n }\n\n // Over budget. An essential section keeps its slot as a pointer; a\n // non-essential optional one drops out. Mandatory pointers above always survive.\n if (section.essential && section.pointer !== '') {\n const fallback = `### ${section.title}\\n\\n${section.pointer}`;\n rendered.push(fallback);\n used += estimateTokens(fallback);\n }\n }\n\n return rendered.join('\\n\\n');\n}\n\n/** True when the policy carries nothing a prompt could act on. */\nexport function isEmptyPolicy(policy: RepositoryPolicy): boolean {\n return (\n policy.issues.templates.length === 0 &&\n policy.issues.types.length === 0 &&\n policy.issues.labels.length === 0 &&\n policy.issues.titleConvention === null &&\n policy.pullRequests.templates.length === 0 &&\n policy.pullRequests.baseBranch === null &&\n policy.pullRequests.titleConvention === null &&\n policy.git.branchConvention === null &&\n policy.git.commitConvention === null &&\n policy.docs.length === 0\n );\n}\n\nexport interface PolicyPlaceholderOptions {\n /** Token budget for `__REPO_POLICY__`. Defaults to the resolved configuration. */\n budgetTokens?: number;\n phase?: AgentPhase;\n}\n\n/**\n * Project a resolved policy onto the prompt placeholders.\n *\n * A `null` policy — or one that carries nothing — yields every placeholder\n * empty, which is what makes the rendered prompt identical to the pre-policy\n * one. This is the contract the whole feature rests on.\n */\nexport function policyPlaceholders(\n policy: RepositoryPolicy | null,\n options: PolicyPlaceholderOptions = {},\n): Record<string, string> {\n if (policy === null || !policy.enabled || isEmptyPolicy(policy)) {\n return emptyPolicyPlaceholders();\n }\n\n policy = policyForPhase(policy, options.phase);\n const budget = options.budgetTokens ?? DEFAULT_POLICY_CONTEXT_BUDGET;\n\n return {\n __REPO_POLICY__: renderPolicySummary(policy, budget),\n __REPO_ISSUE_TEMPLATES__: renderIssueTemplates(policy.issues.templates),\n __REPO_LABELS__: renderLabels(policy),\n __REPO_ISSUE_TYPES__: policy.issues.types.join(', '),\n // The one place a whole file is projected verbatim: a Pull Request body has\n // to *be* the template, so a summary of it would be useless.\n __REPO_PR_TEMPLATE__: policy.pullRequests.template ?? '',\n __REPO_BASE_BRANCH__: policy.pullRequests.baseBranch ?? '',\n __REPO_CONVENTIONS__: renderConventions(policy),\n __REPO_DOCS__: renderDocs(policy.docs),\n };\n}\n\n/** Select decision-relevant policy without rediscovering or mutating its source. */\nexport function policyForPhase(policy: RepositoryPolicy, phase?: AgentPhase): RepositoryPolicy {\n if (!phase || phase === 'generate' || phase === 'analyze') return policy;\n const publication = phase === 'pr' || phase === 'pr-review';\n return {\n ...policy,\n issues: {\n ...policy.issues,\n templates: publication ? policy.issues.templates : [],\n types: publication ? policy.issues.types : [],\n labels: publication ? policy.issues.labels : [],\n titleConvention: null,\n },\n pullRequests: {\n ...policy.pullRequests,\n template: publication ? policy.pullRequests.template : null,\n templates: publication ? policy.pullRequests.templates : [],\n titleConvention: publication ? policy.pullRequests.titleConvention : null,\n },\n git: {\n ...policy.git,\n pullRequestTitleConvention: publication ? policy.git.pullRequestTitleConvention : null,\n },\n };\n}\n\n/** Default token budget for `__REPO_POLICY__`, overridable via `policy.contextBudget`. */\nexport const DEFAULT_POLICY_CONTEXT_BUDGET = 1500;\n\n// ── Conventions that always have a value ────────────────────────────────────\n\nexport { DEFAULT_BRANCH_CONVENTION } from '../conventions/git/index.js';\n\n/**\n * Placeholders that must **never** render empty, because a prompt puts them\n * inside a command it asks the agent to run.\n *\n * `__BASE_BRANCH__` is the reason this group exists separately from the policy\n * projection. `prompts/pr.md` used to spell `main` three times — in\n * `git log main..HEAD`, in `git diff main...HEAD` and in `gh pr create --base\n * main`. In a repository whose base is `develop` that is not a missing feature\n * but an active defect: `main` often *exists* there too, so nothing fails — the\n * agent simply reviews the wrong diff and opens the Pull Request against the\n * wrong target.\n *\n * The resolution order is the repository's declared or discovered base, then\n * `getBaseBranch()`, whose own fallback is `main`. So a repository on `main`\n * renders exactly the text it rendered before.\n */\nexport function conventionPlaceholders(\n policy: RepositoryPolicy | null,\n baseBranchFallback: string,\n): Record<string, string> {\n return {\n __BASE_BRANCH__: policy?.pullRequests.baseBranch ?? baseBranchFallback,\n __BRANCH_CONVENTION__: policy?.git.branchConvention ?? DEFAULT_BRANCH_CONVENTION,\n // The one member of this group that may legitimately render empty: it gates\n // a conditional section rather than sitting inside a command.\n __COMMIT_CONVENTION__: policy?.git.commitConvention ?? '',\n };\n}\n\nexport interface ResolvePolicyPlaceholdersOptions {\n phase?: AgentPhase;\n remote?: boolean;\n /** Repository root. Defaults to the git project root. */\n root?: string;\n /** Subdirectory the policy applies to, for monorepos. */\n scope?: string | null;\n}\n\n/**\n * Resolve the repository policy and project it, in one call.\n *\n * This is what every command uses. It **never throws and never warns**: a\n * repository that is not a git checkout, a discovery that fails, a `gh` that is\n * not installed — all of them yield the empty projection, and the command runs\n * exactly as it did before.\n *\n * The resolution itself is cached per `(root, scope)` by `loadRepositoryPolicy`,\n * so a pipeline that renders eight prompts still discovers once.\n */\nexport async function resolvePolicyPlaceholders(\n options: ResolvePolicyPlaceholdersOptions = {},\n): Promise<Record<string, string>> {\n let policy: RepositoryPolicy | null = null;\n let projection = emptyPolicyPlaceholders();\n try {\n policy = await loadRepositoryPolicy({\n root: options.root,\n scope: options.scope ?? null,\n remote: options.remote,\n });\n const config = await loadPolicyConfig({ projectRoot: policy.root });\n projection = policyPlaceholders(policy, {\n budgetTokens: config.contextBudget,\n phase: options.phase,\n });\n } catch {\n // Discovery is an enrichment; the conventions below still have to resolve.\n policy = null;\n }\n\n let fallbackBase = 'main';\n try {\n if (options.remote !== false) fallbackBase = await getBaseBranch();\n } catch {\n // getBaseBranch() does not throw today, but its contract is \"never fails\",\n // and a base branch that cannot be resolved must not cost the whole prompt.\n }\n\n return { ...projection, ...conventionPlaceholders(policy, fallbackBase) };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,23 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/storage/artifact-paths.ts
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
var ISSUES_DIR_NAME = "issues";
|
|
6
|
+
var SESSION_FILENAME = "session.json";
|
|
7
|
+
var EVENTS_FILENAME = "events.jsonl";
|
|
8
|
+
var ROTATED_EVENTS_FILENAME = "events.1.jsonl";
|
|
9
|
+
var PRD_FILENAME = "prd.md";
|
|
10
|
+
var TASKS_FILENAME = "tasks.json";
|
|
11
|
+
var VERIFY_FILENAME = "verify.json";
|
|
12
|
+
var RUN_LOG_FILENAME = "run.log";
|
|
13
|
+
var ROTATED_RUN_LOG_FILENAME = "run.log.1";
|
|
14
|
+
function normalizeIssueIdentifier(issueNumber) {
|
|
15
|
+
const normalized = String(issueNumber).trim().replace(/^#/, "");
|
|
16
|
+
if (normalized.length === 0) throw new Error("Issue identifier cannot be empty");
|
|
17
|
+
if (/[/\\]/.test(normalized) || normalized === "." || normalized === "..") {
|
|
18
|
+
throw new Error(`Invalid issue identifier: '${issueNumber}'`);
|
|
19
|
+
}
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
function resolveIssueArtifactPaths(projectDir, issueNumber) {
|
|
23
|
+
const issueDir = join(projectDir, ISSUES_DIR_NAME, normalizeIssueIdentifier(issueNumber));
|
|
24
|
+
return {
|
|
25
|
+
issueDir,
|
|
26
|
+
issueFile: join(issueDir, "issue.md"),
|
|
27
|
+
metadataFile: join(issueDir, "metadata.json"),
|
|
28
|
+
prdFile: join(issueDir, PRD_FILENAME),
|
|
29
|
+
tasksFile: join(issueDir, TASKS_FILENAME),
|
|
30
|
+
progressFile: join(issueDir, "progress.txt"),
|
|
31
|
+
analysisFile: join(issueDir, "analysis.md"),
|
|
32
|
+
sessionFile: join(issueDir, SESSION_FILENAME),
|
|
33
|
+
eventsFile: join(issueDir, EVENTS_FILENAME),
|
|
34
|
+
rotatedEventsFile: join(issueDir, ROTATED_EVENTS_FILENAME),
|
|
35
|
+
runLogFile: join(issueDir, RUN_LOG_FILENAME),
|
|
36
|
+
rotatedRunLogFile: join(issueDir, ROTATED_RUN_LOG_FILENAME),
|
|
37
|
+
decompositionFile: join(issueDir, "decomposition.md"),
|
|
38
|
+
verifyFile: join(issueDir, VERIFY_FILENAME),
|
|
39
|
+
lastBranchFile: join(issueDir, ".last-branch"),
|
|
40
|
+
archiveDir: join(issueDir, "archive"),
|
|
41
|
+
prReviewDir: join(issueDir, "pr-review")
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
3
45
|
// src/storage/paths.ts
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { basename, join, resolve } from "path";
|
|
46
|
+
import { homedir } from "node:os";
|
|
47
|
+
import { join as join2, resolve } from "node:path";
|
|
7
48
|
var GLOBAL_DIR_NAME = ".issue-flow";
|
|
8
49
|
var GLOBAL_ROOT_ENV = "ISSUE_FLOW_HOME";
|
|
9
50
|
var PROJECTS_DIR_NAME = "projects";
|
|
10
51
|
var LOGS_DIR_NAME = "logs";
|
|
11
|
-
var ISSUES_DIR_NAME = "issues";
|
|
12
52
|
var QUEUES_DIR_NAME = "queues";
|
|
13
53
|
var RUN_LOCK_FILENAME = "run.lock";
|
|
14
54
|
var PROVIDERS_HEALTH_FILENAME = "providers.json";
|
|
15
|
-
var SESSION_FILENAME = "session.json";
|
|
16
|
-
var EVENTS_FILENAME = "events.jsonl";
|
|
17
|
-
var ROTATED_EVENTS_FILENAME = "events.1.jsonl";
|
|
18
|
-
var VERIFY_FILENAME = "verify.json";
|
|
19
|
-
var RUN_LOG_FILENAME = "run.log";
|
|
20
|
-
var ROTATED_RUN_LOG_FILENAME = "run.log.1";
|
|
21
55
|
function getGlobalRoot(options = {}) {
|
|
22
56
|
const env = options.env ?? process.env;
|
|
23
57
|
const override = env[GLOBAL_ROOT_ENV]?.trim();
|
|
@@ -35,42 +69,17 @@ function getGlobalRoot(options = {}) {
|
|
|
35
69
|
`Unable to resolve the home directory for the global storage. Set ${GLOBAL_ROOT_ENV} to an explicit path.`
|
|
36
70
|
);
|
|
37
71
|
}
|
|
38
|
-
return
|
|
72
|
+
return join2(home, GLOBAL_DIR_NAME);
|
|
39
73
|
}
|
|
40
74
|
function getDiagnosticsDir(options = {}) {
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
var SLUG_MAX_LENGTH = 32;
|
|
44
|
-
var HASH_LENGTH = 12;
|
|
45
|
-
var FALLBACK_SLUG = "project";
|
|
46
|
-
function slugify(name) {
|
|
47
|
-
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SLUG_MAX_LENGTH).replace(/-+$/g, "");
|
|
48
|
-
return slug === "" ? FALLBACK_SLUG : slug;
|
|
49
|
-
}
|
|
50
|
-
function projectIdFromRemote(remote, projectRoot) {
|
|
51
|
-
let seed;
|
|
52
|
-
let name;
|
|
53
|
-
if (remote) {
|
|
54
|
-
seed = `remote:${remote}`;
|
|
55
|
-
name = remote.split("/").pop() ?? "";
|
|
56
|
-
} else {
|
|
57
|
-
const absolute = resolve(projectRoot);
|
|
58
|
-
seed = `path:${absolute}`;
|
|
59
|
-
name = basename(absolute);
|
|
60
|
-
}
|
|
61
|
-
const hash = createHash("sha256").update(seed).digest("hex").slice(0, HASH_LENGTH);
|
|
62
|
-
return `${slugify(name)}-${hash}`;
|
|
75
|
+
return join2(getGlobalRoot(options), LOGS_DIR_NAME);
|
|
63
76
|
}
|
|
64
77
|
function getProjectDir(projectId, options = {}) {
|
|
65
|
-
return
|
|
78
|
+
return join2(getGlobalRoot(options), PROJECTS_DIR_NAME, projectId);
|
|
66
79
|
}
|
|
67
|
-
function
|
|
68
|
-
const queueDir =
|
|
69
|
-
|
|
70
|
-
QUEUES_DIR_NAME,
|
|
71
|
-
normalizeIssueNumber(queueId)
|
|
72
|
-
);
|
|
73
|
-
return { queueDir, planFile: join(queueDir, "execution-plan.json") };
|
|
80
|
+
function getQueuePathsAt(projectDir, queueId) {
|
|
81
|
+
const queueDir = join2(projectDir, QUEUES_DIR_NAME, normalizeIssueNumber(queueId));
|
|
82
|
+
return { queueDir, planFile: join2(queueDir, "execution-plan.json") };
|
|
74
83
|
}
|
|
75
84
|
function normalizeIssueNumber(issueNumber) {
|
|
76
85
|
const normalized = String(issueNumber).trim().replace(/^#/, "");
|
|
@@ -83,52 +92,30 @@ function normalizeIssueNumber(issueNumber) {
|
|
|
83
92
|
return normalized;
|
|
84
93
|
}
|
|
85
94
|
function getIssuePaths(projectId, issueNumber, options = {}) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
91
|
-
return {
|
|
92
|
-
issueDir,
|
|
93
|
-
issueFile: join(issueDir, "issue.md"),
|
|
94
|
-
metadataFile: join(issueDir, "metadata.json"),
|
|
95
|
-
prdFile: join(issueDir, "prd.md"),
|
|
96
|
-
tasksFile: join(issueDir, "tasks.json"),
|
|
97
|
-
progressFile: join(issueDir, "progress.txt"),
|
|
98
|
-
analysisFile: join(issueDir, "analysis.md"),
|
|
99
|
-
sessionFile: join(issueDir, SESSION_FILENAME),
|
|
100
|
-
// The journal, and the single generation kept when it rotates. The
|
|
101
|
-
// snapshot above is the projection; this pair is the history.
|
|
102
|
-
eventsFile: join(issueDir, EVENTS_FILENAME),
|
|
103
|
-
rotatedEventsFile: join(issueDir, ROTATED_EVENTS_FILENAME),
|
|
104
|
-
runLogFile: join(issueDir, RUN_LOG_FILENAME),
|
|
105
|
-
rotatedRunLogFile: join(issueDir, ROTATED_RUN_LOG_FILENAME),
|
|
106
|
-
// "This issue looks larger than one run": written only when the signals
|
|
107
|
-
// agree, and read by a person rather than by the pipeline.
|
|
108
|
-
decompositionFile: join(issueDir, "decomposition.md"),
|
|
109
|
-
verifyFile: join(issueDir, VERIFY_FILENAME),
|
|
110
|
-
lastBranchFile: join(issueDir, ".last-branch"),
|
|
111
|
-
archiveDir: join(issueDir, "archive"),
|
|
112
|
-
prReviewDir: join(issueDir, "pr-review")
|
|
113
|
-
};
|
|
95
|
+
return getIssuePathsAt(getProjectDir(projectId, options), issueNumber);
|
|
96
|
+
}
|
|
97
|
+
function getIssuePathsAt(projectDir, issueNumber) {
|
|
98
|
+
return resolveIssueArtifactPaths(projectDir, issueNumber);
|
|
114
99
|
}
|
|
115
100
|
|
|
116
101
|
export {
|
|
117
|
-
GLOBAL_ROOT_ENV,
|
|
118
|
-
PROJECTS_DIR_NAME,
|
|
119
102
|
ISSUES_DIR_NAME,
|
|
120
|
-
QUEUES_DIR_NAME,
|
|
121
|
-
RUN_LOCK_FILENAME,
|
|
122
|
-
PROVIDERS_HEALTH_FILENAME,
|
|
123
103
|
SESSION_FILENAME,
|
|
124
104
|
EVENTS_FILENAME,
|
|
125
105
|
ROTATED_EVENTS_FILENAME,
|
|
106
|
+
PRD_FILENAME,
|
|
107
|
+
TASKS_FILENAME,
|
|
126
108
|
VERIFY_FILENAME,
|
|
109
|
+
GLOBAL_ROOT_ENV,
|
|
110
|
+
PROJECTS_DIR_NAME,
|
|
111
|
+
QUEUES_DIR_NAME,
|
|
112
|
+
RUN_LOCK_FILENAME,
|
|
113
|
+
PROVIDERS_HEALTH_FILENAME,
|
|
127
114
|
getGlobalRoot,
|
|
128
115
|
getDiagnosticsDir,
|
|
129
|
-
projectIdFromRemote,
|
|
130
116
|
getProjectDir,
|
|
131
|
-
|
|
132
|
-
getIssuePaths
|
|
117
|
+
getQueuePathsAt,
|
|
118
|
+
getIssuePaths,
|
|
119
|
+
getIssuePathsAt
|
|
133
120
|
};
|
|
134
|
-
//# sourceMappingURL=chunk-
|
|
121
|
+
//# sourceMappingURL=chunk-MAJ6QDRL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/storage/artifact-paths.ts","../src/storage/paths.ts"],"sourcesContent":["import { join } from 'node:path';\n\nexport const ISSUES_DIR_NAME = 'issues';\nexport const SESSION_FILENAME = 'session.json';\nexport const EVENTS_FILENAME = 'events.jsonl';\nexport const ROTATED_EVENTS_FILENAME = 'events.1.jsonl';\nexport const PRD_FILENAME = 'prd.md';\nexport const TASKS_FILENAME = 'tasks.json';\nexport const VERIFY_FILENAME = 'verify.json';\nexport const RUN_LOG_FILENAME = 'run.log';\nexport const ROTATED_RUN_LOG_FILENAME = 'run.log.1';\n\nexport interface IssuePaths {\n issueDir: string;\n issueFile: string;\n metadataFile: string;\n prdFile: string;\n tasksFile: string;\n progressFile: string;\n analysisFile: string;\n sessionFile: string;\n eventsFile: string;\n rotatedEventsFile: string;\n runLogFile: string;\n rotatedRunLogFile: string;\n decompositionFile: string;\n verifyFile: string;\n lastBranchFile: string;\n archiveDir: string;\n prReviewDir: string;\n}\n\n/** Validate an identifier before it becomes a path segment. */\nexport function normalizeIssueIdentifier(issueNumber: string | number): string {\n const normalized = String(issueNumber).trim().replace(/^#/, '');\n if (normalized.length === 0) throw new Error('Issue identifier cannot be empty');\n if (/[/\\\\]/.test(normalized) || normalized === '.' || normalized === '..') {\n throw new Error(`Invalid issue identifier: '${issueNumber}'`);\n }\n return normalized;\n}\n\n/** The single canonical layout shared by CLI and portable Skill helpers. */\nexport function resolveIssueArtifactPaths(\n projectDir: string,\n issueNumber: string | number,\n): IssuePaths {\n const issueDir = join(projectDir, ISSUES_DIR_NAME, normalizeIssueIdentifier(issueNumber));\n return {\n issueDir,\n issueFile: join(issueDir, 'issue.md'),\n metadataFile: join(issueDir, 'metadata.json'),\n prdFile: join(issueDir, PRD_FILENAME),\n tasksFile: join(issueDir, TASKS_FILENAME),\n progressFile: join(issueDir, 'progress.txt'),\n analysisFile: join(issueDir, 'analysis.md'),\n sessionFile: join(issueDir, SESSION_FILENAME),\n eventsFile: join(issueDir, EVENTS_FILENAME),\n rotatedEventsFile: join(issueDir, ROTATED_EVENTS_FILENAME),\n runLogFile: join(issueDir, RUN_LOG_FILENAME),\n rotatedRunLogFile: join(issueDir, ROTATED_RUN_LOG_FILENAME),\n decompositionFile: join(issueDir, 'decomposition.md'),\n verifyFile: join(issueDir, VERIFY_FILENAME),\n lastBranchFile: join(issueDir, '.last-branch'),\n archiveDir: join(issueDir, 'archive'),\n prReviewDir: join(issueDir, 'pr-review'),\n };\n}\n","import { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\nimport { getRemoteUrl, normalizeRemoteUrl } from '../utils/git.js';\nimport { ISSUES_DIR_NAME, type IssuePaths, resolveIssueArtifactPaths } from './artifact-paths.js';\nimport { projectIdFromRemote } from './project-identity.js';\n\nexport {\n EVENTS_FILENAME,\n ISSUES_DIR_NAME,\n type IssuePaths,\n PRD_FILENAME,\n ROTATED_EVENTS_FILENAME,\n ROTATED_RUN_LOG_FILENAME,\n RUN_LOG_FILENAME,\n SESSION_FILENAME,\n TASKS_FILENAME,\n VERIFY_FILENAME,\n} from './artifact-paths.js';\nexport { projectIdFromRemote } from './project-identity.js';\n\n/** Directory created under the user's home when no override is provided. */\nexport const GLOBAL_DIR_NAME = '.issue-flow';\n\n/** Environment variable that relocates the whole global storage tree. */\nexport const GLOBAL_ROOT_ENV = 'ISSUE_FLOW_HOME';\n\n/** Directory under the global root holding one folder per project. */\nexport const PROJECTS_DIR_NAME = 'projects';\n\n/** Machine-wide structured diagnostic logs, independent of any project. */\nexport const LOGS_DIR_NAME = 'logs';\n\n/** Directory under a project holding one folder per issue. */\n/** Directory under a project holding one folder per multi-issue execution queue. */\nexport const QUEUES_DIR_NAME = 'queues';\n\n/** Run-ownership lock, a sibling of `issues/` inside the project directory. */\nexport const RUN_LOCK_FILENAME = 'run.lock';\n\n/** Persisted agent-provider health, shared by every run of one project. */\nexport const PROVIDERS_HEALTH_FILENAME = 'providers.json';\n\n/** Projection and journal filenames shared by storage and the web reader. */\n/**\n * Acceptance-contract evidence. Named here — and not at the call site — because\n * `verify/run-issue.ts` also writes it in standalone mode, where there is no\n * `IssuePaths` to ask: one constant, two callers, no second spelling.\n */\n\nexport interface GetGlobalRootOptions {\n /** Environment source. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n}\n\n/**\n * Resolve the root directory of the global storage tree (`~/.issue-flow`).\n *\n * Every path under the global storage MUST be derived from this function: it is\n * the single seam where `ISSUE_FLOW_HOME` takes effect, which is what lets\n * tests, CI and sandboxes point the whole tree at a temporary directory instead\n * of touching the real `$HOME`. A call site that joins `homedir()` by hand\n * silently opts out of that isolation.\n *\n * Pure and synchronous: it never creates directories nor touches the\n * filesystem — callers decide when (and whether) the directory should exist.\n */\nexport function getGlobalRoot(options: GetGlobalRootOptions = {}): string {\n const env = options.env ?? process.env;\n\n const override = env[GLOBAL_ROOT_ENV]?.trim();\n if (override) {\n // A relative override is resolved against the CWD so that every consumer\n // gets an absolute path, exactly as in the homedir() branch below.\n return resolve(override);\n }\n\n // The only failure mode: an environment where the home directory cannot be\n // determined (some containers and CI images). The override is the escape\n // hatch, so the error names it.\n let home: string;\n try {\n home = homedir();\n } catch {\n home = '';\n }\n if (home.trim() === '') {\n throw new Error(\n `Unable to resolve the home directory for the global storage. Set ${GLOBAL_ROOT_ENV} to an explicit path.`,\n );\n }\n\n return join(home, GLOBAL_DIR_NAME);\n}\n\nexport function getDiagnosticsDir(options: GetGlobalRootOptions = {}): string {\n return join(getGlobalRoot(options), LOGS_DIR_NAME);\n}\n\n/**\n * Derive a stable identifier for a project, in the form `<slug>-<hash12>`.\n *\n * The seed is canonical rather than local, so the same repository yields the\n * same id on any machine: `remote:<normalizedRemote>` whenever an `origin`\n * remote exists, falling back to `path:<absoluteProjectRoot>` when it does not.\n * That is what makes moving or renaming the local folder harmless for a cloned\n * repository — the history stays attached to the remote identity.\n *\n * The `remote:` / `path:` prefix is part of the hashed seed, so a project\n * identified by path can never collide with one identified by remote even if\n * the two strings were to coincide. The slug is only cosmetic (it makes the\n * directory recognizable); the hash carries the identity.\n *\n * Known limitation of the path fallback: for a repository with no `origin`\n * remote, the absolute path *is* the identity. Moving or renaming that folder\n * produces a different id, and the previous history is left behind under the\n * old one. Configuring a remote before adopting the global storage avoids it.\n */\nexport async function getProjectId(projectRoot: string): Promise<string> {\n const remote = normalizeRemoteUrl(await getRemoteUrl(projectRoot));\n return projectIdFromRemote(remote, projectRoot);\n}\n\n/**\n * Pure half of {@link getProjectId}: derive the id once the normalized remote\n * (or `null`) is already known.\n *\n * Exported so a caller that already resolved the remote for another reason\n * (e.g. {@link resolveStorageMode} in `compat.ts`, which also persists it into\n * `metadata.json`) can compute the id without shelling out to git a second\n * time.\n */\n/**\n * Absolute directory of a project inside the global storage tree.\n *\n * `projectId` is expected to come from {@link getProjectId}, which already\n * guarantees a single safe path segment.\n */\nexport function getProjectDir(projectId: string, options: GetGlobalRootOptions = {}): string {\n return join(getGlobalRoot(options), PROJECTS_DIR_NAME, projectId);\n}\n\n/**\n * Every artifact of one issue, resolved from a single place.\n *\n * Note that `prdFile` is the requirements document (`prd.md`) and `tasksFile`\n * is the task plan (`tasks.json`) — unlike `ResolvedPaths.prdFile` in\n * `types.ts`, which historically points at `tasks.json`.\n */\n/**\n * Artifacts of one multi-issue execution queue.\n *\n * A queue coordinates several issues that share a branch; each issue keeps its\n * own directory under {@link ISSUES_DIR_NAME} exactly as before, and only the\n * coordination state (order, per-issue status, shared branch, consolidated Pull\n * Request) lives here. That is what keeps a single-issue run byte-identical: it\n * never creates a queue at all.\n */\nexport interface QueuePaths {\n queueDir: string;\n planFile: string;\n}\n\n/**\n * Resolve the paths of one execution queue under the global storage.\n *\n * The queue id is the identifier of the primary issue — the first one the user\n * asked for — so re-running the same command finds the same queue and resumes\n * it instead of starting a parallel one.\n *\n * Pure and synchronous like every other helper here: nothing is created.\n */\nexport function getQueuePaths(\n projectId: string,\n queueId: string | number,\n options: GetGlobalRootOptions = {},\n): QueuePaths {\n return getQueuePathsAt(getProjectDir(projectId, options), queueId);\n}\n\n/** Resolve one queue below an already selected project storage directory. */\nexport function getQueuePathsAt(projectDir: string, queueId: string | number): QueuePaths {\n const queueDir = join(projectDir, QUEUES_DIR_NAME, normalizeIssueNumber(queueId));\n\n return { queueDir, planFile: join(queueDir, 'execution-plan.json') };\n}\n\n/**\n * Issue identifiers become path segments, so anything that could escape the\n * issues directory is rejected before it reaches `join` — the same discipline\n * as `normalizeId()` in `issues/providers/local.ts`.\n *\n * Non-numeric identifiers (`auth-refactor`, `pr-184`) are first-class here:\n * the local provider already supports them, so the global storage must too.\n */\nfunction normalizeIssueNumber(issueNumber: string | number): string {\n const normalized = String(issueNumber).trim().replace(/^#/, '');\n\n if (normalized.length === 0) {\n throw new Error('Issue identifier cannot be empty');\n }\n if (/[/\\\\]/.test(normalized) || normalized === '.' || normalized === '..') {\n throw new Error(`Invalid issue identifier: '${issueNumber}'`);\n }\n\n return normalized;\n}\n\n/**\n * Resolve every path of a single issue under the global storage.\n *\n * This is the only place allowed to know the layout of an issue directory:\n * call sites ask for the artifact they need instead of joining names by hand,\n * so renaming or adding an artifact stays a one-file change.\n *\n * Pure and synchronous: nothing here creates directories or touches the\n * filesystem.\n */\nexport function getIssuePaths(\n projectId: string,\n issueNumber: string | number,\n options: GetGlobalRootOptions = {},\n): IssuePaths {\n return getIssuePathsAt(getProjectDir(projectId, options), issueNumber);\n}\n\n/** Resolve one issue below an already selected project storage directory. */\nexport function getIssuePathsAt(projectDir: string, issueNumber: string | number): IssuePaths {\n return resolveIssueArtifactPaths(projectDir, issueNumber);\n}\n"],"mappings":"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","names":["join","join","join","join"]}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
getGlobalRoot
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-MAJ6QDRL.js";
|
|
5
5
|
import {
|
|
6
6
|
getProjectRoot
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-KUA6KBBD.js";
|
|
8
8
|
import {
|
|
9
9
|
writeFileAtomic
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-PFWHHYEO.js";
|
|
11
11
|
|
|
12
12
|
// src/agents/permissions.ts
|
|
13
|
-
import { mkdir, readFile } from "fs/promises";
|
|
14
|
-
import { homedir } from "os";
|
|
15
|
-
import { dirname, join } from "path";
|
|
13
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
16
|
var MANAGED_MARKER = "issue-flow:managed";
|
|
17
17
|
function managedEntries(root) {
|
|
18
18
|
const glob = `${root.replace(/\\/g, "/")}/**`;
|
|
@@ -74,4 +74,4 @@ export {
|
|
|
74
74
|
ensureCursorStorageGrant,
|
|
75
75
|
cursorStorageGranted
|
|
76
76
|
};
|
|
77
|
-
//# sourceMappingURL=chunk-
|
|
77
|
+
//# sourceMappingURL=chunk-MML2G7IC.js.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
taskPlanSchema
|
|
4
|
+
} from "./chunk-XNK5UN2D.js";
|
|
5
|
+
|
|
6
|
+
// src/core/task-plan.ts
|
|
7
|
+
function executionContext(plan) {
|
|
8
|
+
const ready = eligibleStories(plan.userStories);
|
|
9
|
+
const active = plan.lastReviewFindings ? void 0 : ready[0];
|
|
10
|
+
return {
|
|
11
|
+
objective: plan.description,
|
|
12
|
+
branchName: plan.branchName,
|
|
13
|
+
noBranch: plan.noBranch ?? false,
|
|
14
|
+
counts: {
|
|
15
|
+
total: plan.userStories.length,
|
|
16
|
+
passed: plan.userStories.filter((s) => s.passes).length
|
|
17
|
+
},
|
|
18
|
+
remainingStoryIds: plan.userStories.filter((s) => !s.passes).map((s) => s.id),
|
|
19
|
+
activeStory: active ? {
|
|
20
|
+
id: active.id,
|
|
21
|
+
title: active.title,
|
|
22
|
+
description: active.description,
|
|
23
|
+
acceptanceCriteria: active.acceptanceCriteria,
|
|
24
|
+
notes: active.notes,
|
|
25
|
+
dependencies: (active.dependencies ?? []).map((id) => ({
|
|
26
|
+
id,
|
|
27
|
+
passes: plan.userStories.find((story) => story.id === id)?.passes ?? false
|
|
28
|
+
}))
|
|
29
|
+
} : null,
|
|
30
|
+
lastReviewFindings: plan.lastReviewFindings ?? null,
|
|
31
|
+
lastError: plan.lastError,
|
|
32
|
+
correctionCycle: plan.correctionCycle,
|
|
33
|
+
maxCorrectionCycles: plan.maxCorrectionCycles
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function validateStoryDependencies(stories) {
|
|
37
|
+
const errors = [];
|
|
38
|
+
const byId = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const [index, story] of stories.entries()) {
|
|
40
|
+
if (byId.has(story.id)) {
|
|
41
|
+
errors.push({
|
|
42
|
+
code: "duplicate_story",
|
|
43
|
+
path: `userStories.${index}.id`,
|
|
44
|
+
message: `Duplicate story ID: ${story.id}`
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
byId.set(story.id, story);
|
|
48
|
+
for (const id of story.dependencies ?? []) {
|
|
49
|
+
if (id === story.id || !stories.some((candidate) => candidate.id === id)) {
|
|
50
|
+
errors.push({
|
|
51
|
+
code: id === story.id ? "self_dependency" : "missing_dependency",
|
|
52
|
+
path: `userStories.${index}.dependencies`,
|
|
53
|
+
message: `${story.id} depends on ${id}`
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const visited = /* @__PURE__ */ new Set();
|
|
59
|
+
const active = /* @__PURE__ */ new Set();
|
|
60
|
+
function visit(id, chain) {
|
|
61
|
+
if (active.has(id)) {
|
|
62
|
+
errors.push({
|
|
63
|
+
code: "dependency_cycle",
|
|
64
|
+
path: "userStories",
|
|
65
|
+
message: `Dependency cycle: ${[...chain, id].join(" -> ")}`
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (visited.has(id) || !byId.has(id)) return;
|
|
70
|
+
active.add(id);
|
|
71
|
+
for (const dependency of byId.get(id)?.dependencies ?? []) visit(dependency, [...chain, id]);
|
|
72
|
+
active.delete(id);
|
|
73
|
+
visited.add(id);
|
|
74
|
+
}
|
|
75
|
+
for (const id of byId.keys()) visit(id, []);
|
|
76
|
+
return errors;
|
|
77
|
+
}
|
|
78
|
+
function eligibleStories(stories) {
|
|
79
|
+
const passed = new Set(stories.filter((story) => story.passes).map((story) => story.id));
|
|
80
|
+
return stories.filter((story) => !story.passes && (story.dependencies ?? []).every((id) => passed.has(id))).sort((a, b) => a.priority - b.priority);
|
|
81
|
+
}
|
|
82
|
+
function inspectTaskPlan(value) {
|
|
83
|
+
const parsed = taskPlanSchema.safeParse(value);
|
|
84
|
+
if (!parsed.success) {
|
|
85
|
+
return {
|
|
86
|
+
schemaVersion: 1,
|
|
87
|
+
ok: false,
|
|
88
|
+
data: null,
|
|
89
|
+
errors: parsed.error.issues.map((issue) => ({
|
|
90
|
+
code: "schema",
|
|
91
|
+
path: issue.path.join("."),
|
|
92
|
+
message: issue.message
|
|
93
|
+
}))
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const plan = parsed.data;
|
|
97
|
+
const errors = validateStoryDependencies(plan.userStories);
|
|
98
|
+
if (errors.length) return { schemaVersion: 1, ok: false, data: null, errors };
|
|
99
|
+
const ready = eligibleStories(plan.userStories);
|
|
100
|
+
const correctionRequired = Boolean(plan.lastReviewFindings);
|
|
101
|
+
const next = correctionRequired ? void 0 : ready[0];
|
|
102
|
+
return {
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
ok: true,
|
|
105
|
+
data: {
|
|
106
|
+
counts: {
|
|
107
|
+
total: plan.userStories.length,
|
|
108
|
+
passed: plan.userStories.filter((story) => story.passes).length,
|
|
109
|
+
ready: ready.length
|
|
110
|
+
},
|
|
111
|
+
readyStoryIds: ready.map((story) => story.id),
|
|
112
|
+
blockedStories: plan.userStories.filter((story) => !story.passes && !ready.includes(story)).map((story) => ({
|
|
113
|
+
id: story.id,
|
|
114
|
+
dependencies: (story.dependencies ?? []).filter(
|
|
115
|
+
(id) => !plan.userStories.find((candidate) => candidate.id === id)?.passes
|
|
116
|
+
)
|
|
117
|
+
})),
|
|
118
|
+
correctionRequired,
|
|
119
|
+
executionComplete: plan.userStories.length > 0 && plan.userStories.every((story) => story.passes) && !correctionRequired,
|
|
120
|
+
nextStory: next ? {
|
|
121
|
+
id: next.id,
|
|
122
|
+
title: next.title,
|
|
123
|
+
description: next.description,
|
|
124
|
+
acceptanceCriteria: next.acceptanceCriteria
|
|
125
|
+
} : null
|
|
126
|
+
},
|
|
127
|
+
errors: []
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export {
|
|
132
|
+
executionContext,
|
|
133
|
+
eligibleStories,
|
|
134
|
+
inspectTaskPlan
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=chunk-NP47HRX2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/task-plan.ts"],"sourcesContent":["import { taskPlanSchema } from '../schemas.js';\nimport type { TaskPlan, UserStory } from '../types.js';\n\n/** Facts needed by the current iteration; never a replacement for the mutable plan. */\nexport function executionContext(plan: TaskPlan) {\n const ready = eligibleStories(plan.userStories);\n const active = plan.lastReviewFindings ? undefined : ready[0];\n return {\n objective: plan.description,\n branchName: plan.branchName,\n noBranch: plan.noBranch ?? false,\n counts: {\n total: plan.userStories.length,\n passed: plan.userStories.filter((s) => s.passes).length,\n },\n remainingStoryIds: plan.userStories.filter((s) => !s.passes).map((s) => s.id),\n activeStory: active\n ? {\n id: active.id,\n title: active.title,\n description: active.description,\n acceptanceCriteria: active.acceptanceCriteria,\n notes: active.notes,\n dependencies: (active.dependencies ?? []).map((id) => ({\n id,\n passes: plan.userStories.find((story) => story.id === id)?.passes ?? false,\n })),\n }\n : null,\n lastReviewFindings: plan.lastReviewFindings ?? null,\n lastError: plan.lastError,\n correctionCycle: plan.correctionCycle,\n maxCorrectionCycles: plan.maxCorrectionCycles,\n };\n}\n\nexport interface ArtifactError {\n code: string;\n path: string;\n message: string;\n}\n\n/** Structural dependencies are deliberately never inferred from story prose. */\nexport function validateStoryDependencies(stories: readonly UserStory[]): ArtifactError[] {\n const errors: ArtifactError[] = [];\n const byId = new Map<string, UserStory>();\n for (const [index, story] of stories.entries()) {\n if (byId.has(story.id)) {\n errors.push({\n code: 'duplicate_story',\n path: `userStories.${index}.id`,\n message: `Duplicate story ID: ${story.id}`,\n });\n }\n byId.set(story.id, story);\n for (const id of story.dependencies ?? []) {\n if (id === story.id || !stories.some((candidate) => candidate.id === id)) {\n errors.push({\n code: id === story.id ? 'self_dependency' : 'missing_dependency',\n path: `userStories.${index}.dependencies`,\n message: `${story.id} depends on ${id}`,\n });\n }\n }\n }\n const visited = new Set<string>();\n const active = new Set<string>();\n function visit(id: string, chain: string[]): void {\n if (active.has(id)) {\n errors.push({\n code: 'dependency_cycle',\n path: 'userStories',\n message: `Dependency cycle: ${[...chain, id].join(' -> ')}`,\n });\n return;\n }\n if (visited.has(id) || !byId.has(id)) return;\n active.add(id);\n for (const dependency of byId.get(id)?.dependencies ?? []) visit(dependency, [...chain, id]);\n active.delete(id);\n visited.add(id);\n }\n for (const id of byId.keys()) visit(id, []);\n return errors;\n}\n\nexport function eligibleStories(stories: readonly UserStory[]): UserStory[] {\n const passed = new Set(stories.filter((story) => story.passes).map((story) => story.id));\n return stories\n .filter((story) => !story.passes && (story.dependencies ?? []).every((id) => passed.has(id)))\n .sort((a, b) => a.priority - b.priority);\n}\n\n/** A bounded projection for inspection, never a replacement for the source plan. */\nexport function inspectTaskPlan(value: unknown) {\n const parsed = taskPlanSchema.safeParse(value);\n if (!parsed.success) {\n return {\n schemaVersion: 1 as const,\n ok: false as const,\n data: null,\n errors: parsed.error.issues.map((issue) => ({\n code: 'schema',\n path: issue.path.join('.'),\n message: issue.message,\n })),\n };\n }\n const plan = parsed.data;\n const errors = validateStoryDependencies(plan.userStories);\n if (errors.length) return { schemaVersion: 1 as const, ok: false as const, data: null, errors };\n const ready = eligibleStories(plan.userStories);\n const correctionRequired = Boolean(plan.lastReviewFindings);\n const next = correctionRequired ? undefined : ready[0];\n return {\n schemaVersion: 1 as const,\n ok: true as const,\n data: {\n counts: {\n total: plan.userStories.length,\n passed: plan.userStories.filter((story) => story.passes).length,\n ready: ready.length,\n },\n readyStoryIds: ready.map((story) => story.id),\n blockedStories: plan.userStories\n .filter((story) => !story.passes && !ready.includes(story))\n .map((story) => ({\n id: story.id,\n dependencies: (story.dependencies ?? []).filter(\n (id) => !plan.userStories.find((candidate) => candidate.id === id)?.passes,\n ),\n })),\n correctionRequired,\n executionComplete:\n plan.userStories.length > 0 &&\n plan.userStories.every((story) => story.passes) &&\n !correctionRequired,\n nextStory: next\n ? {\n id: next.id,\n title: next.title,\n description: next.description,\n acceptanceCriteria: next.acceptanceCriteria,\n }\n : null,\n },\n errors: [],\n };\n}\n"],"mappings":";;;;;;AAIO,SAAS,iBAAiB,MAAgB;AAC/C,QAAM,QAAQ,gBAAgB,KAAK,WAAW;AAC9C,QAAM,SAAS,KAAK,qBAAqB,SAAY,MAAM,CAAC;AAC5D,SAAO;AAAA,IACL,WAAW,KAAK;AAAA,IAChB,YAAY,KAAK;AAAA,IACjB,UAAU,KAAK,YAAY;AAAA,IAC3B,QAAQ;AAAA,MACN,OAAO,KAAK,YAAY;AAAA,MACxB,QAAQ,KAAK,YAAY,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE;AAAA,IACnD;AAAA,IACA,mBAAmB,KAAK,YAAY,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;AAAA,IAC5E,aAAa,SACT;AAAA,MACE,IAAI,OAAO;AAAA,MACX,OAAO,OAAO;AAAA,MACd,aAAa,OAAO;AAAA,MACpB,oBAAoB,OAAO;AAAA,MAC3B,OAAO,OAAO;AAAA,MACd,eAAe,OAAO,gBAAgB,CAAC,GAAG,IAAI,CAAC,QAAQ;AAAA,QACrD;AAAA,QACA,QAAQ,KAAK,YAAY,KAAK,CAAC,UAAU,MAAM,OAAO,EAAE,GAAG,UAAU;AAAA,MACvE,EAAE;AAAA,IACJ,IACA;AAAA,IACJ,oBAAoB,KAAK,sBAAsB;AAAA,IAC/C,WAAW,KAAK;AAAA,IAChB,iBAAiB,KAAK;AAAA,IACtB,qBAAqB,KAAK;AAAA,EAC5B;AACF;AASO,SAAS,0BAA0B,SAAgD;AACxF,QAAM,SAA0B,CAAC;AACjC,QAAM,OAAO,oBAAI,IAAuB;AACxC,aAAW,CAAC,OAAO,KAAK,KAAK,QAAQ,QAAQ,GAAG;AAC9C,QAAI,KAAK,IAAI,MAAM,EAAE,GAAG;AACtB,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,MAAM,eAAe,KAAK;AAAA,QAC1B,SAAS,uBAAuB,MAAM,EAAE;AAAA,MAC1C,CAAC;AAAA,IACH;AACA,SAAK,IAAI,MAAM,IAAI,KAAK;AACxB,eAAW,MAAM,MAAM,gBAAgB,CAAC,GAAG;AACzC,UAAI,OAAO,MAAM,MAAM,CAAC,QAAQ,KAAK,CAAC,cAAc,UAAU,OAAO,EAAE,GAAG;AACxE,eAAO,KAAK;AAAA,UACV,MAAM,OAAO,MAAM,KAAK,oBAAoB;AAAA,UAC5C,MAAM,eAAe,KAAK;AAAA,UAC1B,SAAS,GAAG,MAAM,EAAE,eAAe,EAAE;AAAA,QACvC,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACA,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,SAAS,oBAAI,IAAY;AAC/B,WAAS,MAAM,IAAY,OAAuB;AAChD,QAAI,OAAO,IAAI,EAAE,GAAG;AAClB,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN,MAAM;AAAA,QACN,SAAS,qBAAqB,CAAC,GAAG,OAAO,EAAE,EAAE,KAAK,MAAM,CAAC;AAAA,MAC3D,CAAC;AACD;AAAA,IACF;AACA,QAAI,QAAQ,IAAI,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE,EAAG;AACtC,WAAO,IAAI,EAAE;AACb,eAAW,cAAc,KAAK,IAAI,EAAE,GAAG,gBAAgB,CAAC,EAAG,OAAM,YAAY,CAAC,GAAG,OAAO,EAAE,CAAC;AAC3F,WAAO,OAAO,EAAE;AAChB,YAAQ,IAAI,EAAE;AAAA,EAChB;AACA,aAAW,MAAM,KAAK,KAAK,EAAG,OAAM,IAAI,CAAC,CAAC;AAC1C,SAAO;AACT;AAEO,SAAS,gBAAgB,SAA4C;AAC1E,QAAM,SAAS,IAAI,IAAI,QAAQ,OAAO,CAAC,UAAU,MAAM,MAAM,EAAE,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AACvF,SAAO,QACJ,OAAO,CAAC,UAAU,CAAC,MAAM,WAAW,MAAM,gBAAgB,CAAC,GAAG,MAAM,CAAC,OAAO,OAAO,IAAI,EAAE,CAAC,CAAC,EAC3F,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAC3C;AAGO,SAAS,gBAAgB,OAAgB;AAC9C,QAAM,SAAS,eAAe,UAAU,KAAK;AAC7C,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO;AAAA,MACL,eAAe;AAAA,MACf,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,QAAQ,OAAO,MAAM,OAAO,IAAI,CAAC,WAAW;AAAA,QAC1C,MAAM;AAAA,QACN,MAAM,MAAM,KAAK,KAAK,GAAG;AAAA,QACzB,SAAS,MAAM;AAAA,MACjB,EAAE;AAAA,IACJ;AAAA,EACF;AACA,QAAM,OAAO,OAAO;AACpB,QAAM,SAAS,0BAA0B,KAAK,WAAW;AACzD,MAAI,OAAO,OAAQ,QAAO,EAAE,eAAe,GAAY,IAAI,OAAgB,MAAM,MAAM,OAAO;AAC9F,QAAM,QAAQ,gBAAgB,KAAK,WAAW;AAC9C,QAAM,qBAAqB,QAAQ,KAAK,kBAAkB;AAC1D,QAAM,OAAO,qBAAqB,SAAY,MAAM,CAAC;AACrD,SAAO;AAAA,IACL,eAAe;AAAA,IACf,IAAI;AAAA,IACJ,MAAM;AAAA,MACJ,QAAQ;AAAA,QACN,OAAO,KAAK,YAAY;AAAA,QACxB,QAAQ,KAAK,YAAY,OAAO,CAAC,UAAU,MAAM,MAAM,EAAE;AAAA,QACzD,OAAO,MAAM;AAAA,MACf;AAAA,MACA,eAAe,MAAM,IAAI,CAAC,UAAU,MAAM,EAAE;AAAA,MAC5C,gBAAgB,KAAK,YAClB,OAAO,CAAC,UAAU,CAAC,MAAM,UAAU,CAAC,MAAM,SAAS,KAAK,CAAC,EACzD,IAAI,CAAC,WAAW;AAAA,QACf,IAAI,MAAM;AAAA,QACV,eAAe,MAAM,gBAAgB,CAAC,GAAG;AAAA,UACvC,CAAC,OAAO,CAAC,KAAK,YAAY,KAAK,CAAC,cAAc,UAAU,OAAO,EAAE,GAAG;AAAA,QACtE;AAAA,MACF,EAAE;AAAA,MACJ;AAAA,MACA,mBACE,KAAK,YAAY,SAAS,KAC1B,KAAK,YAAY,MAAM,CAAC,UAAU,MAAM,MAAM,KAC9C,CAAC;AAAA,MACH,WAAW,OACP;AAAA,QACE,IAAI,KAAK;AAAA,QACT,OAAO,KAAK;AAAA,QACZ,aAAa,KAAK;AAAA,QAClB,oBAAoB,KAAK;AAAA,MAC3B,IACA;AAAA,IACN;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AACF;","names":[]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/utils/fs.ts
|
|
4
|
-
import * as fsp from "fs/promises";
|
|
5
|
-
import { dirname } from "path";
|
|
4
|
+
import * as fsp from "node:fs/promises";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
6
|
async function writeFileAtomic(path, content, fs = fsp) {
|
|
7
7
|
await fs.mkdir(dirname(path), { recursive: true });
|
|
8
8
|
const tmpFile = `${path}.tmp`;
|
|
@@ -22,4 +22,4 @@ async function writeFileAtomic(path, content, fs = fsp) {
|
|
|
22
22
|
export {
|
|
23
23
|
writeFileAtomic
|
|
24
24
|
};
|
|
25
|
-
//# sourceMappingURL=chunk-
|
|
25
|
+
//# sourceMappingURL=chunk-PFWHHYEO.js.map
|