@storybloq/storybloq 1.4.3 → 1.4.4
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 +2 -0
- package/dist/cli.js +52 -47
- package/dist/index.d.ts +62 -53
- package/dist/index.js +22 -18
- package/dist/mcp.js +48 -44
- package/package.json +1 -1
- package/src/skill/SKILL.md +8 -9
package/README.md
CHANGED
|
@@ -355,6 +355,8 @@ Full type definitions ship with the package (`exports.types`).
|
|
|
355
355
|
|
|
356
356
|
Each record is its own file. IDs are sequential within type (`T-001`, `T-002`, ...). Relationships are single-canonical-owner: a ticket's `blockedBy` field points at blocker tickets, and the reverse (who-blocks-me) is derived by scanning.
|
|
357
357
|
|
|
358
|
+
Create operations are safe to run in parallel. ID assignment and the create write happen together under a project lock, so concurrent creators are serialized and each receives a distinct sequential ID. A create can never silently overwrite an existing record; under heavy simultaneous contention a creator fails loudly with an error rather than colliding.
|
|
359
|
+
|
|
358
360
|
Ticket and issue records preserve unknown JSON fields. Use `storybloq ticket meta` and `storybloq issue meta` to read or mutate those custom passthrough fields without touching core Storybloq fields. Values are JSON, and dot paths address nested objects, for example `storybloq ticket meta set T-001 integration.linear '"ABC-123"'`.
|
|
359
361
|
|
|
360
362
|
## Example workflow
|
package/dist/cli.js
CHANGED
|
@@ -991,7 +991,7 @@ import {
|
|
|
991
991
|
mkdir
|
|
992
992
|
} from "fs/promises";
|
|
993
993
|
import { existsSync as existsSync4 } from "fs";
|
|
994
|
-
import { join as join5, resolve as resolve3, relative as relative2, extname as extname2, dirname as dirname2, basename } from "path";
|
|
994
|
+
import { join as join5, resolve as resolve3, relative as relative2, extname as extname2, dirname as dirname2, basename, sep, isAbsolute as isAbsolute2 } from "path";
|
|
995
995
|
import lockfile from "proper-lockfile";
|
|
996
996
|
async function loadProject(root, options) {
|
|
997
997
|
const absRoot = resolve3(root);
|
|
@@ -1625,7 +1625,8 @@ async function guardPath(target, root) {
|
|
|
1625
1625
|
} catch {
|
|
1626
1626
|
resolvedDir = targetDir;
|
|
1627
1627
|
}
|
|
1628
|
-
|
|
1628
|
+
const rel = relative2(resolvedRoot, resolvedDir);
|
|
1629
|
+
if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute2(rel)) {
|
|
1629
1630
|
throw new ProjectLoaderError(
|
|
1630
1631
|
"invalid_input",
|
|
1631
1632
|
`Path ${target} resolves outside project root`
|
|
@@ -1996,6 +1997,7 @@ __export(output_formatter_exports, {
|
|
|
1996
1997
|
EMPTY_SCAFFOLD_HEADING: () => EMPTY_SCAFFOLD_HEADING,
|
|
1997
1998
|
ExitCode: () => ExitCode,
|
|
1998
1999
|
errorEnvelope: () => errorEnvelope,
|
|
2000
|
+
escapeMarkdownDocument: () => escapeMarkdownDocument,
|
|
1999
2001
|
escapeMarkdownInline: () => escapeMarkdownInline,
|
|
2000
2002
|
fencedBlock: () => fencedBlock,
|
|
2001
2003
|
formatBlockedTickets: () => formatBlockedTickets,
|
|
@@ -2056,6 +2058,9 @@ function partialEnvelope(data, warnings) {
|
|
|
2056
2058
|
};
|
|
2057
2059
|
}
|
|
2058
2060
|
function escapeMarkdownInline(text) {
|
|
2061
|
+
return text.replace(/(^|\n)([#\-+*])/g, "$1\\$2").replace(/(^|\n)(\d+)\./g, "$1$2\\.");
|
|
2062
|
+
}
|
|
2063
|
+
function escapeMarkdownDocument(text) {
|
|
2059
2064
|
return text.replace(/\\/g, "\\\\").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/([`*_~\[\]()|])/g, "\\$1").replace(/(^|\n)([#\-+*])/g, "$1\\$2").replace(/(^|\n)(\d+)\./g, "$1$2\\.");
|
|
2060
2065
|
}
|
|
2061
2066
|
function fencedBlock(content, lang) {
|
|
@@ -2812,11 +2817,11 @@ function formatPhaseExport(state, phaseId, format) {
|
|
|
2812
2817
|
);
|
|
2813
2818
|
}
|
|
2814
2819
|
const lines = [];
|
|
2815
|
-
lines.push(`# ${
|
|
2820
|
+
lines.push(`# ${escapeMarkdownDocument(phase.name)} (${phase.id})`);
|
|
2816
2821
|
lines.push("");
|
|
2817
2822
|
lines.push(`Status: ${phaseStatus}`);
|
|
2818
2823
|
if (phase.description) {
|
|
2819
|
-
lines.push(`Description: ${
|
|
2824
|
+
lines.push(`Description: ${escapeMarkdownDocument(phase.description)}`);
|
|
2820
2825
|
}
|
|
2821
2826
|
if (leaves.length > 0) {
|
|
2822
2827
|
lines.push("");
|
|
@@ -2824,28 +2829,28 @@ function formatPhaseExport(state, phaseId, format) {
|
|
|
2824
2829
|
for (const t of leaves) {
|
|
2825
2830
|
const indicator = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
|
|
2826
2831
|
const parentNote = t.parentTicket && umbrellaAncestors.has(t.parentTicket) ? ` (under ${t.parentTicket})` : "";
|
|
2827
|
-
lines.push(`${indicator} ${t.id}: ${
|
|
2832
|
+
lines.push(`${indicator} ${t.id}: ${escapeMarkdownDocument(t.title)}${parentNote}`);
|
|
2828
2833
|
}
|
|
2829
2834
|
}
|
|
2830
2835
|
if (crossPhaseDeps.size > 0) {
|
|
2831
2836
|
lines.push("");
|
|
2832
2837
|
lines.push("## Cross-Phase Dependencies");
|
|
2833
2838
|
for (const [, dep] of crossPhaseDeps) {
|
|
2834
|
-
lines.push(`- ${dep.id}: ${
|
|
2839
|
+
lines.push(`- ${dep.id}: ${escapeMarkdownDocument(dep.title)} [${dep.status}] (${dep.phase ?? "unphased"})`);
|
|
2835
2840
|
}
|
|
2836
2841
|
}
|
|
2837
2842
|
if (relatedIssues.length > 0) {
|
|
2838
2843
|
lines.push("");
|
|
2839
2844
|
lines.push("## Open Issues");
|
|
2840
2845
|
for (const i of relatedIssues) {
|
|
2841
|
-
lines.push(`- ${i.id} [${i.severity}]: ${
|
|
2846
|
+
lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownDocument(i.title)}`);
|
|
2842
2847
|
}
|
|
2843
2848
|
}
|
|
2844
2849
|
if (activeBlockers.length > 0) {
|
|
2845
2850
|
lines.push("");
|
|
2846
2851
|
lines.push("## Active Blockers");
|
|
2847
2852
|
for (const b of activeBlockers) {
|
|
2848
|
-
lines.push(`- ${
|
|
2853
|
+
lines.push(`- ${escapeMarkdownDocument(b.name)}${b.note ? ` \u2014 ${escapeMarkdownDocument(b.note)}` : ""}`);
|
|
2849
2854
|
}
|
|
2850
2855
|
}
|
|
2851
2856
|
return lines.join("\n");
|
|
@@ -2898,7 +2903,7 @@ function formatFullExport(state, format) {
|
|
|
2898
2903
|
);
|
|
2899
2904
|
}
|
|
2900
2905
|
const lines = [];
|
|
2901
|
-
lines.push(`# ${
|
|
2906
|
+
lines.push(`# ${escapeMarkdownDocument(state.config.project)} \u2014 Full Export`);
|
|
2902
2907
|
lines.push("");
|
|
2903
2908
|
lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
|
|
2904
2909
|
lines.push(`Issues: ${state.activeIssueCount} open`);
|
|
@@ -2909,16 +2914,16 @@ function formatFullExport(state, format) {
|
|
|
2909
2914
|
for (const p of phases) {
|
|
2910
2915
|
const indicator = p.status === "complete" ? "[x]" : p.status === "inprogress" ? "[~]" : "[ ]";
|
|
2911
2916
|
lines.push("");
|
|
2912
|
-
lines.push(`### ${indicator} ${
|
|
2917
|
+
lines.push(`### ${indicator} ${escapeMarkdownDocument(p.phase.name)} (${p.phase.id})`);
|
|
2913
2918
|
if (p.phase.description) {
|
|
2914
|
-
lines.push(
|
|
2919
|
+
lines.push(escapeMarkdownDocument(p.phase.description));
|
|
2915
2920
|
}
|
|
2916
2921
|
const tickets = state.phaseTickets(p.phase.id);
|
|
2917
2922
|
if (tickets.length > 0) {
|
|
2918
2923
|
lines.push("");
|
|
2919
2924
|
for (const t of tickets) {
|
|
2920
2925
|
const ti = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
|
|
2921
|
-
lines.push(`${ti} ${t.id}: ${
|
|
2926
|
+
lines.push(`${ti} ${t.id}: ${escapeMarkdownDocument(t.title)}`);
|
|
2922
2927
|
}
|
|
2923
2928
|
}
|
|
2924
2929
|
}
|
|
@@ -2927,7 +2932,7 @@ function formatFullExport(state, format) {
|
|
|
2927
2932
|
lines.push("## Issues");
|
|
2928
2933
|
for (const i of state.issues) {
|
|
2929
2934
|
const resolved = i.status === "resolved" ? " \u2713" : "";
|
|
2930
|
-
lines.push(`- ${i.id} [${i.severity}]: ${
|
|
2935
|
+
lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownDocument(i.title)}${resolved}`);
|
|
2931
2936
|
}
|
|
2932
2937
|
}
|
|
2933
2938
|
const activeNotes = state.notes.filter((n) => n.status === "active");
|
|
@@ -2936,8 +2941,8 @@ function formatFullExport(state, format) {
|
|
|
2936
2941
|
lines.push("## Notes");
|
|
2937
2942
|
for (const n of activeNotes) {
|
|
2938
2943
|
const title = n.title ?? n.id;
|
|
2939
|
-
const tagInfo = n.tags.length > 0 ? ` (${n.tags.join(", ")})` : "";
|
|
2940
|
-
lines.push(`- ${n.id}: ${
|
|
2944
|
+
const tagInfo = n.tags.length > 0 ? ` (${n.tags.map(escapeMarkdownDocument).join(", ")})` : "";
|
|
2945
|
+
lines.push(`- ${n.id}: ${escapeMarkdownDocument(title)}${tagInfo}`);
|
|
2941
2946
|
}
|
|
2942
2947
|
}
|
|
2943
2948
|
const activeLessons = state.lessons.filter((l) => l.status === "active");
|
|
@@ -2946,8 +2951,8 @@ function formatFullExport(state, format) {
|
|
|
2946
2951
|
lines.push("## Lessons");
|
|
2947
2952
|
for (const l of activeLessons) {
|
|
2948
2953
|
const reinforced = l.reinforcements > 0 ? ` (\xD7${l.reinforcements})` : "";
|
|
2949
|
-
const tagInfo = l.tags.length > 0 ? ` [${l.tags.join(", ")}]` : "";
|
|
2950
|
-
lines.push(`- ${l.id}: ${
|
|
2954
|
+
const tagInfo = l.tags.length > 0 ? ` [${l.tags.map(escapeMarkdownDocument).join(", ")}]` : "";
|
|
2955
|
+
lines.push(`- ${l.id}: ${escapeMarkdownDocument(l.title)}${reinforced}${tagInfo}`);
|
|
2951
2956
|
}
|
|
2952
2957
|
}
|
|
2953
2958
|
const blockers = state.roadmap.blockers;
|
|
@@ -2956,7 +2961,7 @@ function formatFullExport(state, format) {
|
|
|
2956
2961
|
lines.push("## Blockers");
|
|
2957
2962
|
for (const b of blockers) {
|
|
2958
2963
|
const cleared = isBlockerCleared(b) ? "[x]" : "[ ]";
|
|
2959
|
-
lines.push(`${cleared} ${
|
|
2964
|
+
lines.push(`${cleared} ${escapeMarkdownDocument(b.name)}${b.note ? ` \u2014 ${escapeMarkdownDocument(b.note)}` : ""}`);
|
|
2960
2965
|
}
|
|
2961
2966
|
}
|
|
2962
2967
|
return lines.join("\n");
|
|
@@ -3521,8 +3526,8 @@ function handleNodeList(ctx) {
|
|
|
3521
3526
|
}
|
|
3522
3527
|
const lines = ["# Nodes", ""];
|
|
3523
3528
|
const header = "| Name | Path | Stack | Role | Health | Depends On |";
|
|
3524
|
-
const
|
|
3525
|
-
lines.push(header,
|
|
3529
|
+
const sep6 = "|------|------|-------|------|--------|------------|";
|
|
3530
|
+
lines.push(header, sep6);
|
|
3526
3531
|
for (const [name, node] of entries) {
|
|
3527
3532
|
const path2 = node.path ?? "";
|
|
3528
3533
|
const stack = node.stack ?? "";
|
|
@@ -3881,7 +3886,7 @@ var init_session_types = __esm({
|
|
|
3881
3886
|
|
|
3882
3887
|
// src/autonomous/session-selector.ts
|
|
3883
3888
|
import { readdirSync, realpathSync as realpathSync4 } from "fs";
|
|
3884
|
-
import { basename as basename2, join as join8, sep } from "path";
|
|
3889
|
+
import { basename as basename2, join as join8, sep as sep2 } from "path";
|
|
3885
3890
|
function canonicalSessionsRoot(root) {
|
|
3886
3891
|
try {
|
|
3887
3892
|
return realpathSync4.native(sessionsRoot(root));
|
|
@@ -3892,7 +3897,7 @@ function canonicalSessionsRoot(root) {
|
|
|
3892
3897
|
function isContainedSessionDir(root, dir) {
|
|
3893
3898
|
const canonRoot = canonicalSessionsRoot(root);
|
|
3894
3899
|
if (canonRoot === null) return false;
|
|
3895
|
-
const rootPrefix = canonRoot.endsWith(
|
|
3900
|
+
const rootPrefix = canonRoot.endsWith(sep2) ? canonRoot : canonRoot + sep2;
|
|
3896
3901
|
let canonDir;
|
|
3897
3902
|
try {
|
|
3898
3903
|
canonDir = realpathSync4.native(dir);
|
|
@@ -5478,9 +5483,9 @@ var init_activation = __esm({
|
|
|
5478
5483
|
|
|
5479
5484
|
// src/autonomous/review-lenses/path-safety.ts
|
|
5480
5485
|
import { realpathSync as realpathSync5 } from "fs";
|
|
5481
|
-
import { join as join12, resolve as resolve6, sep as
|
|
5486
|
+
import { join as join12, resolve as resolve6, sep as sep3 } from "path";
|
|
5482
5487
|
function resolveAndValidate(projectRoot, file) {
|
|
5483
|
-
const resolvedRoot = resolve6(projectRoot) +
|
|
5488
|
+
const resolvedRoot = resolve6(projectRoot) + sep3;
|
|
5484
5489
|
const fullPath = resolve6(join12(projectRoot, file));
|
|
5485
5490
|
if (!fullPath.startsWith(resolvedRoot)) return null;
|
|
5486
5491
|
let realPath;
|
|
@@ -7997,7 +8002,7 @@ import {
|
|
|
7997
8002
|
existsSync as existsSync9,
|
|
7998
8003
|
statSync as statSync3
|
|
7999
8004
|
} from "fs";
|
|
8000
|
-
import { join as join16, dirname as dirname4, resolve as resolve7, sep as
|
|
8005
|
+
import { join as join16, dirname as dirname4, resolve as resolve7, sep as sep4, posix, win32 } from "path";
|
|
8001
8006
|
import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
|
|
8002
8007
|
import { spawnSync } from "child_process";
|
|
8003
8008
|
function assertValidSessionId(sessionId) {
|
|
@@ -8067,13 +8072,13 @@ function assertValidManifestPath(entryPath, label) {
|
|
|
8067
8072
|
function assertNoSymlinkAncestors(targetPath, stopAt) {
|
|
8068
8073
|
const stopReal = resolve7(stopAt);
|
|
8069
8074
|
const target = resolve7(targetPath);
|
|
8070
|
-
if (!target.startsWith(stopReal +
|
|
8075
|
+
if (!target.startsWith(stopReal + sep4) && target !== stopReal) {
|
|
8071
8076
|
throw new Error(
|
|
8072
8077
|
`review-snapshot: assertNoSymlinkAncestors: target ${target} is not under stopAt ${stopReal}`
|
|
8073
8078
|
);
|
|
8074
8079
|
}
|
|
8075
8080
|
const suffix = target === stopReal ? "" : target.slice(stopReal.length + 1);
|
|
8076
|
-
const segments = suffix.length > 0 ? suffix.split(
|
|
8081
|
+
const segments = suffix.length > 0 ? suffix.split(sep4) : [];
|
|
8077
8082
|
let current = stopReal;
|
|
8078
8083
|
for (const segment of segments) {
|
|
8079
8084
|
current = join16(current, segment);
|
|
@@ -8114,14 +8119,14 @@ function deriveCanonicalPaths(projectRoot, sessionId, reviewId) {
|
|
|
8114
8119
|
}
|
|
8115
8120
|
const canonicalSessionsRoot2 = realpathSync6(sessionsDir);
|
|
8116
8121
|
const sessionDir2 = join16(canonicalSessionsRoot2, sessionId);
|
|
8117
|
-
if (!sessionDir2.startsWith(canonicalSessionsRoot2 +
|
|
8122
|
+
if (!sessionDir2.startsWith(canonicalSessionsRoot2 + sep4)) {
|
|
8118
8123
|
throw new Error(
|
|
8119
8124
|
`review-snapshot: sessionDir escapes sessions root: ${sessionDir2}`
|
|
8120
8125
|
);
|
|
8121
8126
|
}
|
|
8122
8127
|
const snapshotParent = join16(sessionDir2, "review-snapshot");
|
|
8123
8128
|
const finalDir = join16(snapshotParent, reviewId);
|
|
8124
|
-
if (!finalDir.startsWith(snapshotParent +
|
|
8129
|
+
if (!finalDir.startsWith(snapshotParent + sep4)) {
|
|
8125
8130
|
throw new Error(
|
|
8126
8131
|
`review-snapshot: finalDir escapes snapshot parent: ${finalDir}`
|
|
8127
8132
|
);
|
|
@@ -8146,7 +8151,7 @@ function readReviewSnapshotManifestWithBytes(projectRoot, sessionId, reviewId) {
|
|
|
8146
8151
|
);
|
|
8147
8152
|
}
|
|
8148
8153
|
const realFinalDir = realpathSync6(paths.finalDir);
|
|
8149
|
-
if (!realFinalDir.startsWith(paths.canonicalSessionsRoot +
|
|
8154
|
+
if (!realFinalDir.startsWith(paths.canonicalSessionsRoot + sep4) && realFinalDir !== paths.canonicalSessionsRoot) {
|
|
8150
8155
|
throw new Error(
|
|
8151
8156
|
`review-snapshot: finalDir escapes sessions root: ${paths.finalDir}`
|
|
8152
8157
|
);
|
|
@@ -8159,7 +8164,7 @@ function readReviewSnapshotManifestWithBytes(projectRoot, sessionId, reviewId) {
|
|
|
8159
8164
|
);
|
|
8160
8165
|
}
|
|
8161
8166
|
const realManifestPath = realpathSync6(manifestPath);
|
|
8162
|
-
if (!realManifestPath.startsWith(realFinalDir +
|
|
8167
|
+
if (!realManifestPath.startsWith(realFinalDir + sep4)) {
|
|
8163
8168
|
throw new Error(
|
|
8164
8169
|
`review-snapshot: manifest.json escapes finalDir: ${manifestPath}`
|
|
8165
8170
|
);
|
|
@@ -8204,7 +8209,7 @@ function readReviewSnapshotManifestWithBytes(projectRoot, sessionId, reviewId) {
|
|
|
8204
8209
|
for (const entry of manifest.files) {
|
|
8205
8210
|
assertValidManifestPath(entry.path, `manifest entry`);
|
|
8206
8211
|
const stored = join16(paths.finalDir, entry.path);
|
|
8207
|
-
if (!stored.startsWith(paths.finalDir +
|
|
8212
|
+
if (!stored.startsWith(paths.finalDir + sep4)) {
|
|
8208
8213
|
throw new Error(
|
|
8209
8214
|
`review-snapshot: manifest entry escapes finalDir: ${entry.path}`
|
|
8210
8215
|
);
|
|
@@ -8290,7 +8295,7 @@ import {
|
|
|
8290
8295
|
readFileSync as readFileSync8,
|
|
8291
8296
|
realpathSync as realpathSync7
|
|
8292
8297
|
} from "fs";
|
|
8293
|
-
import { resolve as resolve8, sep as
|
|
8298
|
+
import { resolve as resolve8, sep as sep5 } from "path";
|
|
8294
8299
|
import { createHash as createHash4 } from "crypto";
|
|
8295
8300
|
function loadSnapshot(ctx) {
|
|
8296
8301
|
let manifest;
|
|
@@ -8413,7 +8418,7 @@ function verifyPathAndLookup(evidence, snapshotDir, byPath) {
|
|
|
8413
8418
|
});
|
|
8414
8419
|
}
|
|
8415
8420
|
const resolved = resolve8(snapshotDir, evidence.file);
|
|
8416
|
-
const snapshotDirWithSep = snapshotDir.endsWith(
|
|
8421
|
+
const snapshotDirWithSep = snapshotDir.endsWith(sep5) ? snapshotDir : snapshotDir + sep5;
|
|
8417
8422
|
if (!resolved.startsWith(snapshotDirWithSep) && resolved !== snapshotDir) {
|
|
8418
8423
|
return failOutcome("invalid_path", {
|
|
8419
8424
|
file: evidence.file,
|
|
@@ -8468,7 +8473,7 @@ function verifyPayloadIntegrity(file, resolved, entry, snapshotDir, snapshotDirR
|
|
|
8468
8473
|
);
|
|
8469
8474
|
}
|
|
8470
8475
|
}
|
|
8471
|
-
const snapshotDirRealWithSep = dirReal.endsWith(
|
|
8476
|
+
const snapshotDirRealWithSep = dirReal.endsWith(sep5) ? dirReal : dirReal + sep5;
|
|
8472
8477
|
let realResolved;
|
|
8473
8478
|
try {
|
|
8474
8479
|
realResolved = realpathSync7(resolved);
|
|
@@ -17571,7 +17576,7 @@ function getInstalledVersion() {
|
|
|
17571
17576
|
}
|
|
17572
17577
|
}
|
|
17573
17578
|
function getRunningVersion() {
|
|
17574
|
-
return "1.4.
|
|
17579
|
+
return "1.4.4";
|
|
17575
17580
|
}
|
|
17576
17581
|
var init_version_check = __esm({
|
|
17577
17582
|
"src/autonomous/version-check.ts"() {
|
|
@@ -19863,7 +19868,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
19863
19868
|
const result = await runMcpReadTool(pinnedRoot, handleStatus);
|
|
19864
19869
|
try {
|
|
19865
19870
|
const { readUpdateCacheSync: readUpdateCacheSync2, refreshUpdateCacheInBackground: refreshUpdateCacheInBackground2 } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
|
|
19866
|
-
const running = "1.4.
|
|
19871
|
+
const running = "1.4.4";
|
|
19867
19872
|
const info = readUpdateCacheSync2(running);
|
|
19868
19873
|
refreshUpdateCacheInBackground2();
|
|
19869
19874
|
if (info?.updateAvailable && result.content[0]?.type === "text") {
|
|
@@ -20075,7 +20080,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
20075
20080
|
);
|
|
20076
20081
|
});
|
|
20077
20082
|
server.registerTool("storybloq_ticket_create", {
|
|
20078
|
-
description: "Create a new ticket",
|
|
20083
|
+
description: "Create a new ticket. ID assignment is serialized under the project lock, so concurrent creates that acquire the lock receive distinct sequential IDs.",
|
|
20079
20084
|
inputSchema: {
|
|
20080
20085
|
title: z12.string().describe("Ticket title"),
|
|
20081
20086
|
type: z12.enum(TICKET_TYPES).describe("Ticket type: task, feature, chore"),
|
|
@@ -20162,7 +20167,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
20162
20167
|
(root, format) => handleTicketMetaUnset(args.id, args.path, format, root)
|
|
20163
20168
|
));
|
|
20164
20169
|
server.registerTool("storybloq_issue_create", {
|
|
20165
|
-
description: "Create a new issue",
|
|
20170
|
+
description: "Create a new issue. ID assignment is serialized under the project lock, so concurrent creates that acquire the lock receive distinct sequential IDs.",
|
|
20166
20171
|
inputSchema: {
|
|
20167
20172
|
title: z12.string().describe("Issue title"),
|
|
20168
20173
|
severity: z12.enum(ISSUE_SEVERITIES).describe("Issue severity: critical, high, medium, low"),
|
|
@@ -20273,7 +20278,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
20273
20278
|
}
|
|
20274
20279
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleNoteGet(args.id, ctx)));
|
|
20275
20280
|
server.registerTool("storybloq_note_create", {
|
|
20276
|
-
description: "Create a new note",
|
|
20281
|
+
description: "Create a new note. ID assignment is serialized under the project lock, so concurrent creates that acquire the lock receive distinct sequential IDs.",
|
|
20277
20282
|
inputSchema: {
|
|
20278
20283
|
content: z12.string().describe("Note content"),
|
|
20279
20284
|
title: z12.string().optional().describe("Note title"),
|
|
@@ -20337,7 +20342,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
20337
20342
|
inputSchema: {}
|
|
20338
20343
|
}, () => runMcpReadTool(pinnedRoot, (ctx) => handleLessonDigest(ctx)));
|
|
20339
20344
|
server.registerTool("storybloq_lesson_create", {
|
|
20340
|
-
description: "Create a new lesson",
|
|
20345
|
+
description: "Create a new lesson. ID assignment is serialized under the project lock, so concurrent creates that acquire the lock receive distinct sequential IDs.",
|
|
20341
20346
|
inputSchema: {
|
|
20342
20347
|
title: z12.string().describe("Lesson title \u2014 concise lesson name"),
|
|
20343
20348
|
content: z12.string().describe("The actionable rule (1-3 sentences)"),
|
|
@@ -21281,7 +21286,7 @@ var init_inbox_watcher = __esm({
|
|
|
21281
21286
|
// src/mcp/index.ts
|
|
21282
21287
|
var mcp_exports = {};
|
|
21283
21288
|
import { realpathSync as realpathSync8, existsSync as existsSync19 } from "fs";
|
|
21284
|
-
import { resolve as resolve13, join as join44, isAbsolute as
|
|
21289
|
+
import { resolve as resolve13, join as join44, isAbsolute as isAbsolute3 } from "path";
|
|
21285
21290
|
import { z as z14 } from "zod";
|
|
21286
21291
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21287
21292
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -21289,7 +21294,7 @@ function tryDiscoverRoot() {
|
|
|
21289
21294
|
const envRoot = process.env[ENV_VAR2] ?? process.env[LEGACY_ENV_VAR2];
|
|
21290
21295
|
const envName = process.env[ENV_VAR2] ? ENV_VAR2 : LEGACY_ENV_VAR2;
|
|
21291
21296
|
if (envRoot) {
|
|
21292
|
-
if (!
|
|
21297
|
+
if (!isAbsolute3(envRoot)) {
|
|
21293
21298
|
process.stderr.write(`Warning: ${envName} must be an absolute path, got: ${envRoot}
|
|
21294
21299
|
`);
|
|
21295
21300
|
return null;
|
|
@@ -21506,7 +21511,7 @@ var init_mcp = __esm({
|
|
|
21506
21511
|
ENV_VAR2 = "STORYBLOQ_PROJECT_ROOT";
|
|
21507
21512
|
LEGACY_ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
|
|
21508
21513
|
CONFIG_PATH2 = ".story/config.json";
|
|
21509
|
-
version = "1.4.
|
|
21514
|
+
version = "1.4.4";
|
|
21510
21515
|
main().catch((err) => {
|
|
21511
21516
|
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
21512
21517
|
`);
|
|
@@ -23112,7 +23117,7 @@ var init_codex_review = __esm({
|
|
|
23112
23117
|
|
|
23113
23118
|
// src/core/symlink-write.ts
|
|
23114
23119
|
import { lstat as lstat3, mkdir as mkdir7, readlink, realpath as realpath2, rename as rename3, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
|
|
23115
|
-
import { dirname as dirname8, isAbsolute as
|
|
23120
|
+
import { dirname as dirname8, isAbsolute as isAbsolute4, resolve as resolve15 } from "path";
|
|
23116
23121
|
async function resolveSymlinkTarget(path2) {
|
|
23117
23122
|
try {
|
|
23118
23123
|
return await realpath2(path2);
|
|
@@ -23121,7 +23126,7 @@ async function resolveSymlinkTarget(path2) {
|
|
|
23121
23126
|
let cur = path2;
|
|
23122
23127
|
for (let depth = 0; depth < MAX_SYMLINK_DEPTH; depth++) {
|
|
23123
23128
|
const link = await readlink(cur);
|
|
23124
|
-
const next =
|
|
23129
|
+
const next = isAbsolute4(link) ? link : resolve15(dirname8(cur), link);
|
|
23125
23130
|
let st;
|
|
23126
23131
|
try {
|
|
23127
23132
|
st = await lstat3(next);
|
|
@@ -28943,7 +28948,7 @@ async function runCli() {
|
|
|
28943
28948
|
registerNodeCommand: registerNodeCommand2,
|
|
28944
28949
|
registerFeedbackCommand: registerFeedbackCommand2
|
|
28945
28950
|
} = await Promise.resolve().then(() => (init_register(), register_exports));
|
|
28946
|
-
const version2 = "1.4.
|
|
28951
|
+
const version2 = "1.4.4";
|
|
28947
28952
|
const { preCommandHousekeeping: preCommandHousekeeping2 } = await Promise.resolve().then(() => (init_housekeeping(), housekeeping_exports));
|
|
28948
28953
|
await preCommandHousekeeping2(version2);
|
|
28949
28954
|
class HandledError extends Error {
|
package/dist/index.d.ts
CHANGED
|
@@ -1503,49 +1503,16 @@ declare const SnapshotV1Schema: z.ZodObject<{
|
|
|
1503
1503
|
file: z.ZodString;
|
|
1504
1504
|
message: z.ZodString;
|
|
1505
1505
|
}, "strip", z.ZodTypeAny, {
|
|
1506
|
-
type: string;
|
|
1507
1506
|
message: string;
|
|
1507
|
+
type: string;
|
|
1508
1508
|
file: string;
|
|
1509
1509
|
}, {
|
|
1510
|
-
type: string;
|
|
1511
1510
|
message: string;
|
|
1511
|
+
type: string;
|
|
1512
1512
|
file: string;
|
|
1513
1513
|
}>, "many">>;
|
|
1514
1514
|
gitHead: z.ZodOptional<z.ZodString>;
|
|
1515
1515
|
}, "strip", z.ZodTypeAny, {
|
|
1516
|
-
version: 1;
|
|
1517
|
-
config: {
|
|
1518
|
-
version: number;
|
|
1519
|
-
type: string;
|
|
1520
|
-
language: string;
|
|
1521
|
-
project: string;
|
|
1522
|
-
features: {
|
|
1523
|
-
issues: boolean;
|
|
1524
|
-
tickets: boolean;
|
|
1525
|
-
handovers: boolean;
|
|
1526
|
-
roadmap: boolean;
|
|
1527
|
-
reviews: boolean;
|
|
1528
|
-
} & {
|
|
1529
|
-
[k: string]: unknown;
|
|
1530
|
-
};
|
|
1531
|
-
orchestrator?: string | undefined;
|
|
1532
|
-
nodes?: Record<string, unknown> | undefined;
|
|
1533
|
-
federation?: Record<string, unknown> | undefined;
|
|
1534
|
-
schemaVersion?: number | undefined;
|
|
1535
|
-
recipe?: string | undefined;
|
|
1536
|
-
recipeOverrides?: {
|
|
1537
|
-
maxTicketsPerSession?: number | undefined;
|
|
1538
|
-
compactThreshold?: string | undefined;
|
|
1539
|
-
reviewBackends?: string[] | undefined;
|
|
1540
|
-
codexReviewBackends?: string[] | undefined;
|
|
1541
|
-
handoverInterval?: number | undefined;
|
|
1542
|
-
stages?: Record<string, Record<string, unknown>> | undefined;
|
|
1543
|
-
branchStrategy?: "none" | "per-ticket" | undefined;
|
|
1544
|
-
maxParallelAgents?: number | undefined;
|
|
1545
|
-
} | undefined;
|
|
1546
|
-
} & {
|
|
1547
|
-
[k: string]: unknown;
|
|
1548
|
-
};
|
|
1549
1516
|
issues: z.objectOutputType<{
|
|
1550
1517
|
id: z.ZodString;
|
|
1551
1518
|
title: z.ZodString;
|
|
@@ -1583,8 +1550,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
|
|
|
1583
1550
|
crossNodeBlockedBy: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
1584
1551
|
}, z.ZodTypeAny, "passthrough">[];
|
|
1585
1552
|
roadmap: {
|
|
1586
|
-
date: string;
|
|
1587
1553
|
title: string;
|
|
1554
|
+
date: string;
|
|
1588
1555
|
phases: z.objectOutputType<{
|
|
1589
1556
|
id: z.ZodString;
|
|
1590
1557
|
label: z.ZodString;
|
|
@@ -1602,6 +1569,7 @@ declare const SnapshotV1Schema: z.ZodObject<{
|
|
|
1602
1569
|
} & {
|
|
1603
1570
|
[k: string]: unknown;
|
|
1604
1571
|
};
|
|
1572
|
+
version: 1;
|
|
1605
1573
|
project: string;
|
|
1606
1574
|
notes: z.objectOutputType<{
|
|
1607
1575
|
id: z.ZodString;
|
|
@@ -1626,21 +1594,11 @@ declare const SnapshotV1Schema: z.ZodObject<{
|
|
|
1626
1594
|
supersedes: z.ZodNullable<z.ZodString>;
|
|
1627
1595
|
status: z.ZodEnum<["active", "deprecated", "superseded"]>;
|
|
1628
1596
|
}, z.ZodTypeAny, "passthrough">[];
|
|
1629
|
-
createdAt: string;
|
|
1630
|
-
handoverFilenames: string[];
|
|
1631
|
-
warnings?: {
|
|
1632
|
-
type: string;
|
|
1633
|
-
message: string;
|
|
1634
|
-
file: string;
|
|
1635
|
-
}[] | undefined;
|
|
1636
|
-
gitHead?: string | undefined;
|
|
1637
|
-
}, {
|
|
1638
|
-
version: 1;
|
|
1639
1597
|
config: {
|
|
1640
|
-
version: number;
|
|
1641
1598
|
type: string;
|
|
1642
|
-
|
|
1599
|
+
version: number;
|
|
1643
1600
|
project: string;
|
|
1601
|
+
language: string;
|
|
1644
1602
|
features: {
|
|
1645
1603
|
issues: boolean;
|
|
1646
1604
|
tickets: boolean;
|
|
@@ -1668,6 +1626,15 @@ declare const SnapshotV1Schema: z.ZodObject<{
|
|
|
1668
1626
|
} & {
|
|
1669
1627
|
[k: string]: unknown;
|
|
1670
1628
|
};
|
|
1629
|
+
createdAt: string;
|
|
1630
|
+
handoverFilenames: string[];
|
|
1631
|
+
warnings?: {
|
|
1632
|
+
message: string;
|
|
1633
|
+
type: string;
|
|
1634
|
+
file: string;
|
|
1635
|
+
}[] | undefined;
|
|
1636
|
+
gitHead?: string | undefined;
|
|
1637
|
+
}, {
|
|
1671
1638
|
issues: z.objectInputType<{
|
|
1672
1639
|
id: z.ZodString;
|
|
1673
1640
|
title: z.ZodString;
|
|
@@ -1705,8 +1672,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
|
|
|
1705
1672
|
crossNodeBlockedBy: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
1706
1673
|
}, z.ZodTypeAny, "passthrough">[];
|
|
1707
1674
|
roadmap: {
|
|
1708
|
-
date: string;
|
|
1709
1675
|
title: string;
|
|
1676
|
+
date: string;
|
|
1710
1677
|
phases: z.objectInputType<{
|
|
1711
1678
|
id: z.ZodString;
|
|
1712
1679
|
label: z.ZodString;
|
|
@@ -1724,7 +1691,40 @@ declare const SnapshotV1Schema: z.ZodObject<{
|
|
|
1724
1691
|
} & {
|
|
1725
1692
|
[k: string]: unknown;
|
|
1726
1693
|
};
|
|
1694
|
+
version: 1;
|
|
1727
1695
|
project: string;
|
|
1696
|
+
config: {
|
|
1697
|
+
type: string;
|
|
1698
|
+
version: number;
|
|
1699
|
+
project: string;
|
|
1700
|
+
language: string;
|
|
1701
|
+
features: {
|
|
1702
|
+
issues: boolean;
|
|
1703
|
+
tickets: boolean;
|
|
1704
|
+
handovers: boolean;
|
|
1705
|
+
roadmap: boolean;
|
|
1706
|
+
reviews: boolean;
|
|
1707
|
+
} & {
|
|
1708
|
+
[k: string]: unknown;
|
|
1709
|
+
};
|
|
1710
|
+
orchestrator?: string | undefined;
|
|
1711
|
+
nodes?: Record<string, unknown> | undefined;
|
|
1712
|
+
federation?: Record<string, unknown> | undefined;
|
|
1713
|
+
schemaVersion?: number | undefined;
|
|
1714
|
+
recipe?: string | undefined;
|
|
1715
|
+
recipeOverrides?: {
|
|
1716
|
+
maxTicketsPerSession?: number | undefined;
|
|
1717
|
+
compactThreshold?: string | undefined;
|
|
1718
|
+
reviewBackends?: string[] | undefined;
|
|
1719
|
+
codexReviewBackends?: string[] | undefined;
|
|
1720
|
+
handoverInterval?: number | undefined;
|
|
1721
|
+
stages?: Record<string, Record<string, unknown>> | undefined;
|
|
1722
|
+
branchStrategy?: "none" | "per-ticket" | undefined;
|
|
1723
|
+
maxParallelAgents?: number | undefined;
|
|
1724
|
+
} | undefined;
|
|
1725
|
+
} & {
|
|
1726
|
+
[k: string]: unknown;
|
|
1727
|
+
};
|
|
1728
1728
|
createdAt: string;
|
|
1729
1729
|
notes?: z.objectInputType<{
|
|
1730
1730
|
id: z.ZodString;
|
|
@@ -1750,8 +1750,8 @@ declare const SnapshotV1Schema: z.ZodObject<{
|
|
|
1750
1750
|
status: z.ZodEnum<["active", "deprecated", "superseded"]>;
|
|
1751
1751
|
}, z.ZodTypeAny, "passthrough">[] | undefined;
|
|
1752
1752
|
warnings?: {
|
|
1753
|
-
type: string;
|
|
1754
1753
|
message: string;
|
|
1754
|
+
type: string;
|
|
1755
1755
|
file: string;
|
|
1756
1756
|
}[] | undefined;
|
|
1757
1757
|
handoverFilenames?: string[] | undefined;
|
|
@@ -1964,9 +1964,18 @@ declare function successEnvelope<T>(data: T): SuccessEnvelope<T>;
|
|
|
1964
1964
|
declare function errorEnvelope(code: ErrorCode, message: string): ErrorEnvelope;
|
|
1965
1965
|
declare function partialEnvelope<T>(data: T, warnings: readonly LoadWarning[]): PartialEnvelope<T>;
|
|
1966
1966
|
/**
|
|
1967
|
-
* Escapes
|
|
1968
|
-
*
|
|
1969
|
-
*
|
|
1967
|
+
* Escapes only heading and list markers at the start of a line (unordered and
|
|
1968
|
+
* ordered) so an embedded field cannot start a new Markdown block inside the
|
|
1969
|
+
* formatter's own `md` output.
|
|
1970
|
+
*
|
|
1971
|
+
* This is NOT a Markdown or HTML sanitizer. Inline-structural, HTML-entity, and
|
|
1972
|
+
* backslash escaping were intentionally removed: the only consumers of formatter
|
|
1973
|
+
* output are plain-text sinks (CLI stdout and MCP `text` results to the model),
|
|
1974
|
+
* neither of which decodes entities or renders Markdown, so that escaping never
|
|
1975
|
+
* got decoded and instead leaked as visible noise like `&`, `\(`, `\[`.
|
|
1976
|
+
* Blockquotes (`>`) and inline characters are passed through for plain-text
|
|
1977
|
+
* readability. Do not reintroduce inline/HTML escaping unless a real
|
|
1978
|
+
* Markdown/HTML renderer is added downstream.
|
|
1970
1979
|
*/
|
|
1971
1980
|
declare function escapeMarkdownInline(text: string): string;
|
|
1972
1981
|
/**
|
package/dist/index.js
CHANGED
|
@@ -423,7 +423,7 @@ import {
|
|
|
423
423
|
mkdir
|
|
424
424
|
} from "fs/promises";
|
|
425
425
|
import { existsSync as existsSync2 } from "fs";
|
|
426
|
-
import { join as join2, resolve, relative as relative2, extname as extname2, dirname, basename } from "path";
|
|
426
|
+
import { join as join2, resolve, relative as relative2, extname as extname2, dirname, basename, sep, isAbsolute } from "path";
|
|
427
427
|
import lockfile from "proper-lockfile";
|
|
428
428
|
|
|
429
429
|
// src/models/lesson.ts
|
|
@@ -1256,7 +1256,8 @@ async function guardPath(target, root) {
|
|
|
1256
1256
|
} catch {
|
|
1257
1257
|
resolvedDir = targetDir;
|
|
1258
1258
|
}
|
|
1259
|
-
|
|
1259
|
+
const rel = relative2(resolvedRoot, resolvedDir);
|
|
1260
|
+
if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
|
|
1260
1261
|
throw new ProjectLoaderError(
|
|
1261
1262
|
"invalid_input",
|
|
1262
1263
|
`Path ${target} resolves outside project root`
|
|
@@ -2912,6 +2913,9 @@ function partialEnvelope(data, warnings) {
|
|
|
2912
2913
|
};
|
|
2913
2914
|
}
|
|
2914
2915
|
function escapeMarkdownInline(text) {
|
|
2916
|
+
return text.replace(/(^|\n)([#\-+*])/g, "$1\\$2").replace(/(^|\n)(\d+)\./g, "$1$2\\.");
|
|
2917
|
+
}
|
|
2918
|
+
function escapeMarkdownDocument(text) {
|
|
2915
2919
|
return text.replace(/\\/g, "\\\\").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/([`*_~\[\]()|])/g, "\\$1").replace(/(^|\n)([#\-+*])/g, "$1\\$2").replace(/(^|\n)(\d+)\./g, "$1$2\\.");
|
|
2916
2920
|
}
|
|
2917
2921
|
function fencedBlock(content, lang) {
|
|
@@ -3445,11 +3449,11 @@ function formatPhaseExport(state, phaseId, format) {
|
|
|
3445
3449
|
);
|
|
3446
3450
|
}
|
|
3447
3451
|
const lines = [];
|
|
3448
|
-
lines.push(`# ${
|
|
3452
|
+
lines.push(`# ${escapeMarkdownDocument(phase.name)} (${phase.id})`);
|
|
3449
3453
|
lines.push("");
|
|
3450
3454
|
lines.push(`Status: ${phaseStatus}`);
|
|
3451
3455
|
if (phase.description) {
|
|
3452
|
-
lines.push(`Description: ${
|
|
3456
|
+
lines.push(`Description: ${escapeMarkdownDocument(phase.description)}`);
|
|
3453
3457
|
}
|
|
3454
3458
|
if (leaves.length > 0) {
|
|
3455
3459
|
lines.push("");
|
|
@@ -3457,28 +3461,28 @@ function formatPhaseExport(state, phaseId, format) {
|
|
|
3457
3461
|
for (const t of leaves) {
|
|
3458
3462
|
const indicator = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
|
|
3459
3463
|
const parentNote = t.parentTicket && umbrellaAncestors.has(t.parentTicket) ? ` (under ${t.parentTicket})` : "";
|
|
3460
|
-
lines.push(`${indicator} ${t.id}: ${
|
|
3464
|
+
lines.push(`${indicator} ${t.id}: ${escapeMarkdownDocument(t.title)}${parentNote}`);
|
|
3461
3465
|
}
|
|
3462
3466
|
}
|
|
3463
3467
|
if (crossPhaseDeps.size > 0) {
|
|
3464
3468
|
lines.push("");
|
|
3465
3469
|
lines.push("## Cross-Phase Dependencies");
|
|
3466
3470
|
for (const [, dep] of crossPhaseDeps) {
|
|
3467
|
-
lines.push(`- ${dep.id}: ${
|
|
3471
|
+
lines.push(`- ${dep.id}: ${escapeMarkdownDocument(dep.title)} [${dep.status}] (${dep.phase ?? "unphased"})`);
|
|
3468
3472
|
}
|
|
3469
3473
|
}
|
|
3470
3474
|
if (relatedIssues.length > 0) {
|
|
3471
3475
|
lines.push("");
|
|
3472
3476
|
lines.push("## Open Issues");
|
|
3473
3477
|
for (const i of relatedIssues) {
|
|
3474
|
-
lines.push(`- ${i.id} [${i.severity}]: ${
|
|
3478
|
+
lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownDocument(i.title)}`);
|
|
3475
3479
|
}
|
|
3476
3480
|
}
|
|
3477
3481
|
if (activeBlockers.length > 0) {
|
|
3478
3482
|
lines.push("");
|
|
3479
3483
|
lines.push("## Active Blockers");
|
|
3480
3484
|
for (const b of activeBlockers) {
|
|
3481
|
-
lines.push(`- ${
|
|
3485
|
+
lines.push(`- ${escapeMarkdownDocument(b.name)}${b.note ? ` \u2014 ${escapeMarkdownDocument(b.note)}` : ""}`);
|
|
3482
3486
|
}
|
|
3483
3487
|
}
|
|
3484
3488
|
return lines.join("\n");
|
|
@@ -3531,7 +3535,7 @@ function formatFullExport(state, format) {
|
|
|
3531
3535
|
);
|
|
3532
3536
|
}
|
|
3533
3537
|
const lines = [];
|
|
3534
|
-
lines.push(`# ${
|
|
3538
|
+
lines.push(`# ${escapeMarkdownDocument(state.config.project)} \u2014 Full Export`);
|
|
3535
3539
|
lines.push("");
|
|
3536
3540
|
lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
|
|
3537
3541
|
lines.push(`Issues: ${state.activeIssueCount} open`);
|
|
@@ -3542,16 +3546,16 @@ function formatFullExport(state, format) {
|
|
|
3542
3546
|
for (const p of phases) {
|
|
3543
3547
|
const indicator = p.status === "complete" ? "[x]" : p.status === "inprogress" ? "[~]" : "[ ]";
|
|
3544
3548
|
lines.push("");
|
|
3545
|
-
lines.push(`### ${indicator} ${
|
|
3549
|
+
lines.push(`### ${indicator} ${escapeMarkdownDocument(p.phase.name)} (${p.phase.id})`);
|
|
3546
3550
|
if (p.phase.description) {
|
|
3547
|
-
lines.push(
|
|
3551
|
+
lines.push(escapeMarkdownDocument(p.phase.description));
|
|
3548
3552
|
}
|
|
3549
3553
|
const tickets = state.phaseTickets(p.phase.id);
|
|
3550
3554
|
if (tickets.length > 0) {
|
|
3551
3555
|
lines.push("");
|
|
3552
3556
|
for (const t of tickets) {
|
|
3553
3557
|
const ti = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
|
|
3554
|
-
lines.push(`${ti} ${t.id}: ${
|
|
3558
|
+
lines.push(`${ti} ${t.id}: ${escapeMarkdownDocument(t.title)}`);
|
|
3555
3559
|
}
|
|
3556
3560
|
}
|
|
3557
3561
|
}
|
|
@@ -3560,7 +3564,7 @@ function formatFullExport(state, format) {
|
|
|
3560
3564
|
lines.push("## Issues");
|
|
3561
3565
|
for (const i of state.issues) {
|
|
3562
3566
|
const resolved = i.status === "resolved" ? " \u2713" : "";
|
|
3563
|
-
lines.push(`- ${i.id} [${i.severity}]: ${
|
|
3567
|
+
lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownDocument(i.title)}${resolved}`);
|
|
3564
3568
|
}
|
|
3565
3569
|
}
|
|
3566
3570
|
const activeNotes = state.notes.filter((n) => n.status === "active");
|
|
@@ -3569,8 +3573,8 @@ function formatFullExport(state, format) {
|
|
|
3569
3573
|
lines.push("## Notes");
|
|
3570
3574
|
for (const n of activeNotes) {
|
|
3571
3575
|
const title = n.title ?? n.id;
|
|
3572
|
-
const tagInfo = n.tags.length > 0 ? ` (${n.tags.join(", ")})` : "";
|
|
3573
|
-
lines.push(`- ${n.id}: ${
|
|
3576
|
+
const tagInfo = n.tags.length > 0 ? ` (${n.tags.map(escapeMarkdownDocument).join(", ")})` : "";
|
|
3577
|
+
lines.push(`- ${n.id}: ${escapeMarkdownDocument(title)}${tagInfo}`);
|
|
3574
3578
|
}
|
|
3575
3579
|
}
|
|
3576
3580
|
const activeLessons = state.lessons.filter((l) => l.status === "active");
|
|
@@ -3579,8 +3583,8 @@ function formatFullExport(state, format) {
|
|
|
3579
3583
|
lines.push("## Lessons");
|
|
3580
3584
|
for (const l of activeLessons) {
|
|
3581
3585
|
const reinforced = l.reinforcements > 0 ? ` (\xD7${l.reinforcements})` : "";
|
|
3582
|
-
const tagInfo = l.tags.length > 0 ? ` [${l.tags.join(", ")}]` : "";
|
|
3583
|
-
lines.push(`- ${l.id}: ${
|
|
3586
|
+
const tagInfo = l.tags.length > 0 ? ` [${l.tags.map(escapeMarkdownDocument).join(", ")}]` : "";
|
|
3587
|
+
lines.push(`- ${l.id}: ${escapeMarkdownDocument(l.title)}${reinforced}${tagInfo}`);
|
|
3584
3588
|
}
|
|
3585
3589
|
}
|
|
3586
3590
|
const blockers = state.roadmap.blockers;
|
|
@@ -3589,7 +3593,7 @@ function formatFullExport(state, format) {
|
|
|
3589
3593
|
lines.push("## Blockers");
|
|
3590
3594
|
for (const b of blockers) {
|
|
3591
3595
|
const cleared = isBlockerCleared(b) ? "[x]" : "[ ]";
|
|
3592
|
-
lines.push(`${cleared} ${
|
|
3596
|
+
lines.push(`${cleared} ${escapeMarkdownDocument(b.name)}${b.note ? ` \u2014 ${escapeMarkdownDocument(b.note)}` : ""}`);
|
|
3593
3597
|
}
|
|
3594
3598
|
}
|
|
3595
3599
|
return lines.join("\n");
|
package/dist/mcp.js
CHANGED
|
@@ -933,7 +933,7 @@ import {
|
|
|
933
933
|
mkdir
|
|
934
934
|
} from "fs/promises";
|
|
935
935
|
import { existsSync as existsSync4 } from "fs";
|
|
936
|
-
import { join as join5, resolve as resolve3, relative as relative2, extname as extname2, dirname as dirname2, basename } from "path";
|
|
936
|
+
import { join as join5, resolve as resolve3, relative as relative2, extname as extname2, dirname as dirname2, basename, sep, isAbsolute as isAbsolute2 } from "path";
|
|
937
937
|
import lockfile from "proper-lockfile";
|
|
938
938
|
async function loadProject(root, options) {
|
|
939
939
|
const absRoot = resolve3(root);
|
|
@@ -1567,7 +1567,8 @@ async function guardPath(target, root) {
|
|
|
1567
1567
|
} catch {
|
|
1568
1568
|
resolvedDir = targetDir;
|
|
1569
1569
|
}
|
|
1570
|
-
|
|
1570
|
+
const rel = relative2(resolvedRoot, resolvedDir);
|
|
1571
|
+
if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute2(rel)) {
|
|
1571
1572
|
throw new ProjectLoaderError(
|
|
1572
1573
|
"invalid_input",
|
|
1573
1574
|
`Path ${target} resolves outside project root`
|
|
@@ -1817,6 +1818,9 @@ function errorEnvelope(code, message) {
|
|
|
1817
1818
|
return { version: 1, error: { code, message } };
|
|
1818
1819
|
}
|
|
1819
1820
|
function escapeMarkdownInline(text) {
|
|
1821
|
+
return text.replace(/(^|\n)([#\-+*])/g, "$1\\$2").replace(/(^|\n)(\d+)\./g, "$1$2\\.");
|
|
1822
|
+
}
|
|
1823
|
+
function escapeMarkdownDocument(text) {
|
|
1820
1824
|
return text.replace(/\\/g, "\\\\").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/([`*_~\[\]()|])/g, "\\$1").replace(/(^|\n)([#\-+*])/g, "$1\\$2").replace(/(^|\n)(\d+)\./g, "$1$2\\.");
|
|
1821
1825
|
}
|
|
1822
1826
|
function fencedBlock(content, lang) {
|
|
@@ -2550,11 +2554,11 @@ function formatPhaseExport(state, phaseId, format) {
|
|
|
2550
2554
|
);
|
|
2551
2555
|
}
|
|
2552
2556
|
const lines = [];
|
|
2553
|
-
lines.push(`# ${
|
|
2557
|
+
lines.push(`# ${escapeMarkdownDocument(phase.name)} (${phase.id})`);
|
|
2554
2558
|
lines.push("");
|
|
2555
2559
|
lines.push(`Status: ${phaseStatus}`);
|
|
2556
2560
|
if (phase.description) {
|
|
2557
|
-
lines.push(`Description: ${
|
|
2561
|
+
lines.push(`Description: ${escapeMarkdownDocument(phase.description)}`);
|
|
2558
2562
|
}
|
|
2559
2563
|
if (leaves.length > 0) {
|
|
2560
2564
|
lines.push("");
|
|
@@ -2562,28 +2566,28 @@ function formatPhaseExport(state, phaseId, format) {
|
|
|
2562
2566
|
for (const t of leaves) {
|
|
2563
2567
|
const indicator = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
|
|
2564
2568
|
const parentNote = t.parentTicket && umbrellaAncestors.has(t.parentTicket) ? ` (under ${t.parentTicket})` : "";
|
|
2565
|
-
lines.push(`${indicator} ${t.id}: ${
|
|
2569
|
+
lines.push(`${indicator} ${t.id}: ${escapeMarkdownDocument(t.title)}${parentNote}`);
|
|
2566
2570
|
}
|
|
2567
2571
|
}
|
|
2568
2572
|
if (crossPhaseDeps.size > 0) {
|
|
2569
2573
|
lines.push("");
|
|
2570
2574
|
lines.push("## Cross-Phase Dependencies");
|
|
2571
2575
|
for (const [, dep] of crossPhaseDeps) {
|
|
2572
|
-
lines.push(`- ${dep.id}: ${
|
|
2576
|
+
lines.push(`- ${dep.id}: ${escapeMarkdownDocument(dep.title)} [${dep.status}] (${dep.phase ?? "unphased"})`);
|
|
2573
2577
|
}
|
|
2574
2578
|
}
|
|
2575
2579
|
if (relatedIssues.length > 0) {
|
|
2576
2580
|
lines.push("");
|
|
2577
2581
|
lines.push("## Open Issues");
|
|
2578
2582
|
for (const i of relatedIssues) {
|
|
2579
|
-
lines.push(`- ${i.id} [${i.severity}]: ${
|
|
2583
|
+
lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownDocument(i.title)}`);
|
|
2580
2584
|
}
|
|
2581
2585
|
}
|
|
2582
2586
|
if (activeBlockers.length > 0) {
|
|
2583
2587
|
lines.push("");
|
|
2584
2588
|
lines.push("## Active Blockers");
|
|
2585
2589
|
for (const b of activeBlockers) {
|
|
2586
|
-
lines.push(`- ${
|
|
2590
|
+
lines.push(`- ${escapeMarkdownDocument(b.name)}${b.note ? ` \u2014 ${escapeMarkdownDocument(b.note)}` : ""}`);
|
|
2587
2591
|
}
|
|
2588
2592
|
}
|
|
2589
2593
|
return lines.join("\n");
|
|
@@ -2636,7 +2640,7 @@ function formatFullExport(state, format) {
|
|
|
2636
2640
|
);
|
|
2637
2641
|
}
|
|
2638
2642
|
const lines = [];
|
|
2639
|
-
lines.push(`# ${
|
|
2643
|
+
lines.push(`# ${escapeMarkdownDocument(state.config.project)} \u2014 Full Export`);
|
|
2640
2644
|
lines.push("");
|
|
2641
2645
|
lines.push(`Tickets: ${state.completeLeafTicketCount}/${state.leafTicketCount} complete`);
|
|
2642
2646
|
lines.push(`Issues: ${state.activeIssueCount} open`);
|
|
@@ -2647,16 +2651,16 @@ function formatFullExport(state, format) {
|
|
|
2647
2651
|
for (const p of phases) {
|
|
2648
2652
|
const indicator = p.status === "complete" ? "[x]" : p.status === "inprogress" ? "[~]" : "[ ]";
|
|
2649
2653
|
lines.push("");
|
|
2650
|
-
lines.push(`### ${indicator} ${
|
|
2654
|
+
lines.push(`### ${indicator} ${escapeMarkdownDocument(p.phase.name)} (${p.phase.id})`);
|
|
2651
2655
|
if (p.phase.description) {
|
|
2652
|
-
lines.push(
|
|
2656
|
+
lines.push(escapeMarkdownDocument(p.phase.description));
|
|
2653
2657
|
}
|
|
2654
2658
|
const tickets = state.phaseTickets(p.phase.id);
|
|
2655
2659
|
if (tickets.length > 0) {
|
|
2656
2660
|
lines.push("");
|
|
2657
2661
|
for (const t of tickets) {
|
|
2658
2662
|
const ti = t.status === "complete" ? "[x]" : t.status === "inprogress" ? "[~]" : "[ ]";
|
|
2659
|
-
lines.push(`${ti} ${t.id}: ${
|
|
2663
|
+
lines.push(`${ti} ${t.id}: ${escapeMarkdownDocument(t.title)}`);
|
|
2660
2664
|
}
|
|
2661
2665
|
}
|
|
2662
2666
|
}
|
|
@@ -2665,7 +2669,7 @@ function formatFullExport(state, format) {
|
|
|
2665
2669
|
lines.push("## Issues");
|
|
2666
2670
|
for (const i of state.issues) {
|
|
2667
2671
|
const resolved = i.status === "resolved" ? " \u2713" : "";
|
|
2668
|
-
lines.push(`- ${i.id} [${i.severity}]: ${
|
|
2672
|
+
lines.push(`- ${i.id} [${i.severity}]: ${escapeMarkdownDocument(i.title)}${resolved}`);
|
|
2669
2673
|
}
|
|
2670
2674
|
}
|
|
2671
2675
|
const activeNotes = state.notes.filter((n) => n.status === "active");
|
|
@@ -2674,8 +2678,8 @@ function formatFullExport(state, format) {
|
|
|
2674
2678
|
lines.push("## Notes");
|
|
2675
2679
|
for (const n of activeNotes) {
|
|
2676
2680
|
const title = n.title ?? n.id;
|
|
2677
|
-
const tagInfo = n.tags.length > 0 ? ` (${n.tags.join(", ")})` : "";
|
|
2678
|
-
lines.push(`- ${n.id}: ${
|
|
2681
|
+
const tagInfo = n.tags.length > 0 ? ` (${n.tags.map(escapeMarkdownDocument).join(", ")})` : "";
|
|
2682
|
+
lines.push(`- ${n.id}: ${escapeMarkdownDocument(title)}${tagInfo}`);
|
|
2679
2683
|
}
|
|
2680
2684
|
}
|
|
2681
2685
|
const activeLessons = state.lessons.filter((l) => l.status === "active");
|
|
@@ -2684,8 +2688,8 @@ function formatFullExport(state, format) {
|
|
|
2684
2688
|
lines.push("## Lessons");
|
|
2685
2689
|
for (const l of activeLessons) {
|
|
2686
2690
|
const reinforced = l.reinforcements > 0 ? ` (\xD7${l.reinforcements})` : "";
|
|
2687
|
-
const tagInfo = l.tags.length > 0 ? ` [${l.tags.join(", ")}]` : "";
|
|
2688
|
-
lines.push(`- ${l.id}: ${
|
|
2691
|
+
const tagInfo = l.tags.length > 0 ? ` [${l.tags.map(escapeMarkdownDocument).join(", ")}]` : "";
|
|
2692
|
+
lines.push(`- ${l.id}: ${escapeMarkdownDocument(l.title)}${reinforced}${tagInfo}`);
|
|
2689
2693
|
}
|
|
2690
2694
|
}
|
|
2691
2695
|
const blockers = state.roadmap.blockers;
|
|
@@ -2694,7 +2698,7 @@ function formatFullExport(state, format) {
|
|
|
2694
2698
|
lines.push("## Blockers");
|
|
2695
2699
|
for (const b of blockers) {
|
|
2696
2700
|
const cleared = isBlockerCleared(b) ? "[x]" : "[ ]";
|
|
2697
|
-
lines.push(`${cleared} ${
|
|
2701
|
+
lines.push(`${cleared} ${escapeMarkdownDocument(b.name)}${b.note ? ` \u2014 ${escapeMarkdownDocument(b.note)}` : ""}`);
|
|
2698
2702
|
}
|
|
2699
2703
|
}
|
|
2700
2704
|
return lines.join("\n");
|
|
@@ -3195,8 +3199,8 @@ function handleNodeList(ctx) {
|
|
|
3195
3199
|
}
|
|
3196
3200
|
const lines = ["# Nodes", ""];
|
|
3197
3201
|
const header = "| Name | Path | Stack | Role | Health | Depends On |";
|
|
3198
|
-
const
|
|
3199
|
-
lines.push(header,
|
|
3202
|
+
const sep6 = "|------|------|-------|------|--------|------------|";
|
|
3203
|
+
lines.push(header, sep6);
|
|
3200
3204
|
for (const [name, node] of entries) {
|
|
3201
3205
|
const path2 = node.path ?? "";
|
|
3202
3206
|
const stack = node.stack ?? "";
|
|
@@ -3555,7 +3559,7 @@ var init_session_types = __esm({
|
|
|
3555
3559
|
|
|
3556
3560
|
// src/autonomous/session-selector.ts
|
|
3557
3561
|
import { readdirSync, realpathSync as realpathSync4 } from "fs";
|
|
3558
|
-
import { basename as basename2, join as join8, sep } from "path";
|
|
3562
|
+
import { basename as basename2, join as join8, sep as sep2 } from "path";
|
|
3559
3563
|
function canonicalSessionsRoot(root) {
|
|
3560
3564
|
try {
|
|
3561
3565
|
return realpathSync4.native(sessionsRoot(root));
|
|
@@ -3566,7 +3570,7 @@ function canonicalSessionsRoot(root) {
|
|
|
3566
3570
|
function isContainedSessionDir(root, dir) {
|
|
3567
3571
|
const canonRoot = canonicalSessionsRoot(root);
|
|
3568
3572
|
if (canonRoot === null) return false;
|
|
3569
|
-
const rootPrefix = canonRoot.endsWith(
|
|
3573
|
+
const rootPrefix = canonRoot.endsWith(sep2) ? canonRoot : canonRoot + sep2;
|
|
3570
3574
|
let canonDir;
|
|
3571
3575
|
try {
|
|
3572
3576
|
canonDir = realpathSync4.native(dir);
|
|
@@ -5907,7 +5911,7 @@ var init_update_check = __esm({
|
|
|
5907
5911
|
// src/mcp/index.ts
|
|
5908
5912
|
init_esm_shims();
|
|
5909
5913
|
import { realpathSync as realpathSync8, existsSync as existsSync19 } from "fs";
|
|
5910
|
-
import { resolve as resolve13, join as join44, isAbsolute as
|
|
5914
|
+
import { resolve as resolve13, join as join44, isAbsolute as isAbsolute3 } from "path";
|
|
5911
5915
|
import { z as z14 } from "zod";
|
|
5912
5916
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5913
5917
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -7080,9 +7084,9 @@ import { join as join13 } from "path";
|
|
|
7080
7084
|
// src/autonomous/review-lenses/path-safety.ts
|
|
7081
7085
|
init_esm_shims();
|
|
7082
7086
|
import { realpathSync as realpathSync5 } from "fs";
|
|
7083
|
-
import { join as join12, resolve as resolve6, sep as
|
|
7087
|
+
import { join as join12, resolve as resolve6, sep as sep3 } from "path";
|
|
7084
7088
|
function resolveAndValidate(projectRoot, file) {
|
|
7085
|
-
const resolvedRoot = resolve6(projectRoot) +
|
|
7089
|
+
const resolvedRoot = resolve6(projectRoot) + sep3;
|
|
7086
7090
|
const fullPath = resolve6(join12(projectRoot, file));
|
|
7087
7091
|
if (!fullPath.startsWith(resolvedRoot)) return null;
|
|
7088
7092
|
let realPath;
|
|
@@ -9434,7 +9438,7 @@ import {
|
|
|
9434
9438
|
readFileSync as readFileSync8,
|
|
9435
9439
|
realpathSync as realpathSync7
|
|
9436
9440
|
} from "fs";
|
|
9437
|
-
import { resolve as resolve8, sep as
|
|
9441
|
+
import { resolve as resolve8, sep as sep5 } from "path";
|
|
9438
9442
|
import { createHash as createHash4 } from "crypto";
|
|
9439
9443
|
|
|
9440
9444
|
// src/autonomous/review-lenses/review-snapshot.ts
|
|
@@ -9452,7 +9456,7 @@ import {
|
|
|
9452
9456
|
existsSync as existsSync9,
|
|
9453
9457
|
statSync as statSync3
|
|
9454
9458
|
} from "fs";
|
|
9455
|
-
import { join as join16, dirname as dirname4, resolve as resolve7, sep as
|
|
9459
|
+
import { join as join16, dirname as dirname4, resolve as resolve7, sep as sep4, posix, win32 } from "path";
|
|
9456
9460
|
import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
|
|
9457
9461
|
import { spawnSync } from "child_process";
|
|
9458
9462
|
var SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -9524,13 +9528,13 @@ function assertValidManifestPath(entryPath, label) {
|
|
|
9524
9528
|
function assertNoSymlinkAncestors(targetPath, stopAt) {
|
|
9525
9529
|
const stopReal = resolve7(stopAt);
|
|
9526
9530
|
const target = resolve7(targetPath);
|
|
9527
|
-
if (!target.startsWith(stopReal +
|
|
9531
|
+
if (!target.startsWith(stopReal + sep4) && target !== stopReal) {
|
|
9528
9532
|
throw new Error(
|
|
9529
9533
|
`review-snapshot: assertNoSymlinkAncestors: target ${target} is not under stopAt ${stopReal}`
|
|
9530
9534
|
);
|
|
9531
9535
|
}
|
|
9532
9536
|
const suffix = target === stopReal ? "" : target.slice(stopReal.length + 1);
|
|
9533
|
-
const segments = suffix.length > 0 ? suffix.split(
|
|
9537
|
+
const segments = suffix.length > 0 ? suffix.split(sep4) : [];
|
|
9534
9538
|
let current = stopReal;
|
|
9535
9539
|
for (const segment of segments) {
|
|
9536
9540
|
current = join16(current, segment);
|
|
@@ -9571,14 +9575,14 @@ function deriveCanonicalPaths(projectRoot, sessionId, reviewId) {
|
|
|
9571
9575
|
}
|
|
9572
9576
|
const canonicalSessionsRoot2 = realpathSync6(sessionsDir);
|
|
9573
9577
|
const sessionDir2 = join16(canonicalSessionsRoot2, sessionId);
|
|
9574
|
-
if (!sessionDir2.startsWith(canonicalSessionsRoot2 +
|
|
9578
|
+
if (!sessionDir2.startsWith(canonicalSessionsRoot2 + sep4)) {
|
|
9575
9579
|
throw new Error(
|
|
9576
9580
|
`review-snapshot: sessionDir escapes sessions root: ${sessionDir2}`
|
|
9577
9581
|
);
|
|
9578
9582
|
}
|
|
9579
9583
|
const snapshotParent = join16(sessionDir2, "review-snapshot");
|
|
9580
9584
|
const finalDir = join16(snapshotParent, reviewId);
|
|
9581
|
-
if (!finalDir.startsWith(snapshotParent +
|
|
9585
|
+
if (!finalDir.startsWith(snapshotParent + sep4)) {
|
|
9582
9586
|
throw new Error(
|
|
9583
9587
|
`review-snapshot: finalDir escapes snapshot parent: ${finalDir}`
|
|
9584
9588
|
);
|
|
@@ -9603,7 +9607,7 @@ function readReviewSnapshotManifestWithBytes(projectRoot, sessionId, reviewId) {
|
|
|
9603
9607
|
);
|
|
9604
9608
|
}
|
|
9605
9609
|
const realFinalDir = realpathSync6(paths.finalDir);
|
|
9606
|
-
if (!realFinalDir.startsWith(paths.canonicalSessionsRoot +
|
|
9610
|
+
if (!realFinalDir.startsWith(paths.canonicalSessionsRoot + sep4) && realFinalDir !== paths.canonicalSessionsRoot) {
|
|
9607
9611
|
throw new Error(
|
|
9608
9612
|
`review-snapshot: finalDir escapes sessions root: ${paths.finalDir}`
|
|
9609
9613
|
);
|
|
@@ -9616,7 +9620,7 @@ function readReviewSnapshotManifestWithBytes(projectRoot, sessionId, reviewId) {
|
|
|
9616
9620
|
);
|
|
9617
9621
|
}
|
|
9618
9622
|
const realManifestPath = realpathSync6(manifestPath);
|
|
9619
|
-
if (!realManifestPath.startsWith(realFinalDir +
|
|
9623
|
+
if (!realManifestPath.startsWith(realFinalDir + sep4)) {
|
|
9620
9624
|
throw new Error(
|
|
9621
9625
|
`review-snapshot: manifest.json escapes finalDir: ${manifestPath}`
|
|
9622
9626
|
);
|
|
@@ -9661,7 +9665,7 @@ function readReviewSnapshotManifestWithBytes(projectRoot, sessionId, reviewId) {
|
|
|
9661
9665
|
for (const entry of manifest.files) {
|
|
9662
9666
|
assertValidManifestPath(entry.path, `manifest entry`);
|
|
9663
9667
|
const stored = join16(paths.finalDir, entry.path);
|
|
9664
|
-
if (!stored.startsWith(paths.finalDir +
|
|
9668
|
+
if (!stored.startsWith(paths.finalDir + sep4)) {
|
|
9665
9669
|
throw new Error(
|
|
9666
9670
|
`review-snapshot: manifest entry escapes finalDir: ${entry.path}`
|
|
9667
9671
|
);
|
|
@@ -9866,7 +9870,7 @@ function verifyPathAndLookup(evidence, snapshotDir, byPath) {
|
|
|
9866
9870
|
});
|
|
9867
9871
|
}
|
|
9868
9872
|
const resolved = resolve8(snapshotDir, evidence.file);
|
|
9869
|
-
const snapshotDirWithSep = snapshotDir.endsWith(
|
|
9873
|
+
const snapshotDirWithSep = snapshotDir.endsWith(sep5) ? snapshotDir : snapshotDir + sep5;
|
|
9870
9874
|
if (!resolved.startsWith(snapshotDirWithSep) && resolved !== snapshotDir) {
|
|
9871
9875
|
return failOutcome("invalid_path", {
|
|
9872
9876
|
file: evidence.file,
|
|
@@ -9921,7 +9925,7 @@ function verifyPayloadIntegrity(file, resolved, entry, snapshotDir, snapshotDirR
|
|
|
9921
9925
|
);
|
|
9922
9926
|
}
|
|
9923
9927
|
}
|
|
9924
|
-
const snapshotDirRealWithSep = dirReal.endsWith(
|
|
9928
|
+
const snapshotDirRealWithSep = dirReal.endsWith(sep5) ? dirReal : dirReal + sep5;
|
|
9925
9929
|
let realResolved;
|
|
9926
9930
|
try {
|
|
9927
9931
|
realResolved = realpathSync7(resolved);
|
|
@@ -16743,7 +16747,7 @@ function getInstalledVersion() {
|
|
|
16743
16747
|
}
|
|
16744
16748
|
}
|
|
16745
16749
|
function getRunningVersion() {
|
|
16746
|
-
return "1.4.
|
|
16750
|
+
return "1.4.4";
|
|
16747
16751
|
}
|
|
16748
16752
|
|
|
16749
16753
|
// src/autonomous/guide.ts
|
|
@@ -18753,7 +18757,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
18753
18757
|
const result = await runMcpReadTool(pinnedRoot, handleStatus);
|
|
18754
18758
|
try {
|
|
18755
18759
|
const { readUpdateCacheSync: readUpdateCacheSync2, refreshUpdateCacheInBackground: refreshUpdateCacheInBackground2 } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
|
|
18756
|
-
const running = "1.4.
|
|
18760
|
+
const running = "1.4.4";
|
|
18757
18761
|
const info = readUpdateCacheSync2(running);
|
|
18758
18762
|
refreshUpdateCacheInBackground2();
|
|
18759
18763
|
if (info?.updateAvailable && result.content[0]?.type === "text") {
|
|
@@ -18965,7 +18969,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
18965
18969
|
);
|
|
18966
18970
|
});
|
|
18967
18971
|
server.registerTool("storybloq_ticket_create", {
|
|
18968
|
-
description: "Create a new ticket",
|
|
18972
|
+
description: "Create a new ticket. ID assignment is serialized under the project lock, so concurrent creates that acquire the lock receive distinct sequential IDs.",
|
|
18969
18973
|
inputSchema: {
|
|
18970
18974
|
title: z12.string().describe("Ticket title"),
|
|
18971
18975
|
type: z12.enum(TICKET_TYPES).describe("Ticket type: task, feature, chore"),
|
|
@@ -19052,7 +19056,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
19052
19056
|
(root, format) => handleTicketMetaUnset(args.id, args.path, format, root)
|
|
19053
19057
|
));
|
|
19054
19058
|
server.registerTool("storybloq_issue_create", {
|
|
19055
|
-
description: "Create a new issue",
|
|
19059
|
+
description: "Create a new issue. ID assignment is serialized under the project lock, so concurrent creates that acquire the lock receive distinct sequential IDs.",
|
|
19056
19060
|
inputSchema: {
|
|
19057
19061
|
title: z12.string().describe("Issue title"),
|
|
19058
19062
|
severity: z12.enum(ISSUE_SEVERITIES).describe("Issue severity: critical, high, medium, low"),
|
|
@@ -19163,7 +19167,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
19163
19167
|
}
|
|
19164
19168
|
}, (args) => runMcpReadTool(pinnedRoot, (ctx) => handleNoteGet(args.id, ctx)));
|
|
19165
19169
|
server.registerTool("storybloq_note_create", {
|
|
19166
|
-
description: "Create a new note",
|
|
19170
|
+
description: "Create a new note. ID assignment is serialized under the project lock, so concurrent creates that acquire the lock receive distinct sequential IDs.",
|
|
19167
19171
|
inputSchema: {
|
|
19168
19172
|
content: z12.string().describe("Note content"),
|
|
19169
19173
|
title: z12.string().optional().describe("Note title"),
|
|
@@ -19227,7 +19231,7 @@ function registerAllTools(server, pinnedRoot) {
|
|
|
19227
19231
|
inputSchema: {}
|
|
19228
19232
|
}, () => runMcpReadTool(pinnedRoot, (ctx) => handleLessonDigest(ctx)));
|
|
19229
19233
|
server.registerTool("storybloq_lesson_create", {
|
|
19230
|
-
description: "Create a new lesson",
|
|
19234
|
+
description: "Create a new lesson. ID assignment is serialized under the project lock, so concurrent creates that acquire the lock receive distinct sequential IDs.",
|
|
19231
19235
|
inputSchema: {
|
|
19232
19236
|
title: z12.string().describe("Lesson title \u2014 concise lesson name"),
|
|
19233
19237
|
content: z12.string().describe("The actionable rule (1-3 sentences)"),
|
|
@@ -20112,12 +20116,12 @@ async function trimFailedDirectory(inboxPath) {
|
|
|
20112
20116
|
var ENV_VAR2 = "STORYBLOQ_PROJECT_ROOT";
|
|
20113
20117
|
var LEGACY_ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
|
|
20114
20118
|
var CONFIG_PATH2 = ".story/config.json";
|
|
20115
|
-
var version = "1.4.
|
|
20119
|
+
var version = "1.4.4";
|
|
20116
20120
|
function tryDiscoverRoot() {
|
|
20117
20121
|
const envRoot = process.env[ENV_VAR2] ?? process.env[LEGACY_ENV_VAR2];
|
|
20118
20122
|
const envName = process.env[ENV_VAR2] ? ENV_VAR2 : LEGACY_ENV_VAR2;
|
|
20119
20123
|
if (envRoot) {
|
|
20120
|
-
if (!
|
|
20124
|
+
if (!isAbsolute3(envRoot)) {
|
|
20121
20125
|
process.stderr.write(`Warning: ${envName} must be an absolute path, got: ${envRoot}
|
|
20122
20126
|
`);
|
|
20123
20127
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storybloq/storybloq",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
5
5
|
"description": "Project memory for Claude Code. Storybloq stores tickets, issues, handovers, and lessons in a .story/ folder inside your repo so every session starts where the last one left off. Pairs with the Storybloq Mac app on the App Store.",
|
|
6
6
|
"homepage": "https://storybloq.com",
|
package/src/skill/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ Invocation differs by client: use `/story` in Claude Code, `$story` in Codex, or
|
|
|
13
13
|
|
|
14
14
|
This guard runs on EVERY `/story` invocation regardless of subcommand (`/story`, `/story auto`, `/story review`, `/story plan`, `/story guided`, `/story handover`, `/story snapshot`, `/story export`, `/story design`, `/story review-lenses`, `/story settings`, `/story help`, `/story status`, etc.). It MUST complete before ANY other action in this invocation.
|
|
15
15
|
|
|
16
|
-
**Guard prelude: force-surface deferred MCP tools.** Before running step 1 of this guard, make a
|
|
16
|
+
**Guard prelude: force-surface deferred MCP tools.** Before running step 1 of this guard, make a `ToolSearch` call with `query: "storybloq"` and `max_results: 80` (high enough to surface the full `storybloq_*` tool set, currently ~53 tools, in one call; a smaller cap truncates alphabetically and can drop `storybloq_status`). On Claude Code desktop/web, `storybloq_*` tool schemas are deferred, so without this prelude the subsequent `storybloq_status` call in step 1 is not dispatchable. If `storybloq_status` is still not available after that call (some clients clamp or ignore `max_results`), make a second targeted `ToolSearch` with `query: "storybloq_status"` and `max_results: 5`, which ranks that exact tool to the top, before concluding anything about MCP availability. The prelude is explicitly part of the guard, not a separate pre-guard step; it satisfies the whitelist below.
|
|
17
17
|
|
|
18
18
|
- If `ToolSearch` itself is not available or returns an error on this harness, SKIP the prelude and continue to step 1. Do NOT treat a missing `ToolSearch` tool as evidence that MCP is unavailable — step 1's `storybloq_status` call will either succeed (MCP already surfaced) or its failure will route the skill to the Step 0 setup/CLI-fallback path below.
|
|
19
19
|
- The prelude is idempotent: on terminal CLI sessions where `storybloq_*` tools are already in the base list, it simply returns the same tool set.
|
|
@@ -62,7 +62,7 @@ Check if the storybloq MCP tools are available.
|
|
|
62
62
|
**Deferred tools note (Claude Code app).** Claude Code desktop/web may register MCP tools at session start but defer exposing their full schemas to your tool list until you explicitly request them. A naive "look for `storybloq_status` in available tools" check fails on a cold session even when the MCP server is healthy and connected, routing the skill to the CLI fallback unnecessarily. The Step 0.5 guard prelude above (the `ToolSearch` call) has already force-surfaced any deferred tools by this point, so this step only needs to check the current tool list:
|
|
63
63
|
|
|
64
64
|
1. **Check for storybloq MCP tools in your tool list.** If any `storybloq_*` tools (for example `storybloq_status`) are present, MCP is available -- proceed to Step 1.
|
|
65
|
-
2. **If no `storybloq_*` tools are present**, try a
|
|
65
|
+
2. **If no `storybloq_*` tools are present**, try a `ToolSearch` call with `query: "storybloq"` and `max_results: 80` (and, if `storybloq_status` is still not listed, a targeted `query: "storybloq_status"`, `max_results: 5`) as a safety net in case the guard prelude was skipped or failed silently. If the response lists any `storybloq_*` tools, proceed to Step 1.
|
|
66
66
|
3. **If `ToolSearch` is unavailable on this harness OR returned no matches**, MCP is genuinely unavailable -- continue with the setup/fallback path below. Missing `ToolSearch` is never by itself evidence that MCP is broken; it just means the harness exposes tools differently.
|
|
67
67
|
|
|
68
68
|
**If MCP tools are NOT available:**
|
|
@@ -138,18 +138,17 @@ Open with the project name and progress. Mention what the last session accomplis
|
|
|
138
138
|
|
|
139
139
|
You MUST show the following tables after the prose intro. Do not summarize them in paragraph form.
|
|
140
140
|
|
|
141
|
-
**Ready to Work table** -- call `storybloq_recommend` for context-aware suggestions.
|
|
141
|
+
**Ready to Work table** -- call `storybloq_recommend` for context-aware suggestions. `storybloq_recommend` MIXES tickets and issues, so render as a neutral markdown table:
|
|
142
142
|
|
|
143
143
|
```
|
|
144
144
|
## Ready to Work
|
|
145
|
-
|
|
|
146
|
-
|
|
147
|
-
| T-
|
|
148
|
-
|
|
|
149
|
-
| T-012 | Audit trail infrastructure | foundation |
|
|
145
|
+
| Item | Type | Title | Context |
|
|
146
|
+
|---------|--------|----------------------------------|----------------|
|
|
147
|
+
| T-011 | ticket | Rate agreement conditions schema | foundation |
|
|
148
|
+
| ISS-042 | issue | Auth token expiry bug | severity: high |
|
|
150
149
|
```
|
|
151
150
|
|
|
152
|
-
Show up to 5
|
|
151
|
+
Ticket rows show their phase in Context; issue rows show severity. Show up to 5 recommendations. If more exist, note "(+N more)". Note: tickets are filtered to unblocked ones, but issues are ranked by severity and have no blocker model, so a listed issue may be externally blocked -- verify it is actionable before starting.
|
|
153
152
|
|
|
154
153
|
**Decisions Pending** (show only if there are TBD items in CLAUDE.md or undecided tech choices):
|
|
155
154
|
|